Android学习之路——简易版微信为例(三)

最近好久没有更新博文,一则是因为公司最近比较忙,另外自己在Android学习过程和简易版微信的开发过程中碰到了一些绊脚石,所以最近一直在学习充电中。下面来列举一下自己所走过的弯路:

(1)本来打算前端(即客户端)和后端(即服务端)都由自己实现,后来发现服务端已经有成熟的程序可以使用,如基于XMPP协议的OpenFire服务器程序;客户端也已经有成熟的框架供我们使用,如Smack,同样基于XMPP协议。这一系列笔记式文章主要是记录自己学习Android开发的过程,为突出重点(Android的学习),故使用开源框架OpenStack + Smack组合。而且开源框架肯定比你自己一个人写出来的要好得多。

(2)对于Android初学者来说,自定义控件是一道坎,需要花大量时间去学习和尝试。之前楼主也一直没有接触过自定义控件,所以在这段时间也做了初步的学习和尝试。

下面我们首先对XMPP做一个简单的介绍,并利用Smake框架改写客户端的登陆和注册功能;接着实现主界面UI界面和初步交互。

1 XMPP协议简介

多台计算机通过传输媒介(如:光纤、双绞线、同轴电缆等)连接和传输信息,这是计算机网络的硬件层;多台计算机之间需要传送信息,从一台计算机到另一台计算机或从一台计算机到多台计算机,这就要定一个规则,这个规则就是协议,这是计算机网络的软件层。对软件开发者来说,我们几乎无需研究连接介质,但需要了解协议,其中最重要的计算机互联协议便是因特网的基础——TCP/IP协议族。对底层系统开发者而言,需要关心底层的TCP协议、IP协议、UDP协议、CDMA/CD协议等应用无关的通用协议的实现;对应用软件开发者而言,只需要了解底层协议,需要认真研究的是应用层协议,如:HTTP协议、FTP协议、SMTP协议等。

HTTP(S)协议应该是最常见的应用层协议了,Web服务器和Web应用程序客户端(即浏览器)之间通信的规则就是由这个协议规定的。HTTP的服务器有Apache、Nginx、IIS或自己写的HTTP服务器(如果你很牛的话)等;HTTP协议的客户端就是浏览器或自己写的HTTP客户端解析程序(借助于开源Http库),负责解析服务端发过来的HTML、CSS、JavaScript或其他内容,并向服务器发送请求数据。

和HTTP协议一样,XMPP是即时通信应用层协议,定义了即时通信客户端与服务器端的数据传输格式及各字段的含义。XMPP协议有很多服务器端程序和客户端程序(库)的实现,本系列博文使用的OpenFire就是XMPP协议服务器程序的Java实现,Smack是客户端库,这些程序(库)都是开源的。OpenFire可以直接下载二进制包安装,也可以下载源代码、然后用Eclipse编译之后运行。只要部署好OpenFire服务器之后,基本就不用管它了。对于Smock客户端程序库,如果使用Android Studio的话,根据github说明,配置gradle文件即可。

有了OpenFire服务器和Smack客户端,实现简易版微信应用就简单多了,我们不再需要编写服务端逻辑,也不需要定义和服务端交互的命令格式,只需要实现和Smack类库的交互逻辑以及界面显示逻辑即可。整个APP的结构如下:

 

关于XMPP协议的介绍就暂时说这一些,在开发过程中结合具体需求再做进一步深入。其实,我们也无需了解太多,因为OpenFire和Smack都已经封装的很好了,只需要了解一些最基本概念就足够了。

2 登陆、注册的重新实现

客户端的实现主要是基于Smock第三方程序库。使用Smack库来进行客户端逻辑的编写,第一件事就是建立一个XMPP连接,所以首先学习的是建立连接的类——XMPPConnection,其实这是一个接口,其实现类继承体系结构如下:

 

接触到的第一个方法就是建立XMPP连接的方法,签名如下:

public AbstractXMPPConnection connect()
                               throws SmackException,
                                      IOException,
                                      XMPPException

下面的代码片段可以建立一个到OpenFire服务器的XMPP连接:

1  // Create a connection to the igniterealtime.org XMPP server.
2  XMPPTCPConnection con = new XMPPTCPConnection("igniterealtime.org");
3  // Connect to the server
4  con.connect();

