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

  • 相关阅读:
    PHP中有多态么
    【Android】九宫格实现
    采用xshell链路本地虚拟机Linux
    读取资源文件的工具.
    dede织梦背景经常使用标签
    PHP第三个教训 PHP基本数据类型
    Linux经常使用的命令(必看)
    易Android登录Demo
    [2013山东ACM]省赛 The number of steps (可能DP,数学期望)
    web开发性能优化---UI接口章
  • 原文地址:https://www.cnblogs.com/lijihong/p/5514191.html
Copyright © 2020-2023  润新知