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

时间:2022-10-12 17:45:43

最近好久没有更新博文,一则是因为公司最近比较忙,另外自己在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的结构如下:

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

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

2 登陆、注册的重新实现

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

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

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

public AbstractXMPPConnection connect()
throws SmackException,
IOException,
XMPPException

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

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

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

 /**
* Single instance, for manage XMPP connection.
*/
public class XMPPConnectionManager { private static AbstractXMPPConnection mInstance;
private static String HOST_ADDRESS = "192.168.1.111";
private static String HOST_NAME = "doll-pc";
private static int PORT = 5222; public static AbstractXMPPConnection getInstance() {
if (mInstance == null) {
openConnection();
}
return mInstance;
} private static boolean openConnection() {
XMPPTCPConnectionConfiguration config = XMPPTCPConnectionConfiguration.builder()
.setHost(HOST_ADDRESS)
.setPort(PORT)
.setServiceName(HOST_ADDRESS)
.setDebuggerEnabled(true)
.setSecurityMode(ConnectionConfiguration.SecurityMode.disabled)
.build();
mInstance = new XMPPTCPConnection(config);
try {
mInstance.connect();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}

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

2.1 登陆功能

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

 public class LoginAsyncTask extends AsyncTask<String, Void, Boolean> {

     private ProgressDialog mDialog;
private Context mContext; public LoginAsyncTask(Context context) {
mDialog = new ProgressDialog(context);
mDialog.setTitle("提示信息");
mDialog.setMessage("正在登录,请稍等...");
mDialog.show(); mContext = context;
} @Override
protected void onPreExecute() {
super.onPreExecute();
if (!mDialog.isShowing()) {
mDialog.show();
}
} @Override
protected Boolean doInBackground(String... params) {
AbstractXMPPConnection connection = XMPPConnectionManager.getInstance();
try {
connection.login(params[0], params[1]);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
} @Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
if (mDialog.isShowing()) mDialog.dismiss();
if (result) {
// jump to the Main page
Intent intent = new Intent(mContext, MainActivity.class);
mContext.startActivity(intent);
} else {
Toast.makeText(mContext, "登录失败!", Toast.LENGTH_LONG).show();
}
}
}

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

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

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

2.2 注册功能

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

 public class RegisterAsyncTask extends AsyncTask<String, Void, Boolean> {

     private ProgressDialog mDialog;
private Context mContext; public RegisterAsyncTask(Context context) {
mDialog = new ProgressDialog(context);
mDialog.setTitle("提示信息");
mDialog.setMessage("正在注册,请稍等..."); mContext = context;
} @Override
protected void onPreExecute() {
super.onPreExecute();
if (!mDialog.isShowing()) {
mDialog.show();
}
} @Override
protected Boolean doInBackground(String... params) { AbstractXMPPConnection connection = XMPPConnectionManager.getInstance();
AccountManager ac = AccountManager.getInstance(connection);
try {
ac.createAccount(params[0], params[1]);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
} @Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
if (mDialog.isShowing()) mDialog.dismiss();
if (result) {
// jump to Main page
Intent intent = new Intent(mContext, MainActivity.class);
mContext.startActivity(intent);
} else {
Toast.makeText(mContext, "注册失败!", Toast.LENGTH_LONG).show();
}
}
}

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

 @Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_press_register:
String [] params = new String[3];
params[0] = mEditTxtPhoneNumber.getText().toString().trim();
params[1] = mEdtTxtPassword.getText().toString().trim();
params[2] = mEdtTxtNickName.getText().toString().trim(); try {
new RegisterAsyncTask(this).execute(params);
} catch (Exception e) {
e.printStackTrace();
}
break;
}
}

3 登陆后主界面

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

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

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

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

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

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中用到的选项菜单。

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

  • 定义菜单资源文件

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

 <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> <item