一般来说,连接只需要建立一次即可,可以使用单例模式来实现,为此写了XMPPConnectionManager类来创建和管理连接:

 1 /**
 2  * Single instance, for manage XMPP connection.
 3  */
 4 public class XMPPConnectionManager {
 5 
 6     private static AbstractXMPPConnection mInstance;
 7     private static String HOST_ADDRESS = "192.168.1.111";
 8     private static String HOST_NAME    = "doll-pc";
 9     private static int PORT            = 5222;
10 
11     public static AbstractXMPPConnection getInstance() {
12         if (mInstance == null) {
13             openConnection();
14         }
15         return mInstance;
16     }
17 
18     private static boolean openConnection() {
19         XMPPTCPConnectionConfiguration config = XMPPTCPConnectionConfiguration.builder()
20                 .setHost(HOST_ADDRESS)
21                 .setPort(PORT)
22                 .setServiceName(HOST_ADDRESS)
23                 .setDebuggerEnabled(true)
24                 .setSecurityMode(ConnectionConfiguration.SecurityMode.disabled)
25                 .build();
26         mInstance = new XMPPTCPConnection(config);
27         try {
28             mInstance.connect();
29             return true;
30         } catch (Exception e) {
31             e.printStackTrace();
32             return false;
33         }
34     }
35 }
View Code

这样,一旦需要使用XMPP连接,只需要调用XMPPConnectionManager的getInstance方法即可。

2.1 登陆功能

有了XMPP连接,登陆功能就变得十分简单了,只需要调用AbstractXMPPConnection的成员方法login,传入用户名密码即可,这样实现用户登录的异步任务如下:

 1 public class LoginAsyncTask extends AsyncTask<String, Void, Boolean> {
 2 
 3     private ProgressDialog mDialog;
 4     private Context mContext;
 5 
 6     public LoginAsyncTask(Context context) {
 7         mDialog = new ProgressDialog(context);
 8         mDialog.setTitle("提示信息");
 9         mDialog.setMessage("正在登录,请稍等...");
10         mDialog.show();
11 
12         mContext = context;
13     }
14 
15     @Override
16     protected void onPreExecute() {
17         super.onPreExecute();
18         if (!mDialog.isShowing()) {
19             mDialog.show();
20         }
21     }
22 
23     @Override
24     protected Boolean doInBackground(String... params) {
25         AbstractXMPPConnection connection = XMPPConnectionManager.getInstance();
26         try {
27             connection.login(params[0], params[1]);
28             return true;
29         } catch (Exception e) {
30             e.printStackTrace();
31             return false;
32         }
33     }
34 
35     @Override
36     protected void onPostExecute(Boolean result) {
37         super.onPostExecute(result);
38         if (mDialog.isShowing())    mDialog.dismiss();
39         if (result) {
40             // jump to the Main page
41             Intent intent = new Intent(mContext, MainActivity.class);
42             mContext.startActivity(intent);
43         } else {
44             Toast.makeText(mContext, "登录失败!", Toast.LENGTH_LONG).show();
45         }
46     }
47 }
View Code

在点击登录按钮监听器的回调函数中实例化上述异步任务,传入用户名和密码字符串数组,如下:

 1         mLoginButton.setOnClickListener(new View.OnClickListener() {
 2             @Override
 3             public void onClick(View v) {
 4                 Log.d("OnClick", "Enter the click callback of Login Button");
 5 
 6                 String params[] = new String[2];
 7                 params[0] = mEditTextUserName.getText().toString().trim();
 8                 params[1] = mEditTextPassword.getText().toString().trim();
 9 
10                 new LoginAsyncTask(LoginActivity.this).execute(params);
11             }
12         });
View Code

短短的几行代码,便实现了登录的基本功能。

2.2 注册功能

注册功能的实现也非常简单,这里用到了AccountManager类来实现注册,注意这是一个单例。下述代码实现了注册的异步任务调用:

 1 public class RegisterAsyncTask extends AsyncTask<String, Void, Boolean> {
 2 
 3     private ProgressDialog mDialog;
 4     private Context mContext;
 5 
 6     public RegisterAsyncTask(Context context) {
 7         mDialog = new ProgressDialog(context);
 8         mDialog.setTitle("提示信息");
 9         mDialog.setMessage("正在注册,请稍等...");
10 
11         mContext = context;
12     }
13 
14     @Override
15     protected void onPreExecute() {
16         super.onPreExecute();
17         if (!mDialog.isShowing()) {
18             mDialog.show();
19         }
20     }
21 
22     @Override
23     protected Boolean doInBackground(String... params) {
24 
25         AbstractXMPPConnection connection = XMPPConnectionManager.getInstance();
26         AccountManager ac = AccountManager.getInstance(connection);
27         try {
28             ac.createAccount(params[0], params[1]);
29             return true;
30         } catch (Exception e) {
31             e.printStackTrace();
32             return false;
33         }
34     }
35 
36     @Override
37     protected void onPostExecute(Boolean result) {
38         super.onPostExecute(result);
39         if (mDialog.isShowing())    mDialog.dismiss();
40         if (result) {
41             // jump to Main page
42             Intent intent = new Intent(mContext, MainActivity.class);
43             mContext.startActivity(intent);
44         } else {
45             Toast.makeText(mContext, "注册失败!", Toast.LENGTH_LONG).show();
46         }
47     }
48 }
View Code

