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


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

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

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

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

1 XMPP協議簡介

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

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

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

有了OpenFire服務器和Smack客戶端,實現簡易版微信應用就簡單多了,我們不再需要編寫服務端邏輯,也不需要定義和服務端交互的命令格式,只需要實現和Smack類庫的交互邏輯以及界面顯示邏輯即可。整個APP的結構如下:

 

關於XMPP協議的介紹就暫時說這一些,在開發過程中結合具體需求再做進一步深入。其實,我們也無需了解太多,因為OpenFire和Smack都已經封裝的很好了,只需要了解一些最基本概念就足夠了。

2 登陸、注冊的重新實現

客戶端的實現主要是基於Smock第三方程序庫。使用Smack庫來進行客戶端邏輯的編寫,第一件事就是建立一個XMPP連接,所以首先學習的是建立連接的類——XMPPConnection,其實這是一個接口,其實現類繼承體系結構如下:

 

接觸到的第一個方法就是建立XMPP連接的方法,簽名如下:

public AbstractXMPPConnection connect() throws SmackException, IOException, XMPPException

下面的代碼片段可以建立一個到OpenFire服務器的XMPP連接:

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

一般來說,連接只需要建立一次即可,可以使用單例模式來實現,為此寫了XMPPConnectionManager類來創建和管理連接:

 1 /**
 2  * Single instance, for manage XMPP connection.  3  */
 4 public class XMPPConnectionManager {  5 
 6     private static AbstractXMPPConnection mInstance;  7     private static String HOST_ADDRESS = "192.168.1.111";  8     private static String HOST_NAME    = "doll-pc";  9     private static int PORT            = 5222; 10 
11     public static AbstractXMPPConnection getInstance() { 12         if (mInstance == null) { 13  openConnection(); 14  } 15         return mInstance; 16  } 17 
18     private static boolean openConnection() { 19         XMPPTCPConnectionConfiguration config = XMPPTCPConnectionConfiguration.builder() 20  .setHost(HOST_ADDRESS) 21  .setPort(PORT) 22  .setServiceName(HOST_ADDRESS) 23                 .setDebuggerEnabled(true) 24  .setSecurityMode(ConnectionConfiguration.SecurityMode.disabled) 25  .build(); 26         mInstance = new XMPPTCPConnection(config); 27         try { 28  mInstance.connect(); 29             return true; 30         } catch (Exception e) { 31  e.printStackTrace(); 32             return false; 33  } 34  } 35 }
View Code

這樣,一旦需要使用XMPP連接,只需要調用XMPPConnectionManager的getInstance方法即可。

2.1 登陸功能

有了XMPP連接,登陸功能就變得十分簡單了,只需要調用AbstractXMPPConnection的成員方法login,傳入用戶名密碼即可,這樣實現用戶登錄的異步任務如下:

 1 public class LoginAsyncTask extends AsyncTask<String, Void, Boolean> {  2 
 3     private ProgressDialog mDialog;  4     private Context mContext;  5 
 6     public LoginAsyncTask(Context context) {  7         mDialog = new ProgressDialog(context);  8         mDialog.setTitle("提示信息");  9         mDialog.setMessage("正在登錄,請稍等..."); 10  mDialog.show(); 11 
12         mContext = context; 13  } 14 
15  @Override 16     protected void onPreExecute() { 17         super.onPreExecute(); 18         if (!mDialog.isShowing()) { 19  mDialog.show(); 20  } 21  } 22 
23  @Override 24     protected Boolean doInBackground(String... params) { 25         AbstractXMPPConnection connection = XMPPConnectionManager.getInstance(); 26         try { 27             connection.login(params[0], params[1]); 28             return true; 29         } catch (Exception e) { 30  e.printStackTrace(); 31             return false; 32  } 33  } 34 
35  @Override 36     protected void onPostExecute(Boolean result) { 37         super.onPostExecute(result); 38         if (mDialog.isShowing()) mDialog.dismiss(); 39         if (result) { 40             // jump to the Main page
41             Intent intent = new Intent(mContext, MainActivity.class); 42  mContext.startActivity(intent); 43         } else { 44             Toast.makeText(mContext, "登錄失敗!", Toast.LENGTH_LONG).show(); 45  } 46  } 47 }
View Code