android:id="@+id/menu_main_activity_search"
android:icon="@mipmap/icon_menu_search"
android:title="@string/string_search"
app:showAsAction="always"
/> <item
android:icon="@mipmap/ic_group_chat"
android:title="@string/string_group_chat"
app:showAsAction="never"
/> <item
android:icon="@mipmap/icon_sub_menu_add"
android:title="@string/string_add_friend"
app:showAsAction="never"
/> <item
android:icon="@mipmap/ic_scan"
android:title="@string/string_scaning"
app:showAsAction="never"
/> <item
android:icon="@mipmap/ic_pay"
android:title="@string/string_make_pay"
app:showAsAction="never"
/> <item
android:icon="@mipmap/ic_helper"
android:title="@string/string_help"
app:showAsAction="never"
/> </menu>

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

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

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子类如下:

 public class DiscoveryFragment extends Fragment {

     public static DiscoveryFragment newInstance() {
DiscoveryFragment fragment = new DiscoveryFragment();
return fragment;
} @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_discovery, container, false);
} }

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

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

 private List<Fragment> GetFragments() {
List<Fragment> fragments = new ArrayList<>(); ChattingFragment chattingFragment = ChattingFragment.newInstance();
fragments.add(chattingFragment); ContactFragment contactFragment = ContactFragment.newInstance();
fragments.add(contactFragment); DiscoveryFragment discoveryFragment = DiscoveryFragment.newInstance();
fragments.add(discoveryFragment); MyselfFragment myselfFragment = MyselfFragment.newInstance();
fragments.add(myselfFragment); return fragments;
}

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中怎么获取这五个属性值呢?

② 获取属性值

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

 // Obtain the styled attribute from context
TypedArray typedArray = context.obtainStyledAttributes(
attrs, R.styleable.ChangeColorIconView); // traverse the obtained return value.
int n = typedArray.getIndexCount();
for (int i = 0; i < n; ++i) {
int attr = typedArray.getIndex(i);
switch (attr) {
case R.styleable.ChangeColorIconView_tab_icon:
BitmapDrawable drawable = (BitmapDrawable) typedArray.getDrawable(attr);
mIconBitmap = drawable.getBitmap();
break;
case R.styleable.ChangeColorIconView_text:
mText = typedArray.getString(attr);
break;
case R.styleable.ChangeColorIconView_text_size:
mTextSize = (int) typedArray.getDimension(attr, 12);
break;
case R.styleable.ChangeColorIconView_icon_color:
mIconColor = typedArray.getColor(attr,
context.getResources().getColor(R.color.colorPrimary));
break;
case R.styleable.ChangeColorIconView_tab_icon_inactive:
BitmapDrawable d = (BitmapDrawable) typedArray.getDrawable(attr);
mIconBitmapInActive = d.getBitmap();
break;
}
}
typedArray.recycle();

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

③ 重写onMeasure方法

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

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

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

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

     @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // determine the size of icon - a rect
int bitmapWidth = Math.min(
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
getMeasuredHeight() - getPaddingTop() - getPaddingBottom() - mTextBound.height()); int left = getMeasuredWidth() / 2 - bitmapWidth / 2;
int top = (getMeasuredHeight() - mTextBound.height()) / 2 - bitmapWidth / 2; mIconRect = new Rect(left, top, left + bitmapWidth, top + bitmapWidth);
}

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

④ 重写onDraw方法

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

 @Override
protected void onDraw(Canvas canvas) { // clear the old icon.
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.XOR); // draw an icon on the canvas
int foregroundAlpha = (int) (mIconAlpha * 255);
int backgroundAlpha = 255 - foregroundAlpha; drawBaseLayer(canvas, backgroundAlpha);
drawUpperLayer(canvas, foregroundAlpha);
}

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

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

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

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

 private void drawBaseLayer(Canvas canvas, int alpha) {
// draw icon
mPaint.setAlpha(alpha);
canvas.drawBitmap(mIconBitmapInActive, null, mIconRect, mPaint); // draw text
mPaint.setColor(getResources().getColor(android.R.color.darker_gray));
mPaint.setAlpha(alpha);
canvas.drawText(mText, mIconRect.centerX() - mTextBound.width() / 2,
mIconRect.bottom + mTextBound.height(), mPaint);
}

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

     public void setIconAlpha(double iconAlpha) {
mIconAlpha = iconAlpha;
invalidate();
}

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