同样,在RegisterActivity中注册相应监听器,代码如下:

 1 @Override
 2     public void onClick(View v) {
 3         switch (v.getId()) {
 4             case R.id.btn_press_register:
 5                 String [] params = new String[3];
 6                 params[0] = mEditTxtPhoneNumber.getText().toString().trim();
 7                 params[1] = mEdtTxtPassword.getText().toString().trim();
 8                 params[2] = mEdtTxtNickName.getText().toString().trim();
 9 
10                 try {
11                     new RegisterAsyncTask(this).execute(params);
12                 } catch (Exception e) {
13                     e.printStackTrace();
14                 }
15                 break;
16         }
17     }
View Code

3 登陆后主界面

下面正式进入本篇博文的主体内容——登录后主界面的UI显示与基本交互逻辑。首先来看看登陆后的主界面UI的运行效果,基本和微信是一样的:

主界面分为三个部分,分别为顶部的ActionBar(也可以用ToolBar)、底部的标签导航Tab Navigation、以及中间的主体内容部分,如下图所示:

接下来的三个小节,我们就分别来介绍这三个部分的具体实现。由于内容较多,关于一些很基础的内容,介绍的可能会比较简单。

3.1 顶部的ActionBar

现在所有App的顶部都会有一个Action Bar,直译就是操作条,这是在Android SDK 3.0引入的。在Android SDK 5.0中,为了使用更为灵活,谷歌又提供了更为灵活的Toolbar,直译为工具条。无论是ActionBar还是ToolBar,其主要是提供选项菜单菜单,供用户点击触发执行相应操作,类似于Windows应用程序中的工具栏。除此之外,Action Bar还支持回退操作、Logo和Title显示、添加Spinner下拉式导航等功能,详细内容请参考谷歌官方文档,这一小节我们只关注本文实现所用到的一些知识点:

1. 如何得到ActionBar实例

为了使用ActionBar,首先要得到其实例。Action Bar的实例不能由我们直接new出来;也不是声明在布局文件中,所以不能通过findViewById的方式获得Action Bar的实例。要想在Activity中得到ActionBar的实例,必须让我们的Activity继承自AppCompatActivity或ActionActivity类(这应该是ActionBar最不灵活的地方之一),这两个类中都一提供一个方法:getSupportActionBar,来获取该Activity中ActionBar的实例。对,就这么简单,也就是这一句代码:

mActionBar = getSupportActionBar();

2. 如何为ActionBar设置属性值

通过上一点,我们可以知道ActionBar实例是由系统为我们生成好的,那么Action Bar中显示哪些内容、怎么显示这些内容,都是由系统根据一定规则确定的,那么该如何将我们需要的值设置给ActionBar呢?这里主要有两种方式:

(I)在Activity的onCreate中设置

这一方式是通过ActionBar的API来设置Action Bar的属性,例如标题、子标题、Logo、Icon、回退按钮等,上述主界面中,通过API可以设置ActionBar标题,如下:

mActionBar.setTitle(getResources().getString(R.string.string_wechat));

(II)在配置文件中指定

 通过ActionBar的API,我们可以可以设置一些部分数据,但这些数据如何在ActionBar中展示,则需要在style.xml文件中来定义;另外菜单项的定义也需要通过配置文件(也可以称为资源文件)来指定。首先,我们先来说说菜单的使用。
对于初学者来说,也许会觉得Android中菜单(Menu)涉及的内容似乎很多,就分类来说就有三种:选项菜单、上下文菜单和弹出式菜单。但其实这些菜单的使用基本是一样的。包括两个步骤:

(1)在res/menu目录下添加菜单声明文件;

(2)在Activity相应回调方法中将对应声明文件inflate出来,另外在Activity中也可以重写相应回调函数中,以实现各菜单项的想赢。

这部分的细节请参考谷歌的Android开发文档,上面对menu的介绍十分详细,本小节只阐述ActionBar中用到的选项菜单。

正如刚才所说,所有菜单的使用都分两步走,下面来看看选项菜单的这两步是怎么走的:

  • 定义菜单资源文件

