Android 开发小作:Minofo(1)

来源:互联网 发布:黑魂三捏脸数据 编辑:程序博客网 时间:2024/06/06 14:12

记录了开发一个完整 Android 软件 Minofo 的整个过程,本系列博客包括两篇,本文是第一篇,主要内容包括如何解析一个 App,Toolbar 标题栏,NavigationView 导航栏以及悬浮按钮的实现,从而完成 Minofo 主界面的开发。本文提供 PDF 版本可供查阅及下载。

Android 开发小作:Minofo(1)

Android 开发小作:Minofo(2)

通过我个人网站 Nightn 阅读此篇博客效果更佳哦!

一、简介

学习 Android 开发已经一个月了,除了敲书本上的 demo,就只写过一个简易版的浏览器,为了巩固之前所学,我决定开发一个稍微完整点的具有 Material Design 风格的 App。浏览了手机上的 App,发现在 Material Design 风格的软件中,除了 Google 系列的 App,就只有印象笔记和 ofo 共享单车了(其他软件基本都是微信风格)。ofo 共享单车需要引入地图模块,实现起来更加有成就感,因此最终决定模仿 ofo 共享单车(主要是对界面的模仿)。

整个开发花了两天时间,期间遇到不少问题,以下便是对整个过程的梳理和总结,希望读者看完后能对 Android 开发有一个初步的认识,然后挥舞起键盘和本子,把灵感用代码一一实现。

话不多说,先看最终效果吧!

视频预览效果

  1. Apk 下载:点击下载 apk 或扫码下载

  1. Github 源码:https://github.com/nightn/minofo
  2. 声明:本产品用作分享与学习,若转载请注明出处,勿作任何商业用途。

二、如何解析一个 Apk

开发环境搭建什么的在此不再赘述,我使用的 IDE 是 Android Studio 2.2.3,JDK 版本是 1.8.0。要模仿一个 App,首先需要获取这个 Apk 的一些资源,如图片、布局文件。这个很简单,把下载下来的 Apk 文件后缀名改成 zip直接打开,相当于直接解压,如下图所示。

其中 res 目录存放的就是一个 App 所有的图片资源和布局文件了,找到其中需要的图片,放到 Android 项目的相应目录即可,下图便是我所选的部分图片资源。

当然,如果深入解析,比如通过 ApkTool 工具,还能获取一个 apk 可读的 AndroidManifest.xml(通过 zip 得到的 AndroidManifest.xml 是乱码的),这样就能查看它的 package name、Activity 组件、所需要的权限等信息了。如果你觉得这些信息还不够用,甚至还可以通过 dex2jar 工具反编译 dex 文件,获得它的 java 源代码,听起来是不是很厉害,不过这些代码中的命名都是经过处理的,基本上也读不懂。如果你想更深入地了解 Android 反编译技术,你可以参考这篇文章。

言归正传,在我们这个项目中,主要用到的只是 apk 中的图片资源,下面就一起来开启开发之旅吧!

三、主界面开发

先来观察一下 ofo 共享单车的主界面布局,在下图中,我们可以看到主界面包括三个部分,最上面的标题栏、中间的地图模块以及底下的三个悬浮按钮,非常的简洁明了。

然后通过点击左上角的导航按钮或者在屏幕上左滑,会从侧边出现一个导航栏,如下图所示,导航栏又分为上下两个部分,上部是导航栏的标题,下部是导航栏的菜单,菜单中的每一项都是一个 item。

从上面的分析来看,可以将主界面分为四个部分:标题栏、导航栏、悬浮按钮以及地图模块。下面对每个部分的进行分析和实现。

1. 标题栏实现

分析了这么多,从这开始就要真正动手写代码了。在 Android Studio 中新建项目,既然是精简版的 ofo,那就取名为 Minofo 吧,记得将之前解析 apk 包得到的图片资源拷贝到项目的 res/drawable-xxhdpi 目录下,准备好之后就可以编写 activity_main.xml 布局文件了。在此之前我们先分析一下 ofo 共享单车的标题栏。

可以看到标题栏和我们写 Android 程序常见的标题栏有很大区别,默认生成的标题栏是这样的:

这差距有点大啊,没关系,我们慢慢来实现。ofo 共享单车的标题栏从左到右分别是导航栏入口按钮、标题和右侧的活动中心入口按钮。而目前我们的标题栏是默认是由 ActionBar 控件实现的,ActionBar 不能实现很多 Material Design 的效果,而且 Google 也不建议开发者使用 ActionBar 了。因此我们使用 Toolbar 来实现标题栏。为了修改掉默认的 ActionBar,我们需要更改主题,主题是在 AndroidManifest.xml 文件中指定的,打开该文件,可以看到,theme 现在的值是 AppTheme。

<application        android:allowBackup="true"        android:icon="@mipmap/ic_launcher"        android:label="@string/app_name"        android:roundIcon="@mipmap/ic_launcher_round"        android:supportsRtl="true"        android:theme="@style/AppTheme">        ...    </application>

AppTheme 只是主题的代号,我们要找到它所对应的定义。打开 res/values/styles.xml 文件:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">        ...</style>

在这里指定了 AppTheme 的 parent 是 Theme.AppCompat.Light.DarkActionBar,DarkActionBar 就是默认的深色 ActionBar 主题,我们准备用 Toolbar 取代 ActionBar,在这里不需要 ActionBar,

因此将 parent 指定为 Theme.AppCompat.Light.NoActionBar 。现在 ActionBar 不见了,我们开始添加 Toolbar,修改 activity_main.xml 如下:

<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"             xmlns:app="http://schemas.android.com/apk/res-auto"             android:layout_width="match_parent"             android:layout_height="match_parent">    <android.support.v7.widget.Toolbar        android:id="@+id/toolbar"        android:layout_width="match_parent"        android:layout_height="?attr/actionBarSize"        android:background="@drawable/bg_top">        <ImageView            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:background="@drawable/actionbar_logo"/>    </android.support.v7.widget.Toolbar></FrameLayout>

简单分析一下上述 xml 代码,我们定义了一个帧布局 FrameLayout,在其中添加了一个 Toolbar 作为标题栏,由于 Toolbar 是由 appcompat-v7 提供的,不是自带的,因此我们需要使用它的全名,然后指定了这个 Toolbar 的 id 为 toolbar,宽度跟随父控件,高度定义为 ActionBar 的默认高度,并且指定了其背景,这里的 bg_top 是一张白底的图片,底部有一条阴影效果。然后在 Toolbar 里面,包含了一张图片,图片用 ImageView 显示,大小跟随图片的内容,这张图就是之前从 ofo 共享单车解析得到的标题栏图片。写完 toolbar 之后,还需要在 MainActivity.java 中的 onCreate() 方法中修改,如下:

 @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar);        setSupportActionBar(toolbar);    }

通过 Toolbar 的 id 创建实例,然后将实例传入 setSupportActionBar() 方法,使得 toolbar 具有和 ActionBar 一样的功能。但这样一来,标题栏会多出一个应用的名称:Minofo,这不是我们想要的效果,因为我们已经用了一张图片作为标题栏的标题了。为此,需要在 onCreate() 方法的 setSupportActionBar(toolbar) 后面增加一条语句:(多亏了刘忠良同学提供的思路。之前我的方法是将 app_name 设置成空字符串,这样虽然隐藏了标题,但 APP 的名字也没有了,所以不可取)

        getSupportActionBar().setTitle(""); //隐藏标题栏上的标题

最后,我们来看看运行效果。

效果还可以,不过标题栏上单单一个标题有点单调,下面把右侧的活动中心入口写写。右击 res 目录,选择 NewDirectory 新建一个名为 menu 的目录,然后在 menu 目录添加一个 Menu resourse file, 名为 toolbar_action.xml,这个文件就是用来在 toolbar 上放置一些动作按钮的,当然在此我们只需要一个通知按钮就可以了,编写代码如下:

<?xml version="1.0" encoding="utf-8"?><menu xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto">    <item        android:id="@+id/notification"        android:icon="@drawable/ic_notifications_active_black_24dp"        android:title="活动中心"        app:showAsAction="always" /></menu>

menu 中只有一个 item,设置相应的 id,icon 和 title,此处 showAsAction 设置成 always 的意思是让该 item 一直显示在 Toolbar 中,而不会显示在菜单中,如果屏幕空间不够就隐藏掉(就算是隐藏掉也不显示在菜单中,就是这么刚烈)。接下来要重写菜单被创建时的回调方法 onCreateOptionsMenu(),然后定义菜单的响应事件,就是按下这个菜单后会执行什么。在 MainActivity.java 中的 MainActivity 类中添加如下两个方法:

// 重写「菜单创建时的回调方法」public boolean onCreateOptionsMenu(Menu menu){  // 通过布局文件tool_action.xml创建Menu对象  getMenuInflater().inflate(R.menu.toolbar_action, menu);  return true; // 返回true表示允许创建的Menu对象显示}// 重写「菜单响应事件」public boolean onOptionsItemSelected(MenuItem item){  switch(item.getItemId()){  // 获取被选中项的id    case R.id.notification:      // 跳出toast提示      Toast.makeText(this, "活动中心正在建设中", Toast.LENGTH_SHORT).show();      break;    default:      break;  }  return true;}

onCreateOptionsMenu() 会在菜单创建时调用,主要是让之前定义的菜单显示出来,onOptionsItemSelected() 在菜单的某一项被选中时调用,然后执行响应逻辑,比如跳转到另外一个活动、跳出一个对话框等。此处为简单起见,跳出一个 Toast 提示,来看一下运行效果:

相比之前,标题栏上多了通知按钮,并且点击该按钮,会提示“活动中心正在建设中”。这样一来我们的标题栏就实现了。什么,你说这一小节有点水?哦对,标题栏还有导航栏入口按钮还没加呢,不过别着急,我们在介绍导航栏部分时再添加。对了,我们先把 app 的图标设置成 ofo 共享单车的样子,在 AndroidManifest.xml 通过设置 icon 和 roundIcon 属性,将 app 的图标改掉。效果如下:

你问这图片资源哪里来的?当然是之前解析 apk 得到的。好了,标题栏部分叙述完毕,开始实现导航栏部分吧。

2. 导航栏实现

导航栏是 Material Design 的一大特色,Google 开发的很多应用都有导航栏,比如最近比较火的谷歌翻译:

再比如向来都不火的我的个人网站:

它们无一不是采用了 Material 的设计风格,使得无论是 app 还是网站都更加出色。那下面我们就开始根据 ofo 共享单车,来实现一个 Material 风格的导航栏吧。

别看导航栏看起来好像很复杂,我们借助 Google 提供的工具,实现起来还是不难的,没错,就是用 Google 提供的 DrawerLayout 控件,DrawerLayout 作为一个布局,允许在其中放入两个子布局,第一个是主界面的内容,第二个就是可以滑动的导航栏菜单的内容,因此我们将之前的帧布局作为 DrawerLayout 的第一个子布局,另外,为了先预览一下 DrawerLayout 的效果,第二个子布局就用一个简单的 TextView 占个位,修改 activity_main.xml:

<?xml version="1.0" encoding="utf-8"?><android.support.v4.widget.DrawerLayout    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    android:id="@+id/drawer_layout"    android:layout_width="match_parent"    android:layout_height="match_parent">    <FrameLayout      ...    </FrameLayout>    <TextView        android:layout_width="match_parent"        android:layout_height="match_parent"        android:layout_gravity="start"        android:background="#FFF"        android:text="这是导航栏部分"        android:textSize="40dp"/></android.support.v4.widget.DrawerLayout>

可以看到,DrawerLayout 包含了两个控件,第一个 FrameLayout 就是之前所定义的,第二个是一个简单的文本显示控件。值得注意的地方是:(1)layout_gravity 属性必须要设置,它指定了导航栏出现的方式,如果未指定,那么导航栏就会直接把你的主界面覆盖掉,这里定义为 start 指的是根据系统语言(从左往右还是从右往左)判断导航栏菜单出现的方式,一般是从左往右出现。(2)background 的颜色设置为白色,如果不设置的话,默认是一种半透明的颜色。运行一下,看看 DrawerLayout 的效果:

通过从左往右滑动屏幕,即可调出导航栏。但是如果只有通过滑动屏幕才能调出导航栏的话,那么会出现很多用户根本不知道这个功能的现象,因此我们需要在 Toolbar 上加入一个导航栏入口按钮,这样就有两种方式调出导航栏了,修改 MainActivity.java 如下:

private DrawerLayout mDrawerLayout;@Overrideprotected void onCreate(Bundle savedInstanceState) {  //...  // 通过布局id找到DrawerLayout实例  mDrawerLayout = (DrawerLayout)findViewById(R.id.drawer_layout);  // 获取当前actionBar,这里的actionBar是我们之前由toolbar实现的  ActionBar actionBar = getSupportActionBar();  if(actionBar != null){    // 让导航栏入口按钮显示出来    actionBar.setDisplayHomeAsUpEnabled(true);    // 为导航栏入口按钮设置一个图标    actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);  }}public boolean onOptionsItemSelected(MenuItem item){  switch(item.getItemId()){  // 获取被选中项的id      //...    case android.R.id.home: // 导航栏入口按钮的响应事件      // 调出导航栏菜单      mDrawerLayout.openDrawer(GravityCompat.START);      break;      //...  }  return true;}

这样一来标题栏最左侧便多出了一个导航栏入口按钮了,通过点击该按钮也能调出导航栏。

导航栏的功能已经实现,下面便是往里面添加具体内容了,观察 ofo 共享单车的导航栏,如下图所示.

这个部分可以通过 Design Support 库提供的 NavigationView 控件来实现,另外由于这里还出现了圆形的头像,因此还需要一个图片圆形化的工具:CircleImageView。为了使用这两个控件,我们需要添加依赖库,在 app/build.gradle 中的 dependencies 闭包中添加依赖库:

dependencies{  compile 'com.android.support:design:24.2.1'  compile 'de.hdodenhof:circleimageview:2.1.0'}

当然,support 的具体版本依据你的 appcompat-v7 的版本确定,第三方库你也可以去开原网站查看最新版本。添加好依赖库,同步成功后,就可以开始完善导航栏的内容了。

在 res/menu 目录下新建 menu resource file,命名为 nav.xml,作为导航栏的 body,修改 nav.xml 的内容如下:

<?xml version="1.0" encoding="utf-8"?><menu xmlns:android="http://schemas.android.com/apk/res/android">    <group android:checkableBehavior="single">        <item            android:id="@+id/nav_receipt"            android:icon="@drawable/ic_receipt_white_24dp"            android:title="我的行程"/>        <item            android:id="@+id/nav_wallet"            android:icon="@drawable/ic_account_balance_wallet_white_24dp"            android:title="我的钱包" />        <item            android:id="@+id/nav_redeem"            android:icon="@drawable/redeem_icon"            android:title="输入优惠码" />        <item            android:id="@+id/nav_invite"            android:icon="@drawable/ic_share_black_24dp"            android:title="邀请赢奖" />        <item            android:id="@+id/nav_join"            android:icon="@drawable/ic_person_add_white_24dp"            android:title="加入共享" />        <item            android:id="@+id/nav_help"            android:icon="@drawable/ic_help_white_24dp"            android:title="使用指南" />        <item            android:id="@+id/nav_about"            android:icon="@drawable/ic_info_white_24dp"            android:title="关于" />    </group></menu>

然后添加导航栏的 header 部分,在 res/layout 目录下新建 layout resource file,命名为 header.xml,修改内容如下:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"                android:layout_width="match_parent"                android:layout_height="100dp"                android:padding="10dp"                android:background="?attr/colorPrimary">    <de.hdodenhof.circleimageview.CircleImageView        android:id="@+id/icon_image"        android:layout_width="70dp"        android:layout_height="70dp"        android:src="@drawable/default_avatar_fg"        android:layout_centerVertical="true"/>    <TextView        android:id="@+id/username"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_marginLeft="30dp"        android:layout_toRightOf="@id/icon_image"        android:layout_centerVertical="true"        android:text="ofo 新用户"        android:textColor="#40320D"        android:textSize="20sp"        /></RelativeLayout>

这部分用了一个相对布局,作为导航栏的 header,布局包括一个圆形头像,头像尺寸设为固定值;还包括一个昵称,设置好文字的内容,尺寸和颜色等信息。

导航栏的 body 和 header 都准备好,接下来将他们添加到 DrawerLayout 的第二个子布局,修改 activity_main.xml,用如下代码取代之前定义的 TextView。

<android.support.design.widget.NavigationView        android:id="@+id/nav_view"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:layout_gravity="start"        app:menu="@menu/nav"        app:headerLayout="@layout/header"        android:background="?attr/colorPrimary"/>

现在已经能够成功显示导航栏的内容了,效果如下图所示:

光有界面是不够的,导航栏里的 item 要能够点击并执行点什么才行。修改 MainActivity.java 中的代码,在 onCreate() 方法中加入以下代码:

NavigationView navView = (NavigationView)findViewById(R.id.nav_view);        navView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener(){            @Override            public boolean onNavigationItemSelected(MenuItem item){                mDrawerLayout.closeDrawers();                return true;            }        });