在點擊登錄按鈕監聽器的回調函數中實例化上述異步任務,傳入用戶名和密碼字符串數組,如下:

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

短短的幾行代碼,便實現了登錄的基本功能。

2.2 注冊功能

注冊功能的實現也非常簡單,這里用到了AccountManager類來實現注冊,注意這是一個單例。下述代碼實現了注冊的異步任務調用:

 1 public class RegisterAsyncTask extends AsyncTask<String, Void, Boolean> {  2 
 3     private ProgressDialog mDialog;  4     private Context mContext;  5 
 6     public RegisterAsyncTask(Context context) {  7         mDialog = new ProgressDialog(context);  8         mDialog.setTitle("提示信息");  9         mDialog.setMessage("正在注冊,請稍等..."); 10 
11         mContext = context; 12  } 13 
14  @Override 15     protected void onPreExecute() { 16         super.onPreExecute(); 17         if (!mDialog.isShowing()) { 18  mDialog.show(); 19  } 20  } 21 
22  @Override 23     protected Boolean doInBackground(String... params) { 24 
25         AbstractXMPPConnection connection = XMPPConnectionManager.getInstance(); 26         AccountManager ac = AccountManager.getInstance(connection); 27         try { 28             ac.createAccount(params[0], params[1]); 29             return true; 30         } catch (Exception e) { 31  e.printStackTrace(); 32             return false; 33  } 34  } 35 
36  @Override 37     protected void onPostExecute(Boolean result) { 38         super.onPostExecute(result); 39         if (mDialog.isShowing()) mDialog.dismiss(); 40         if (result) { 41             // jump to Main page
42             Intent intent = new Intent(mContext, MainActivity.class); 43  mContext.startActivity(intent); 44         } else { 45             Toast.makeText(mContext, "注冊失敗!", Toast.LENGTH_LONG).show(); 46  } 47  } 48 }
View Code

同樣,在RegisterActivity中注冊相應監聽器,代碼如下:

 1 @Override  2     public void onClick(View v) {  3         switch (v.getId()) {  4             case R.id.btn_press_register:  5                 String [] params = new String[3];  6                 params[0] = mEditTxtPhoneNumber.getText().toString().trim();  7                 params[1] = mEdtTxtPassword.getText().toString().trim();  8                 params[2] = mEdtTxtNickName.getText().toString().trim();  9 
10                 try { 11                     new RegisterAsyncTask(this).execute(params); 12                 } catch (Exception e) { 13  e.printStackTrace(); 14  } 15                 break; 16  } 17     }
View Code

3 登陸后主界面

下面正式進入本篇博文的主體內容——登錄后主界面的UI顯示與基本交互邏輯。首先來看看登陸后的主界面UI的運行效果,基本和微信是一樣的:

主界面分為三個部分,分別為頂部的ActionBar(也可以用ToolBar)、底部的標簽導航Tab Navigation、以及中間的主體內容部分,如下圖所示:

接下來的三個小節,我們就分別來介紹這三個部分的具體實現。由於內容較多,關於一些很基礎的內容,介紹的可能會比較簡單。

3.1 頂部的ActionBar

現在所有App的頂部都會有一個Action Bar,直譯就是操作條,這是在Android SDK 3.0引入的。在Android SDK 5.0中,為了使用更為靈活,谷歌又提供了更為靈活的Toolbar,直譯為工具條。無論是ActionBar還是ToolBar,其主要是提供選項菜單菜單,供用戶點擊觸發執行相應操作,類似於Windows應用程序中的工具欄。除此之外,Action Bar還支持回退操作、Logo和Title顯示、添加Spinner下拉式導航等功能,詳細內容請參考谷歌官方文檔,這一小節我們只關注本文實現所用到的一些知識點:

1. 如何得到ActionBar實例

為了使用ActionBar,首先要得到其實例。Action Bar的實例不能由我們直接new出來;也不是聲明在布局文件中,所以不能通過findViewById的方式獲得Action Bar的實例。要想在Activity中得到ActionBar的實例,必須讓我們的Activity繼承自AppCompatActivity或ActionActivity類(這應該是ActionBar最不靈活的地方之一),這兩個類中都一提供一個方法:getSupportActionBar,來獲取該Activity中ActionBar的實例。對,就這么簡單,也就是這一句代碼:

mActionBar = getSupportActionBar();

2. 如何為ActionBar設置屬性值

通過上一點,我們可以知道ActionBar實例是由系統為我們生成好的,那么Action Bar中顯示哪些內容、怎么顯示這些內容,都是由系統根據一定規則確定的,那么該如何將我們需要的值設置給ActionBar呢?這里主要有兩種方式:

(I)在Activity的onCreate中設置

這一方式是通過ActionBar的API來設置Action Bar的屬性,例如標題、子標題、Logo、Icon、回退按鈕等,上述主界面中,通過API可以設置ActionBar標題,如下:

mActionBar.setTitle(getResources().getString(R.string.string_wechat));

(II)在配置文件中指定

 通過ActionBar的API,我們可以可以設置一些部分數據,但這些數據如何在ActionBar中展示,則需要在style.xml文件中來定義;另外菜單項的定義也需要通過配置文件(也可以稱為資源文件)來指定。首先,我們先來說說菜單的使用。
對於初學者來說,也許會覺得Android中菜單(Menu)涉及的內容似乎很多,就分類來說就有三種:選項菜單、上下文菜單和彈出式菜單。但其實這些菜單的使用基本是一樣的。包括兩個步驟:

(1)在res/menu目錄下添加菜單聲明文件;

(2)在Activity相應回調方法中將對應聲明文件inflate出來,另外在Activity中也可以重寫相應回調函數中,以實現各菜單項的想贏。

這部分的細節請參考谷歌的Android開發文檔,上面對menu的介紹十分詳細,本小節只闡述ActionBar中用到的選項菜單。

正如剛才所說,所有菜單的使用都分兩步走,下面來看看選項菜單的這兩步是怎么走的:

  • 定義菜單資源文件

先貼上本文所使用的選項菜單聲明文件代碼,然后分析其含義:

 1 <?xml version="1.0" encoding="utf-8"?>
 2 <menu xmlns:android="http://schemas.android.com/apk/res/android"
 3  xmlns:app="http://schemas.android.com/apk/res-auto">
 4 
 5     <item  6         android:id="@+id/menu_main_activity_search"
 7  android:icon="@mipmap/icon_menu_search"
 8  android:title="@string/string_search"
 9  app:showAsAction="always"
10         />
11 
12     <item 13         android:icon="@mipmap/ic_group_chat"
14  android:title="@string/string_group_chat"
15  app:showAsAction="never"
16         />
17 
18     <item 19         android:icon="@mipmap/icon_sub_menu_add"
20  android:title="@string/string_add_friend"
21  app:showAsAction="never"
22         />
23 
24     <item 25         android:icon="@mipmap/ic_scan"
26  android:title="@string/string_scaning"
27  app:showAsAction="never"
28         />
29 
30     <item 31         android:icon="@mipmap/ic_pay"
32  android:title="@string/string_make_pay"
33  app:showAsAction="never"
34         />
35 
36     <item 37         android:icon="@mipmap/ic_helper"
38  android:title="@string/string_help"
39  app:showAsAction="never"
40         />
41 
42 </menu>
View Code

 這個文件就兩類結點——menu節點和item節點,其中menu節點相當於item結點的容器,這沒有什么可以多說的;各菜單項數據在item節點中定義,item節點中前三個屬性——id、icon、title——分別是標識符、圖標和標題,如下圖所示

showAsAction用來指定該菜單項是出現在ActionBar上還是出現在彈出菜單上,屬性值可以設置為以下四種或它們的組合:

a) always:始終出現在ActionBar上;

b) never:永遠不出現在ActionBar上,只出現在彈出的浮動菜單上;

c) ifRoom:如果ActionBar上有空間,則顯示在ActionBar上,否則顯示在彈出菜單上;

d) withText:前三個用於指定顯示位置的,這個則用於指定是否顯示標題的,如果帶上此標簽,則顯示標題,否則不顯示。

  • Activity中inflate上述定義的文件

其實menu的使用和UI布局是一模一樣的:對UI布局來說,第一步也是在資源文件xml中聲明UI布局,第二步則是在Activity的onCreate中將聲明的UI布局inflate出來,並設置View的監聽事件;菜單也一樣,第一步就是如上面所說的定義menu菜單資源,第二步也是在Activity的onCreateOptionsMenu回調函數中inflate資源文件,代碼如下:

@Override
    public boolean onCreateOptionsMenu(Menu menu) {
        setMenuIconVisible(menu, true);
        getMenuInflater().inflate(R.menu.menu_main_activity, menu);
        return super.onCreateOptionsMenu(menu);
    }

上述代碼中,除了第4行inflate菜單資源外,還在第3行的函數調用中設置了菜單圖標的可見性。這是因為在高版本的Android SDK中,默認情況下溢出菜單中的菜單項只顯示菜單標題(title),而不顯示圖標(icon),要想將圖標顯示出來,只能通過反射的方式,具體邏輯如下:

private void setMenuIconVisible(Menu menu, boolean visible) {
        try {
            Class<?> clazz = Class.forName("android.support.v7.view.menu.MenuBuilder");
            Method method = clazz.getDeclaredMethod("setOptionalIconsVisible", boolean.class);

            method.setAccessible(true);
            method.invoke(menu, visible);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

經過了上述兩步,便實現在Action Bar上顯示選項菜單的功能。到此為止,我們以及將所需的數據統統都告訴系統了,系統會根據相應的主題和樣式來顯示ActionBar和溢出菜單項。當然,這些系統的主題或樣式不一定符合我們的需求,所以需要對其進行重新定義。

關於Android的主題和樣式,這也是一個比較寬泛的話題,作用相當於Web前端開發中的CSS。這一小節樓主就根據自己的理解作一個簡單地說明:所謂樣式,就是將UI布局文件View視圖中的部分屬性抽出來,定義在style.xml文件中,在UI布局文件中,通過android:style來引用style.xml中的相關條目;所謂主題,相當於樣式的集合,用於控制整個App或某個Activity的樣式。Android中內置了許許多多樣式和主題,我們初學者最好能對其有一個大致的認識,在這里推薦兩篇比較好的博文:

http://www.cnblogs.com/qianxudetianxia/p/3725466.html

http://www.cnblogs.com/qianxudetianxia/p/3996020.html

這兩篇博文對常用的系統樣式和主題做了歸類和整理,雖然有點老,但還是值得一看的。簡易版微信的主題繼承自Theme.AppCompat.Light.DarkActionBar:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">

下面我們來看看這里重寫的樣式吧:

a) 修改頂部StatusBar的背景色

目前找到兩種方式:

① 修改樣式中的colorPrimaryDark,將其改為你需要的顏色,即:

<item name="colorPrimaryDark">your color</item>

② 修改android:statusBarColor,即:

<item name="android:statusBarColor">your color</item>

b) 修改Action Bar相關的屬性

① 修改ActionBar的背景色

同樣有兩種方式:1)修改樣式中的colorPrimary,設置為你需要的ActionBar背景色;2)單獨設置ActionBar的背景色。為了不改變ActionBar的其他屬性的樣式,可以通過繼承系統的ActionBar樣式,如本文中定義ActionBar的背景色如下:

    <style name="ActionBar" parent="Base.Theme.AppCompat.Light.DarkActionBar">
        <item name="background">@color/colorActionBarBackground</item>
        <item name="android:background">@color/colorActionBarBackground</item>
    </style>

然后將此樣式設置給actionBarStyle,如下:

<item name="actionBarStyle">@style/ActionBar</item>
<item name="android:actionBarStyle">@style/ActionBar</item>

② 修改溢出菜單按鈕的圖標

溢出菜單按鈕本質就是一個ImageButton,改變其圖標可以通過修改相應樣式中的src屬性來實現,同樣要繼承系統的樣式,具體定義樣式如下:

<style name="ActionButton.Overflow" parent="android:Widget.Holo.ActionButton.Overflow">
        <item name="android:src">@mipmap/icon_menu_add</item>
        <item name="android:padding">10dip</item>
        <item name="android:scaleType">fitCenter</item>
    </style>

將此樣式設置給actionOverflowButtonStyle,如下:

<item name="actionOverflowButtonStyle">@style/ActionButton.Overflow</item>

③ 溢出菜單樣式

- 菜單文本顏色修改

修改菜單文本顏色樣式如下:

<style name="TextAppearance.PopupMenu" parent="android:TextAppearance.Holo.Widget.PopupMenu">
        <item name="android:textColor">@android:color/white</item>
</style>

並將上述樣式賦值給android:textAppearanceLargePopupMenu,即:

<item name="android:textAppearanceLargePopupMenu">@style/TextAppearance.PopupMenu</item>

- 菜單彈出位置修改

修改溢出菜單的彈出位置,使其彈出來的時候,位於ActionBar之下的樣式如下:

<style name="PopupMenu.Overflow" parent="Widget.AppCompat.Light.PopupMenu.Overflow">
    <item name="overlapAnchor">false</item>
</style>

並將此樣式賦值給主題中的popupMenuStyle,如下:

<item name="popupMenuStyle">@style/PopupMenu.Toolbar</item>
<item name="android:popupMenuStyle">@style/PopupMenu.Toolbar</item>

這里我們還可以設置彈出菜單的左右偏移(dropdownHorizontalOffset)和上下偏移(dropdownVerticalOffset),但是設置這兩個屬性時,必須先設置overlapAnchor為false。

3.2 可滑動的Tab頁實現

這部分采用的是ViewPager + Fragment的方式實現,即用Fragment填充ViewPager,下面進行詳細介紹:

第一步先在UI布局文件中添加ViewPager:

<android.support.v4.view.ViewPager
        android:id="@+id/mainViewPager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

第二步獲取ViewPager實例,並設置適配器Adapter和設置當前顯示頁面索引:

mMainViewPager = (ViewPager) this.findViewById(R.id.mainViewPager);
mMainViewPager.setAdapter(new MainPagerFragmentAdapter(fragments, getSupportFragmentManager()));
mMainViewPager.setCurrentItem(0);

第三步: Fragment列表

Fragment,直譯過來就是片段,是從Android 3.0 SDK引入的,主要用於平板開發,當然手機客戶端也是可以使用的。Fragment相當於一個子Activity,有它自己的UI布局,也有生命周期,也可以像Activity那樣為View添加事件響應函數。通過Fragment,可以使UI的復用性更好,邏輯代碼分布更合理。

我們的微信主界面的每個Tab頁,都是一個Fragment。每個Fragment展示其對應的UI布局,每個Fragment有其自己的邏輯。和Activity的使用類似,要想給Fragment設置UI,需要繼承Fragment,重寫onCreateView來設置需要顯示的UI,例如“發現”頁面的Fragment子類如下:

 1 public class DiscoveryFragment extends Fragment {
 2 
 3     public static DiscoveryFragment newInstance() {
 4         DiscoveryFragment fragment = new DiscoveryFragment();
 5         return fragment;
 6     }
 7 
 8     @Override
 9     public View onCreateView(LayoutInflater inflater, ViewGroup container,
10                              Bundle savedInstanceState) {
11         // Inflate the layout for this fragment
12         return inflater.inflate(R.layout.fragment_discovery, container, false);
13     }
14 
15 }
View Code

現在沒寫實現邏輯,所以四個Fragment的實現大同小異,其余的Fragment就不做闡述了。

Fragment列表獲取很簡單,就是通過newInstance方法獲得各Fragment實例,注意Fragment的順序,代碼如下:

 1 private List<Fragment> GetFragments() {
 2         List<Fragment> fragments = new ArrayList<>();
 3 
 4         ChattingFragment chattingFragment = ChattingFragment.newInstance();
 5         fragments.add(chattingFragment);
 6 
 7         ContactFragment contactFragment = ContactFragment.newInstance();
 8         fragments.add(contactFragment);
 9 
10         DiscoveryFragment discoveryFragment = DiscoveryFragment.newInstance();
11         fragments.add(discoveryFragment);
12 
13         MyselfFragment myselfFragment = MyselfFragment.newInstance();
14         fragments.add(myselfFragment);
15 
16         return fragments;
17     }
View Code

3.3 底部導航條的實現

1. 自定義View顯示圖標和文本

微信的底部導航條其實還是蠻復雜的,它不是圖片(ImageView)+文字(TextView)的簡單組合,然后均勻分布在一個LinearLayout中。因為當ViewPager滑動時,圖標和文字的透明度不斷改變的,所以需要用自定義View來實現顏色的實時變化。