先贴上本文所使用的选项菜单声明文件代码,然后分析其含义:

 1 <?xml version="1.0" encoding="utf-8"?>
 2 <menu xmlns:android="http://schemas.android.com/apk/res/android"
 3       xmlns:app="http://schemas.android.com/apk/res-auto">
 4 
 5     <item
 6         android:id="@+id/menu_main_activity_search"
 7         android:icon="@mipmap/icon_menu_search"
 8         android:title="@string/string_search"
 9         app:showAsAction="always"
10         />
11 
12     <item
13         android:icon="@mipmap/ic_group_chat"
14         android:title="@string/string_group_chat"
15         app:showAsAction="never"
16         />
17 
18     <item
19         android:icon="@mipmap/icon_sub_menu_add"
20         android:title="@string/string_add_friend"
21         app:showAsAction="never"
22         />
23 
24     <item
25         android:icon="@mipmap/ic_scan"
26         android:title="@string/string_scaning"
27         app:showAsAction="never"
28         />
29 
30     <item
31         android:icon="@mipmap/ic_pay"
32         android:title="@string/string_make_pay"
33         app:showAsAction="never"
34         />
35 
36     <item
37         android:icon="@mipmap/ic_helper"
38         android:title="@string/string_help"
39         app:showAsAction="never"
40         />
41 
42 </menu>
View Code

 这个文件就两类结点——menu节点和item节点,其中menu节点相当于item结点的容器,这没有什么可以多说的;各菜单项数据在item节点中定义,item节点中前三个属性——id、icon、title——分别是标识符、图标和标题,如下图所示

showAsAction用来指定该菜单项是出现在ActionBar上还是出现在弹出菜单上,属性值可以设置为以下四种或它们的组合:

a) always:始终出现在ActionBar上;

b) never:永远不出现在ActionBar上,只出现在弹出的浮动菜单上;

c) ifRoom:如果ActionBar上有空间,则显示在ActionBar上,否则显示在弹出菜单上;

d) withText:前三个用于指定显示位置的,这个则用于指定是否显示标题的,如果带上此标签,则显示标题,否则不显示。

  • Activity中inflate上述定义的文件

其实menu的使用和UI布局是一模一样的:对UI布局来说,第一步也是在资源文件xml中声明UI布局,第二步则是在Activity的onCreate中将声明的UI布局inflate出来,并设置View的监听事件;菜单也一样,第一步就是如上面所说的定义menu菜单资源,第二步也是在Activity的onCreateOptionsMenu回调函数中inflate资源文件,代码如下:

@Override
    public boolean onCreateOptionsMenu(Menu menu) {
        setMenuIconVisible(menu, true);
        getMenuInflater().inflate(R.menu.menu_main_activity, menu);
        return super.onCreateOptionsMenu(menu);
    }

上述代码中,除了第4行inflate菜单资源外,还在第3行的函数调用中设置了菜单图标的可见性。这是因为在高版本的Android SDK中,默认情况下溢出菜单中的菜单项只显示菜单标题(title),而不显示图标(icon),要想将图标显示出来,只能通过反射的方式,具体逻辑如下:

private void setMenuIconVisible(Menu menu, boolean visible) {
        try {
            Class<?> clazz = Class.forName("android.support.v7.view.menu.MenuBuilder");
            Method method = clazz.getDeclaredMethod("setOptionalIconsVisible", boolean.class);

            method.setAccessible(true);
            method.invoke(menu, visible);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

经过了上述两步,便实现在Action Bar上显示选项菜单的功能。到此为止,我们以及将所需的数据统统都告诉系统了,系统会根据相应的主题和样式来显示ActionBar和溢出菜单项。当然,这些系统的主题或样式不一定符合我们的需求,所以需要对其进行重新定义。

关于Android的主题和样式,这也是一个比较宽泛的话题,作用相当于Web前端开发中的CSS。这一小节楼主就根据自己的理解作一个简单地说明:所谓样式,就是将UI布局文件View视图中的部分属性抽出来,定义在style.xml文件中,在UI布局文件中,通过android:style来引用style.xml中的相关条目;所谓主题,相当于样式的集合,用于控制整个App或某个Activity的样式。Android中内置了许许多多样式和主题,我们初学者最好能对其有一个大致的认识,在这里推荐两篇比较好的博文:

http://www.cnblogs.com/qianxudetianxia/p/3725466.html

http://www.cnblogs.com/qianxudetianxia/p/3996020.html

这两篇博文对常用的系统样式和主题做了归类和整理,虽然有点老,但还是值得一看的。简易版微信的主题继承自Theme.AppCompat.Light.DarkActionBar:

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

下面我们来看看这里重写的样式吧:

a) 修改顶部StatusBar的背景色

目前找到两种方式:

① 修改样式中的colorPrimaryDark,将其改为你需要的颜色,即:

<item name="colorPrimaryDark">your color</item>

② 修改android:statusBarColor,即:

<item name="android:statusBarColor">your color</item>

b) 修改Action Bar相关的属性

