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

  • 相关阅读:
    姐姐的vue(1)
    LeetCode 64. Minimum Path Sum 20170515
    LeetCode 56. 56. Merge Intervals 20170508
    LeetCode 26. Remove Duplicates from Sorted Array
    LeetCode 24. Swap Nodes in Pairs 20170424
    LeetCode 19. Remove Nth Node From End of List 20170417
    LeetCode No.9 Palindrome Number 20170410
    LeetCode No.8. String to Integer (atoi) 2017/4/10(补上一周)
    LeetCode No.7 Reverse Integer 2017/3/27
    LeetCode No.4 Median of Two Sorted Arrays 20170319
  • 原文地址:https://www.cnblogs.com/lijihong/p/5514191.html
Copyright © 2011-2022 走看看