简单分析一下上述代码,先根据布局 id 获取 NavigationView 实例,然后设置监听事件,任何一个按钮被点击,都会通过 closeDrawers() 方法关闭导航栏。至于对于点击具体 item 执行什么事件,这是后续讨论的事情了。至此,导航栏部分已经全部实现。接下来实现主界面的三个悬浮按钮。

3. 悬浮按钮实现

观察 ofo 共享单车主界面,底下有三个按钮,它们都是悬浮在地图上面的,我们称之为悬浮按钮,悬浮按钮可以通过 Design Support 库提供的 FloatingActionButton 实现,用于之前我们已经引入了 Design Support 依赖库,在此我们直接可以使用 FloatingActionButton。

修改 activity_main.xml 文件,在之前定义的 DrawerLayout 中的第一个子布局 FrameLayout 中加入 3 个 FloatingActionButton,代码如下:

        <android.support.design.widget.FloatingActionButton            android:id="@+id/fab"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_gravity="bottom|end"            android:layout_margin="16dp"            android:src="@drawable/tiny_fab_right"            app:elevation="8dp"            android:scaleType="center"/>        <android.support.design.widget.FloatingActionButton            android:id="@+id/refresh"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_gravity="bottom|start"            android:layout_margin="16dp"            android:src="@drawable/homepage_refresh"            app:elevation="8dp"            android:scaleType="center"/>        <android.support.design.widget.FloatingActionButton            android:id="@+id/begin"            android:layout_width="100dp"            android:layout_height="100dp"            android:layout_gravity="bottom|center"            android:layout_margin="16dp"            android:src="@drawable/ridenow"            app:elevation="8dp"            android:scaleType="center"/>

除了用于设置布局位置的 layout_gravity 属性,三个悬浮按钮的属性基本差不多。另外需要注意到是 scaleType 属性需要设置为 center,这样才能保证图片将悬浮按钮铺满。然后我们为这三个悬浮按钮注册监听事件。由于按钮比较多,我们就不打算让每个按钮都去实现一遍监听事件,而是让 MainActivity 类实现 View.OnClickListener 接口,然后重写接口中的 onClick() 方法,在方法中通过判断按钮的 id 来设置对应按钮的点击事件。

在 onCreate() 方法中为三个悬浮按钮注册点击事件监听器:

        // 悬浮按钮注册监听器        begin = (FloatingActionButton)findViewById(R.id.begin);        refresh = (FloatingActionButton)findViewById(R.id.refresh);        fab = (FloatingActionButton)findViewById(R.id.fab);        begin.setOnClickListener(this);        refresh.setOnClickListener(this);        fab.setOnClickListener(this);

此处的 this 指的是 MainActivity,因为 MainActivity 类 implements 了 View.OnClickListener 接口,因此它具备了监听点击事件的能力,之后要做的便于重写接口中的 onClick 方法,在 MainActivity 类中添加如下代码:

public void onClick(View v){  switch(v.getId()){    case R.id.begin:{      //TODO 开始按钮点击事件      break;    }    case R.id.refresh:{      //TODO 刷新定位按钮点击事件      break;    }    case R.id.fab:{      //举报按钮点击事件      Toast.makeText(this, "举报功能正在完善", Toast.LENGTH_SHORT).show();      break;    }    default:      break;  }}

通过 View 的 getId() 方法获取当前被点击按钮的 id,然后根据 id 设置相应的点击逻辑,在这里还未加入过多的点击逻辑,但可以先分析一下,开始按钮 begin 点击后应该跳转至一个新的活动(即后面要讲到的用车界面),定位刷新按钮 refresh 点击之后会重新获取当前位置信息,并显示在主界面的地图模块中(地图模块将在下一节实现),所以这两个按钮的点击事件我们先用 TODO 注释占个坑,至于举报按钮 fab,我就不打算去实现它的点击事件了,所以在此加入一个简单的 Toast 提示语句。

好了,经过上面的开发过程,我们往主界面加入了标题栏、导航栏和悬浮按钮,功能已经基本完善了,下面需要加入地图显示模块,在此之前,我们先预览一下到目前为止的成果吧,如下图所示:

本来想通过一篇博客就把整个过程写完,现在发现前面的部分写得过于详细,以致于才写到一半就已经快 6000 字了。因此我将开发过程的后续内容又写了一篇 7000 字的博客,内容非常详尽,请参考Android 开发小作:Minofo(2) 这篇文章吧!

1 0
原创粉丝点击