① 修改ActionBar的背景色

同样有两种方式:1)修改样式中的colorPrimary,设置为你需要的ActionBar背景色;2)单独设置ActionBar的背景色。为了不改变ActionBar的其他属性的样式,可以通过继承系统的ActionBar样式,如本文中定义ActionBar的背景色如下:

    <style name="ActionBar" parent="Base.Theme.AppCompat.Light.DarkActionBar">
        <item name="background">@color/colorActionBarBackground</item>
        <item name="android:background">@color/colorActionBarBackground</item>
    </style>

然后将此样式设置给actionBarStyle,如下:

<item name="actionBarStyle">@style/ActionBar</item>
<item name="android:actionBarStyle">@style/ActionBar</item>

② 修改溢出菜单按钮的图标

溢出菜单按钮本质就是一个ImageButton,改变其图标可以通过修改相应样式中的src属性来实现,同样要继承系统的样式,具体定义样式如下:

<style name="ActionButton.Overflow" parent="android:Widget.Holo.ActionButton.Overflow">
        <item name="android:src">@mipmap/icon_menu_add</item>
        <item name="android:padding">10dip</item>
        <item name="android:scaleType">fitCenter</item>
    </style>

将此样式设置给actionOverflowButtonStyle,如下:

<item name="actionOverflowButtonStyle">@style/ActionButton.Overflow</item>

③ 溢出菜单样式

- 菜单文本颜色修改

修改菜单文本颜色样式如下:

<style name="TextAppearance.PopupMenu" parent="android:TextAppearance.Holo.Widget.PopupMenu">
        <item name="android:textColor">@android:color/white</item>
</style>

并将上述样式赋值给android:textAppearanceLargePopupMenu,即:

<item name="android:textAppearanceLargePopupMenu">@style/TextAppearance.PopupMenu</item>

- 菜单弹出位置修改

修改溢出菜单的弹出位置,使其弹出来的时候,位于ActionBar之下的样式如下:

<style name="PopupMenu.Overflow" parent="Widget.AppCompat.Light.PopupMenu.Overflow">
    <item name="overlapAnchor">false</item>
</style>

并将此样式赋值给主题中的popupMenuStyle,如下:

<item name="popupMenuStyle">@style/PopupMenu.Toolbar</item>
<item name="android:popupMenuStyle">@style/PopupMenu.Toolbar</item>

这里我们还可以设置弹出菜单的左右偏移(dropdownHorizontalOffset)和上下偏移(dropdownVerticalOffset),但是设置这两个属性时,必须先设置overlapAnchor为false。

3.2 可滑动的Tab页实现

这部分采用的是ViewPager + Fragment的方式实现,即用Fragment填充ViewPager,下面进行详细介绍:

第一步先在UI布局文件中添加ViewPager:

<android.support.v4.view.ViewPager
        android:id="@+id/mainViewPager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

第二步获取ViewPager实例,并设置适配器Adapter和设置当前显示页面索引:

mMainViewPager = (ViewPager) this.findViewById(R.id.mainViewPager);
mMainViewPager.setAdapter(new MainPagerFragmentAdapter(fragments, getSupportFragmentManager()));
mMainViewPager.setCurrentItem(0);

第三步: Fragment列表

Fragment,直译过来就是片段,是从Android 3.0 SDK引入的,主要用于平板开发,当然手机客户端也是可以使用的。Fragment相当于一个子Activity,有它自己的UI布局,也有生命周期,也可以像Activity那样为View添加事件响应函数。通过Fragment,可以使UI的复用性更好,逻辑代码分布更合理。

我们的微信主界面的每个Tab页,都是一个Fragment。每个Fragment展示其对应的UI布局,每个Fragment有其自己的逻辑。和Activity的使用类似,要想给Fragment设置UI,需要继承Fragment,重写onCreateView来设置需要显示的UI,例如“发现”页面的Fragment子类如下:

 1 public class DiscoveryFragment extends Fragment {
 2 
 3     public static DiscoveryFragment newInstance() {
 4         DiscoveryFragment fragment = new DiscoveryFragment();
 5         return fragment;
 6     }
 7 
 8     @Override
 9     public View onCreateView(LayoutInflater inflater, ViewGroup container,
10                              Bundle savedInstanceState) {
11         // Inflate the layout for this fragment
12         return inflater.inflate(R.layout.fragment_discovery, container, false);
13     }
14 
15 }
View Code

