Android學習之路——簡易版微信爲例(三)

最近很久沒有更新博文,一則是由於公司最近比較忙,另外本身在Android學習過程和簡易版微信的開發過程當中碰到了一些絆腳石,因此最近一直在學習充電中。下面來列舉一下本身所走過的彎路:html

(1)原本打算前端(即客戶端)和後端(即服務端)都由本身實現,後來發現服務端已經有成熟的程序可使用,如基於XMPP協議的OpenFire服務器程序;客戶端也已經有成熟的框架供咱們使用,如Smack,一樣基於XMPP協議。這一系列筆記式文章主要是記錄本身學習Android開發的過程,爲突出重點(Android的學習),故使用開源框架OpenStack + Smack組合。並且開源框架確定比你本身一我的寫出來的要好得多。前端

(2)對於Android初學者來講,自定義控件是一道坎,須要花大量時間去學習和嘗試。以前樓主也一直沒有接觸過自定義控件,因此在這段時間也作了初步的學習和嘗試。java

下面咱們首先對XMPP作一個簡單的介紹,並利用Smake框架改寫客戶端的登錄和註冊功能;接着實現主界面UI界面和初步交互。android

1 XMPP協議簡介

多臺計算機經過傳輸媒介(如:光纖、雙絞線、同軸電纜等)鏈接和傳輸信息,這是計算機網絡的硬件層;多臺計算機之間須要傳送信息,從一臺計算機到另外一臺計算機或從一臺計算機到多臺計算機,這就要定一個規則,這個規則就是協議,這是計算機網絡的軟件層。對軟件開發者來講,咱們幾乎無需研究鏈接介質,但須要了解協議,其中最重要的計算機互聯協議即是因特網的基礎——TCP/IP協議族。對底層系統開發者而言,須要關心底層的TCP協議、IP協議、UDP協議、CDMA/CD協議等應用無關的通用協議的實現;對應用軟件開發者而言,只須要了解底層協議,須要認真研究的是應用層協議,如:HTTP協議、FTP協議、SMTP協議等。git

HTTP(S)協議應該是最多見的應用層協議了,Web服務器和Web應用程序客戶端(即瀏覽器)之間通訊的規則就是由這個協議規定的。HTTP的服務器有Apache、Nginx、IIS或本身寫的HTTP服務器(若是你很牛的話)等;HTTP協議的客戶端就是瀏覽器或本身寫的HTTP客戶端解析程序(藉助於開源Http庫),負責解析服務端發過來的HTML、CSS、JavaScript或其餘內容,並向服務器發送請求數據。github

和HTTP協議同樣,XMPP是即時通訊應用層協議,定義了即時通訊客戶端與服務器端的數據傳輸格式及各字段的含義。XMPP協議有不少服務器端程序和客戶端程序(庫)的實現,本系列博文使用的OpenFire就是XMPP協議服務器程序的Java實現,Smack是客戶端庫,這些程序(庫)都是開源的。OpenFire能夠直接下載二進制包安裝,也能夠下載源代碼、而後用Eclipse編譯以後運行。只要部署好OpenFire服務器以後,基本就不用管它了。對於Smock客戶端程序庫,若是使用Android Studio的話,根據github說明,配置gradle文件便可。canvas

有了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文檔、源代碼和互聯網資料,一點點敲代碼,總有一天可以學會不少的,下次學習筆記講介紹好友的添加及好友列表的顯示!

相關文章
相關標籤/搜索