最近好久沒有更新博文,一則是因為公司最近比較忙,另外自己在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文檔、源代碼和互聯網資料,一點點敲代碼,總有一天能夠學會很多的,下次學習筆記講介紹好友的添加及好友列表的顯示!