现在没写实现逻辑,所以四个Fragment的实现大同小异,其余的Fragment就不做阐述了。

Fragment列表获取很简单,就是通过newInstance方法获得各Fragment实例,注意Fragment的顺序,代码如下:

 1 private List<Fragment> GetFragments() {
 2         List<Fragment> fragments = new ArrayList<>();
 3 
 4         ChattingFragment chattingFragment = ChattingFragment.newInstance();
 5         fragments.add(chattingFragment);
 6 
 7         ContactFragment contactFragment = ContactFragment.newInstance();
 8         fragments.add(contactFragment);
 9 
10         DiscoveryFragment discoveryFragment = DiscoveryFragment.newInstance();
11         fragments.add(discoveryFragment);
12 
13         MyselfFragment myselfFragment = MyselfFragment.newInstance();
14         fragments.add(myselfFragment);
15 
16         return fragments;
17     }
View Code

3.3 底部导航条的实现

1. 自定义View显示图标和文本

微信的底部导航条其实还是蛮复杂的,它不是图片(ImageView)+文字(TextView)的简单组合,然后均匀分布在一个LinearLayout中。因为当ViewPager滑动时,图标和文字的透明度不断改变的,所以需要用自定义View来实现颜色的实时变化。

1) 自定义View的第一步当然是继承View类:

public class ChangeColorIconWithTextView extends View

2) 在构造函数中获取用户提供的样式

这个对初学者来说有点复杂,分两小步:

① 控件自定义属性的声明

    <attr name="tab_icon" format="reference" />
    <attr name="tab_icon_inactive" format="reference" />
    <attr name="text" format="string" />
    <attr name="text_size" format="dimension" />
    <attr name="icon_color" format="color" />

    <declare-styleable name="ChangeColorIconView">
        <attr name="tab_icon" />
        <attr name="tab_icon_inactive" />
        <attr name="text" />
        <attr name="text_size" />
        <attr name="icon_color" />
    </declare-styleable>

使用此View时,用户可以为其指定5个属性,那在View中怎么获取这五个属性值呢?

② 获取属性值

在构造函数中获取,具体代码如下:

 1 // Obtain the styled attribute from context
 2         TypedArray typedArray = context.obtainStyledAttributes(
 3                 attrs, R.styleable.ChangeColorIconView);
 4 
 5         // traverse the obtained return value.
 6         int n = typedArray.getIndexCount();
 7         for (int i = 0; i < n; ++i) {
 8             int attr = typedArray.getIndex(i);
 9             switch (attr) {
10                 case R.styleable.ChangeColorIconView_tab_icon:
11                     BitmapDrawable drawable = (BitmapDrawable) typedArray.getDrawable(attr);
12                     mIconBitmap = drawable.getBitmap();
13                     break;
14                 case R.styleable.ChangeColorIconView_text:
15                     mText = typedArray.getString(attr);
16                     break;
17                 case R.styleable.ChangeColorIconView_text_size:
18                     mTextSize = (int) typedArray.getDimension(attr, 12);
19                     break;
20                 case R.styleable.ChangeColorIconView_icon_color:
21                     mIconColor = typedArray.getColor(attr,
22                             context.getResources().getColor(R.color.colorPrimary));
23                     break;
24                 case R.styleable.ChangeColorIconView_tab_icon_inactive:
25                     BitmapDrawable d = (BitmapDrawable) typedArray.getDrawable(attr);
26                     mIconBitmapInActive = d.getBitmap();
27                     break;
28             }
29         }
30         typedArray.recycle();
View Code

可以看到,通过Context获得TypedArray实例,然后逐一遍历,选择需要的属性值即可。这部分涉及的东西很多,本人功力还不够深厚,还需要慢慢深入,Android SDK里就是这么做的。

③ 重写onMeasure方法

自定义View,一般需要重写onMeasure和onDraw方法,有时也需要重写onLayout方法。其中,onMeasure方法用于测量待绘制的视图;onDraw方法用于往Canvas方法绘制视图;onLayout则用于布局视图,一般不需要重写。

下面来看看ChangeColorIconWithTextView的onMeasure的实现,已知条件如下图:

自定义View要绘制两部分内容:图标Icon和文本,并且一旦图标绘制区域确定了,文本的绘制区域也就定了,因此onMeasure阶段的任务就是确定图标的绘制区域——一个正方形区域Rect。根据上图,不难得到下述代码:

 1     @Override
 2     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 3 
 4         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 5 
 6         // determine the size of icon - a rect
 7         int bitmapWidth = Math.min(
 8                 getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
 9                 getMeasuredHeight() - getPaddingTop() - getPaddingBottom() - mTextBound.height());
