最近好久没有更新博文,一则是因为公司最近比较忙,另外自己在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 }
这样,一旦需要使用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 }
在点击登录按钮监听器的回调函数中实例化上述异步任务,传入用户名和密码字符串数组,如下:
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 });
短短的几行代码,便实现了登录的基本功能。
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 }
同样,在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 }
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>
这个文件就两类结点——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 }
现在没写实现逻辑,所以四个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 }
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();
可以看到,通过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 }
这段代码首先求出图片所在区域的边长,接着根据边长,可以很容易求出绘制区域的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 }
第一步:清空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 }
前两行代码是根据onMeasure阶段得到的Rect区域往Canvas上绘制Icon位图;后三句代码是根据指定颜色绘制文本。绘制上层图像的方法是类似的,只不过颜色和位图资源不同。至此,可以改变透明度的Icon就做好了。当然,我们的ChangeColorIconWithTextView需要提供一个Set透明度的方法,如下:
1 public void setIconAlpha(double iconAlpha) { 2 mIconAlpha = iconAlpha; 3 invalidate(); 4 }
设置了透明度后,调用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>
第二步:获取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 }
点击事件回调函数如下:
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 }
第三步:添加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 });
这样,一旦ViewPager滑动,便会触发ChangeColorIconWithTextView更新透明度,并重绘图像,从而实现滑动ViewPager时透明度实时改变的效果。
4 总结
这一次学习笔记中,记录的内容有点杂,毕竟是楼主苦练20多天之后的一些学习成果(当然平时要上班的哈,其实也就周末学学)。我们首先简单介绍了XMPP及其开源实现Openfire + Smack,并使用Smack三方库来改写了客户端登陆、注册功能的逻辑;接着实现了简易版微信的主界面,逐一介绍了ActionBar、ViewPager + Fragment和底部导航。介绍ActionBar时,引入了在系统Style的基础上自定义Style,实现系统组件的定制;实现底部导航时,介绍了自定义控件的基本实现步骤。
虽然这些东西看着不难,但是作为初学者,从头到尾一步步走下来还是需要一些精力的,尤其是Android的碎片化问题,有些问题更是让初学者一时摸不着头脑。不过没事,一点点学SDK文档、源代码和互联网资料,一点点敲代码,总有一天能够学会很多的,下次学习笔记讲介绍好友的添加及好友列表的显示!