1) 自定義View的第一步當然是繼承View類:

public class ChangeColorIconWithTextView extends View

2) 在構造函數中獲取用戶提供的樣式

這個對初學者來說有點復雜,分兩小步:

① 控件自定義屬性的聲明

    <attr name="tab_icon" format="reference" />
    <attr name="tab_icon_inactive" format="reference" />
    <attr name="text" format="string" />
    <attr name="text_size" format="dimension" />
    <attr name="icon_color" format="color" />

    <declare-styleable name="ChangeColorIconView">
        <attr name="tab_icon" />
        <attr name="tab_icon_inactive" />
        <attr name="text" />
        <attr name="text_size" />
        <attr name="icon_color" />
    </declare-styleable>

使用此View時,用戶可以為其指定5個屬性,那在View中怎么獲取這五個屬性值呢?

② 獲取屬性值

在構造函數中獲取,具體代碼如下:

 1 // Obtain the styled attribute from context
 2         TypedArray typedArray = context.obtainStyledAttributes(
 3                 attrs, R.styleable.ChangeColorIconView);
 4 
 5         // traverse the obtained return value.
 6         int n = typedArray.getIndexCount();
 7         for (int i = 0; i < n; ++i) {
 8             int attr = typedArray.getIndex(i);
 9             switch (attr) {
10                 case R.styleable.ChangeColorIconView_tab_icon:
11                     BitmapDrawable drawable = (BitmapDrawable) typedArray.getDrawable(attr);
12                     mIconBitmap = drawable.getBitmap();
13                     break;
14                 case R.styleable.ChangeColorIconView_text:
15                     mText = typedArray.getString(attr);
16                     break;
17                 case R.styleable.ChangeColorIconView_text_size:
18                     mTextSize = (int) typedArray.getDimension(attr, 12);
19                     break;
20                 case R.styleable.ChangeColorIconView_icon_color:
21                     mIconColor = typedArray.getColor(attr,
22                             context.getResources().getColor(R.color.colorPrimary));
23                     break;
24                 case R.styleable.ChangeColorIconView_tab_icon_inactive:
25                     BitmapDrawable d = (BitmapDrawable) typedArray.getDrawable(attr);
26                     mIconBitmapInActive = d.getBitmap();
27                     break;
28             }
29         }
30         typedArray.recycle();
View Code

可以看到,通過Context獲得TypedArray實例,然后逐一遍歷,選擇需要的屬性值即可。這部分涉及的東西很多,本人功力還不夠深厚,還需要慢慢深入,Android SDK里就是這么做的。

③ 重寫onMeasure方法

自定義View,一般需要重寫onMeasure和onDraw方法,有時也需要重寫onLayout方法。其中,onMeasure方法用於測量待繪制的視圖;onDraw方法用於往Canvas方法繪制視圖;onLayout則用於布局視圖,一般不需要重寫。

下面來看看ChangeColorIconWithTextView的onMeasure的實現,已知條件如下圖:

自定義View要繪制兩部分內容:圖標Icon和文本,並且一旦圖標繪制區域確定了,文本的繪制區域也就定了,因此onMeasure階段的任務就是確定圖標的繪制區域——一個正方形區域Rect。根據上圖,不難得到下述代碼:

 1     @Override
 2     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 3 
 4         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 5 
 6         // determine the size of icon - a rect
 7         int bitmapWidth = Math.min(
 8                 getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
 9                 getMeasuredHeight() - getPaddingTop() - getPaddingBottom() - mTextBound.height());
10 
11         int left = getMeasuredWidth() / 2 - bitmapWidth / 2;
12         int top = (getMeasuredHeight() - mTextBound.height()) / 2 - bitmapWidth / 2;
13 
14         mIconRect = new Rect(left, top, left + bitmapWidth, top + bitmapWidth);
15     }
View Code

這段代碼首先求出圖片所在區域的邊長,接着根據邊長,可以很容易求出繪制區域的left坐標,同時right坐標也就確定了;注意top或bottom坐標在求解時需要減去文本部分的高度。可以看到整個onMeasure函數還是比較簡單的。

④ 重寫onDraw方法