10 
11         int left = getMeasuredWidth() / 2 - bitmapWidth / 2;
12         int top = (getMeasuredHeight() - mTextBound.height()) / 2 - bitmapWidth / 2;
13 
14         mIconRect = new Rect(left, top, left + bitmapWidth, top + bitmapWidth);
15     }
View Code

这段代码首先求出图片所在区域的边长,接着根据边长,可以很容易求出绘制区域的left坐标,同时right坐标也就确定了;注意top或bottom坐标在求解时需要减去文本部分的高度。可以看到整个onMeasure函数还是比较简单的。

④ 重写onDraw方法

这一步就是将图标以及文本绘制到Canvas的指定区域上,需要注意的是这里要绘制两层图像——底层图像和上层图像——并且,这两层图像之间按照一定的比例融合,融合系数(透明度Alpha)根据ViewPager中,页面所在位置而定,这一系数可以由外部提供。下面来看看绘制部分的代码:

 1 @Override
 2     protected void onDraw(Canvas canvas) {
 3 
 4         // clear the old icon.
 5         canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.XOR);
 6 
 7         // draw an icon on the canvas
 8         int foregroundAlpha = (int) (mIconAlpha * 255);
 9         int backgroundAlpha = 255 - foregroundAlpha;
10 
11         drawBaseLayer(canvas, backgroundAlpha);
12         drawUpperLayer(canvas, foregroundAlpha);
13     }
View Code

第一步:清空Canvas,为绘制做准备;

第二步:根据外部传入的透明度系数,求出上下层的Alpha系数;

第三步:绘制底层图像和上层图像。

其中,绘制底层图像代码如下:

 1 private void drawBaseLayer(Canvas canvas, int alpha) {
 2         // draw icon
 3         mPaint.setAlpha(alpha);
 4         canvas.drawBitmap(mIconBitmapInActive, null, mIconRect, mPaint);
 5 
 6         // draw text
 7         mPaint.setColor(getResources().getColor(android.R.color.darker_gray));
 8         mPaint.setAlpha(alpha);
 9         canvas.drawText(mText, mIconRect.centerX() - mTextBound.width() / 2,
10                 mIconRect.bottom + mTextBound.height(), mPaint);
11     }
View Code

前两行代码是根据onMeasure阶段得到的Rect区域往Canvas上绘制Icon位图;后三句代码是根据指定颜色绘制文本。绘制上层图像的方法是类似的,只不过颜色和位图资源不同。至此,可以改变透明度的Icon就做好了。当然,我们的ChangeColorIconWithTextView需要提供一个Set透明度的方法,如下:

1     public void setIconAlpha(double iconAlpha) {
2         mIconAlpha = iconAlpha;
3         invalidate();
4     }
View Code

设置了透明度后,调用invalidate函数,强制重绘。

2. 底部导航的实现

第一步:首先在UI布局文件中添加四个ChangeColorIconWithTextView,放在一个水平的LinearLayout中均匀排列:

 1     <LinearLayout
 2         android:layout_width="match_parent"
 3         android:layout_height="50dp">
 4 
 5         <com.doll.mychat.widget.ChangeColorIconWithTextView
 6             android:id="@+id/nav_tab_record"
 7             android:layout_width="0dp"
 8             android:layout_weight="1"
 9             android:layout_height="match_parent"
10             android:padding="5dp"
11             app:tab_icon="@mipmap/icon_chat_main_nav_active"
12             app:tab_icon_inactive="@mipmap/icon_chat_main_nav_tab_inactive"
13             app:icon_color="@color/colorPrimary"
14             app:text="@string/string_nav_tab_wechat"
15             app:text_size="12sp"
16             />
17 
18         <com.doll.mychat.widget.ChangeColorIconWithTextView
19             android:id="@+id/nav_tab_contact"
20             android:layout_width="0dp"
21             android:layout_weight="1"
22             android:layout_height="match_parent"
23             android:padding="5dp"
24             app:tab_icon="@mipmap/icon_contact_main_nav_active"
25             app:tab_icon_inactive="@mipmap/icon_contact_main_nav_inactive"
26             app:icon_color="@color/colorPrimary"
27             app:text="@string/string_nav_tab_contact"
28             app:text_size="12sp"
29             />
30 
31         <com.doll.mychat.widget.ChangeColorIconWithTextView
32             android:id="@+id/nav_tab_discovery"
33             android:layout_width="0dp"
34             android:layout_weight="1"
35             android:layout_height="match_parent"
36             android:padding="5dp"
37             app:tab_icon="@mipmap/icon_discovery_main_nav_active"
38             app:tab_icon_inactive="@mipmap/icon_discovery_main_nav_inactive"
39             app:icon_color="@color/colorPrimary"
40             app:text="@string/string_nav_bar_discovery"
41             app:text_size="12sp"
42             />
43 
44         <com.doll.mychat.widget.ChangeColorIconWithTextView
45             android:id="@+id/nav_tab_myself"
46             android:layout_width="0dp"
47             android:layout_height="match_parent"
48             android:layout_weight="1"
49             android:padding="5dp"
50             app:tab_icon="@mipmap/icon_myself_main_nav_active"
51             app:tab_icon_inactive="@mipmap/icon_myself_main_nav_inactive"
52             app:icon_color="@color/colorPrimary"
53             app:text="@string/string_nav_tab_myself"
54             app:text_size="12sp"
55             />
56 
57     </LinearLayout>
View Code