2. 底部导航的实现

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

     <LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"> <com.doll.mychat.widget.ChangeColorIconWithTextView
android:id="@+id/nav_tab_record"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:padding="5dp"
app:tab_icon="@mipmap/icon_chat_main_nav_active"
app:tab_icon_inactive="@mipmap/icon_chat_main_nav_tab_inactive"
app:icon_color="@color/colorPrimary"
app:text="@string/string_nav_tab_wechat"
app:text_size="12sp"
/> <com.doll.mychat.widget.ChangeColorIconWithTextView
android:id="@+id/nav_tab_contact"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:padding="5dp"
app:tab_icon="@mipmap/icon_contact_main_nav_active"
app:tab_icon_inactive="@mipmap/icon_contact_main_nav_inactive"
app:icon_color="@color/colorPrimary"
app:text="@string/string_nav_tab_contact"
app:text_size="12sp"
/> <com.doll.mychat.widget.ChangeColorIconWithTextView
android:id="@+id/nav_tab_discovery"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:padding="5dp"
app:tab_icon="@mipmap/icon_discovery_main_nav_active"
app:tab_icon_inactive="@mipmap/icon_discovery_main_nav_inactive"
app:icon_color="@color/colorPrimary"
app:text="@string/string_nav_bar_discovery"
app:text_size="12sp"
/> <com.doll.mychat.widget.ChangeColorIconWithTextView
android:id="@+id/nav_tab_myself"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:padding="5dp"
app:tab_icon="@mipmap/icon_myself_main_nav_active"
app:tab_icon_inactive="@mipmap/icon_myself_main_nav_inactive"
app:icon_color="@color/colorPrimary"
app:text="@string/string_nav_tab_myself"
app:text_size="12sp"
/> </LinearLayout>

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

     private void initTabIndicator() {
ChangeColorIconWithTextView one = (ChangeColorIconWithTextView) findViewById(
R.id.nav_tab_record);
ChangeColorIconWithTextView two = (ChangeColorIconWithTextView) findViewById(
R.id.nav_tab_contact);
ChangeColorIconWithTextView three = (ChangeColorIconWithTextView) findViewById(
R.id.nav_tab_discovery);
ChangeColorIconWithTextView four = (ChangeColorIconWithTextView) findViewById(
R.id.nav_tab_myself); mTabList.add(one);
mTabList.add(two);
mTabList.add(three);
mTabList.add(four); one.setOnClickListener(this);
two.setOnClickListener(this);
three.setOnClickListener(this);
four.setOnClickListener(this); one.setIconAlpha(1.0f);
}

点击事件回调函数如下:

     @Override
public void onClick(View v) { deselectAllTabs(); switch (v.getId()) {
case R.id.nav_tab_record:
selectTab(0);
break;
case R.id.nav_tab_contact:
selectTab(1);
break;
case R.id.nav_tab_discovery:
selectTab(2);
break;
case R.id.nav_tab_myself:
selectTab(3);
break;
}
} private void selectTab(int tabIndex) {
mTabList.get(tabIndex).setIconAlpha(1.0);
mMainViewPager.setCurrentItem(tabIndex);
} private void deselectAllTabs() {
for (ChangeColorIconWithTextView v : mTabList) {
v.setIconAlpha(0.0);
}
}

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

         mMainViewPager.clearOnPageChangeListeners();
mMainViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (positionOffset > 0) {
mTabList.get(position).setIconAlpha(1 - positionOffset);
mTabList.get(position + 1).setIconAlpha(positionOffset);
}
} @Override
public void onPageSelected(int position) {} @Override
public void onPageScrollStateChanged(int state) {}
});

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

4 总结

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

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