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

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

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

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

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

1 XMPP协议简介

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

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

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

有了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文档、源代码和互联网资料,一点点敲代码,总有一天可以学会不少的,下次学习笔记讲介绍好友的添加及好友列表的显示!

相关文章
相关标签/搜索