第二步:获取ChangeColorIconWithTextView的实例,存放在一个容器中,以便ViewPager滑动时设置透明度,并为其添加点击事件回调函数:

 1     private void initTabIndicator() {
 2         ChangeColorIconWithTextView one = (ChangeColorIconWithTextView) findViewById(
 3                 R.id.nav_tab_record);
 4         ChangeColorIconWithTextView two = (ChangeColorIconWithTextView) findViewById(
 5                 R.id.nav_tab_contact);
 6         ChangeColorIconWithTextView three = (ChangeColorIconWithTextView) findViewById(
 7                 R.id.nav_tab_discovery);
 8         ChangeColorIconWithTextView four = (ChangeColorIconWithTextView) findViewById(
 9                 R.id.nav_tab_myself);
10 
11         mTabList.add(one);
12         mTabList.add(two);
13         mTabList.add(three);
14         mTabList.add(four);
15 
16         one.setOnClickListener(this);
17         two.setOnClickListener(this);
18         three.setOnClickListener(this);
19         four.setOnClickListener(this);
20 
21         one.setIconAlpha(1.0f);
22     }
View Code

点击事件回调函数如下:

 1     @Override
 2     public void onClick(View v) {
 3 
 4         deselectAllTabs();
 5 
 6         switch (v.getId()) {
 7             case R.id.nav_tab_record:
 8                 selectTab(0);
 9                 break;
10             case R.id.nav_tab_contact:
11                 selectTab(1);
12                 break;
13             case R.id.nav_tab_discovery:
14                 selectTab(2);
15                 break;
16             case R.id.nav_tab_myself:
17                 selectTab(3);
18                 break;
19         }
20     }
21 
22     private void selectTab(int tabIndex) {
23         mTabList.get(tabIndex).setIconAlpha(1.0);
24         mMainViewPager.setCurrentItem(tabIndex);
25     }
26 
27     private void deselectAllTabs() {
28         for (ChangeColorIconWithTextView v : mTabList) {
29             v.setIconAlpha(0.0);
30         }
31     }
View Code

第三步:添加ViewPager滑动时的回调函数:

 1         mMainViewPager.clearOnPageChangeListeners();
 2         mMainViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
 3             @Override
 4             public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
 5                 if (positionOffset > 0) {
 6                     mTabList.get(position).setIconAlpha(1 - positionOffset);
 7                     mTabList.get(position + 1).setIconAlpha(positionOffset);
 8                 }
 9             }
10 
11             @Override
12             public void onPageSelected(int position) {}
13 
14             @Override
15             public void onPageScrollStateChanged(int state) {}
16         });
View Code

这样,一旦ViewPager滑动,便会触发ChangeColorIconWithTextView更新透明度,并重绘图像,从而实现滑动ViewPager时透明度实时改变的效果。

4 总结

这一次学习笔记中,记录的内容有点杂,毕竟是楼主苦练20多天之后的一些学习成果(当然平时要上班的哈,其实也就周末学学)。我们首先简单介绍了XMPP及其开源实现Openfire + Smack,并使用Smack三方库来改写了客户端登陆、注册功能的逻辑;接着实现了简易版微信的主界面,逐一介绍了ActionBar、ViewPager + Fragment和底部导航。介绍ActionBar时,引入了在系统Style的基础上自定义Style,实现系统组件的定制;实现底部导航时,介绍了自定义控件的基本实现步骤。

虽然这些东西看着不难,但是作为初学者,从头到尾一步步走下来还是需要一些精力的,尤其是Android的碎片化问题,有些问题更是让初学者一时摸不着头脑。不过没事,一点点学SDK文档、源代码和互联网资料,一点点敲代码,总有一天能够学会很多的,下次学习笔记讲介绍好友的添加及好友列表的显示!

原文地址:https://www.cnblogs.com/lijihong/p/5514191.html