這一步就是將圖標以及文本繪制到Canvas的指定區域上,需要注意的是這里要繪制兩層圖像——底層圖像和上層圖像——並且,這兩層圖像之間按照一定的比例融合,融合系數(透明度Alpha)根據ViewPager中,頁面所在位置而定,這一系數可以由外部提供。下面來看看繪制部分的代碼:

 1 @Override
 2     protected void onDraw(Canvas canvas) {
 3 
 4         // clear the old icon.
 5         canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.XOR);
 6 
 7         // draw an icon on the canvas
 8         int foregroundAlpha = (int) (mIconAlpha * 255);
 9         int backgroundAlpha = 255 - foregroundAlpha;
10 
11         drawBaseLayer(canvas, backgroundAlpha);
12         drawUpperLayer(canvas, foregroundAlpha);
13     }
View Code

第一步:清空Canvas,為繪制做准備;

第二步:根據外部傳入的透明度系數,求出上下層的Alpha系數;

第三步:繪制底層圖像和上層圖像。

其中,繪制底層圖像代碼如下:

 1 private void drawBaseLayer(Canvas canvas, int alpha) {
 2         // draw icon
 3         mPaint.setAlpha(alpha);
 4         canvas.drawBitmap(mIconBitmapInActive, null, mIconRect, mPaint);
 5 
 6         // draw text
 7         mPaint.setColor(getResources().getColor(android.R.color.darker_gray));
 8         mPaint.setAlpha(alpha);
 9         canvas.drawText(mText, mIconRect.centerX() - mTextBound.width() / 2,
10                 mIconRect.bottom + mTextBound.height(), mPaint);
11     }
View Code

前兩行代碼是根據onMeasure階段得到的Rect區域往Canvas上繪制Icon位圖;后三句代碼是根據指定顏色繪制文本。繪制上層圖像的方法是類似的,只不過顏色和位圖資源不同。至此,可以改變透明度的Icon就做好了。當然,我們的ChangeColorIconWithTextView需要提供一個Set透明度的方法,如下:

1     public void setIconAlpha(double iconAlpha) {
2         mIconAlpha = iconAlpha;
3         invalidate();
4     }
View Code

設置了透明度后,調用invalidate函數,強制重繪。

2. 底部導航的實現

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

 1     <LinearLayout
 2         android:layout_width="match_parent"
 3         android:layout_height="50dp">
 4 
 5         <com.doll.mychat.widget.ChangeColorIconWithTextView
 6             android:id="@+id/nav_tab_record"
 7             android:layout_width="0dp"
 8             android:layout_weight="1"
 9             android:layout_height="match_parent"
10             android:padding="5dp"
11             app:tab_icon="@mipmap/icon_chat_main_nav_active"
12             app:tab_icon_inactive="@mipmap/icon_chat_main_nav_tab_inactive"
13             app:icon_color="@color/colorPrimary"
14             app:text="@string/string_nav_tab_wechat"
15             app:text_size="12sp"
16             />
17 
18         <com.doll.mychat.widget.ChangeColorIconWithTextView
19             android:id="@+id/nav_tab_contact"
20             android:layout_width="0dp"
21             android:layout_weight="1"
22             android:layout_height="match_parent"
23             android:padding="5dp"
24             app:tab_icon="@mipmap/icon_contact_main_nav_active"
25             app:tab_icon_inactive="@mipmap/icon_contact_main_nav_inactive"
26             app:icon_color="@color/colorPrimary"
27             app:text="@string/string_nav_tab_contact"
28             app:text_size="12sp"
29             />
30 
31         <com.doll.mychat.widget.ChangeColorIconWithTextView
32             android:id="@+id/nav_tab_discovery"
33             android:layout_width="0dp"
34             android:layout_weight="1"
35             android:layout_height="match_parent"
36             android:padding="5dp"
37             app:tab_icon="@mipmap/icon_discovery_main_nav_active"
38             app:tab_icon_inactive="@mipmap/icon_discovery_main_nav_inactive"
39             app:icon_color="@color/colorPrimary"
40             app:text="@string/string_nav_bar_discovery"
41             app:text_size="12sp"
42             />
43 
44         <com.doll.mychat.widget.ChangeColorIconWithTextView
45             android:id="@+id/nav_tab_myself"
46             android:layout_width="0dp"
47             android:layout_height="match_parent"
48             android:layout_weight="1"
49             android:padding="5dp"
50             app:tab_icon="@mipmap/icon_myself_main_nav_active"
51             app:tab_icon_inactive="@mipmap/icon_myself_main_nav_inactive"
52             app:icon_color="@color/colorPrimary"
53             app:text="@string/string_nav_tab_myself"
54             app:text_size="12sp"
55             />
56 
57     </LinearLayout>
View Code

第二步:獲取ChangeColorIconWithTextView的實例,存放在一個容器中,以便ViewPager滑動時設置透明度,並為其添加點擊事件回調函數:

 1     private void initTabIndicator() {
 2         ChangeColorIconWithTextView one = (ChangeColorIconWithTextView) findViewById(
 3                 R.id.nav_tab_record);
 4         ChangeColorIconWithTextView two = (ChangeColorIconWithTextView) findViewById(
 5                 R.id.nav_tab_contact);
 6         ChangeColorIconWithTextView three = (ChangeColorIconWithTextView) findViewById(
 7                 R.id.nav_tab_discovery);
 8         ChangeColorIconWithTextView four = (ChangeColorIconWithTextView) findViewById(
 9                 R.id.nav_tab_myself);
10 
11         mTabList.add(one);
12         mTabList.add(two);
13         mTabList.add(three);
14         mTabList.add(four);
15 
16         one.setOnClickListener(this);
17         two.setOnClickListener(this);
18         three.setOnClickListener(this);
19         four.setOnClickListener(this);
20 
21         one.setIconAlpha(1.0f);
22     }
View Code

點擊事件回調函數如下:

 1     @Override
 2     public void onClick(View v) {
 3 
 4         deselectAllTabs();
 5 
 6         switch (v.getId()) {
 7             case R.id.nav_tab_record:
 8                 selectTab(0);
 9                 break;
10             case R.id.nav_tab_contact:
11                 selectTab(1);
12                 break;
13             case R.id.nav_tab_discovery:
14                 selectTab(2);
15                 break;
16             case R.id.nav_tab_myself:
17                 selectTab(3);
18                 break;
19         }
20     }
21 
22     private void selectTab(int tabIndex) {
23         mTabList.get(tabIndex).setIconAlpha(1.0);
24         mMainViewPager.setCurrentItem(tabIndex);
25     }
26 
27     private void deselectAllTabs() {
28         for (ChangeColorIconWithTextView v : mTabList) {
29             v.setIconAlpha(0.0);
30         }
31     }
View Code

第三步:添加ViewPager滑動時的回調函數:

 1         mMainViewPager.clearOnPageChangeListeners();
 2         mMainViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
 3             @Override
 4             public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
 5                 if (positionOffset > 0) {
 6                     mTabList.get(position).setIconAlpha(1 - positionOffset);
 7                     mTabList.get(position + 1).setIconAlpha(positionOffset);
 8                 }
 9             }
10 
11             @Override
12             public void onPageSelected(int position) {}
13 
14             @Override
15             public void onPageScrollStateChanged(int state) {}
16         });
View Code

這樣,一旦ViewPager滑動,便會觸發ChangeColorIconWithTextView更新透明度,並重繪圖像,從而實現滑動ViewPager時透明度實時改變的效果。

4 總結

這一次學習筆記中,記錄的內容有點雜,畢竟是樓主苦練20多天之后的一些學習成果(當然平時要上班的哈,其實也就周末學學)。我們首先簡單介紹了XMPP及其開源實現Openfire + Smack,並使用Smack三方庫來改寫了客戶端登陸、注冊功能的邏輯;接着實現了簡易版微信的主界面,逐一介紹了ActionBar、ViewPager + Fragment和底部導航。介紹ActionBar時,引入了在系統Style的基礎上自定義Style,實現系統組件的定制;實現底部導航時,介紹了自定義控件的基本實現步驟。

雖然這些東西看着不難,但是作為初學者,從頭到尾一步步走下來還是需要一些精力的,尤其是Android的碎片化問題,有些問題更是讓初學者一時摸不着頭腦。不過沒事,一點點學SDK文檔、源代碼和互聯網資料,一點點敲代碼,總有一天能夠學會很多的,下次學習筆記講介紹好友的添加及好友列表的顯示!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM