最近很久沒有更新博文,一則是由於公司最近比較忙,另外本身在Android學習過程和簡易版微信的開發過程當中碰到了一些絆腳石,因此最近一直在學習充電中。下面來列舉一下本身所走過的彎路:html
(1)原本打算前端(即客戶端)和後端(即服務端)都由本身實現,後來發現服務端已經有成熟的程序可使用,如基於XMPP協議的OpenFire服務器程序;客戶端也已經有成熟的框架供咱們使用,如Smack,一樣基於XMPP協議。這一系列筆記式文章主要是記錄本身學習Android開發的過程,爲突出重點(Android的學習),故使用開源框架OpenStack + Smack組合。並且開源框架確定比你本身一我的寫出來的要好得多。前端
(2)對於Android初學者來講,自定義控件是一道坎,須要花大量時間去學習和嘗試。以前樓主也一直沒有接觸過自定義控件,因此在這段時間也作了初步的學習和嘗試。java
下面咱們首先對XMPP作一個簡單的介紹,並利用Smake框架改寫客戶端的登錄和註冊功能;接着實現主界面UI界面和初步交互。android
多臺計算機經過傳輸媒介(如:光纖、雙絞線、同軸電纜等)鏈接和傳輸信息,這是計算機網絡的硬件層;多臺計算機之間須要傳送信息,從一臺計算機到另外一臺計算機或從一臺計算機到多臺計算機,這就要定一個規則,這個規則就是協議,這是計算機網絡的軟件層。對軟件開發者來講,咱們幾乎無需研究鏈接介質,但須要了解協議,其中最重要的計算機互聯協議即是因特網的基礎——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都已經封裝的很好了,只須要了解一些最基本概念就足夠了。數組
客戶端的實現主要是基於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方法便可。
有了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 });
短短的幾行代碼,便實現了登陸的基本功能。
註冊功能的實現也很是簡單,這裏用到了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 }
下面正式進入本篇博文的主體內容——登陸後主界面的UI顯示與基本交互邏輯。首先來看看登錄後的主界面UI的運行效果,基本和微信是同樣的:
主界面分爲三個部分,分別爲頂部的ActionBar(也能夠用ToolBar)、底部的標籤導航Tab Navigation、以及中間的主體內容部分,以下圖所示:
接下來的三個小節,咱們就分別來介紹這三個部分的具體實現。因爲內容較多,關於一些很基礎的內容,介紹的可能會比較簡單。
如今全部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:前三個用於指定顯示位置的,這個則用於指定是否顯示標題的,若是帶上此標籤,則顯示標題,不然不顯示。
其實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。
這部分採用的是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 }
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時透明度實時改變的效果。
這一次學習筆記中,記錄的內容有點雜,畢竟是樓主苦練20多天以後的一些學習成果(固然平時要上班的哈,其實也就週末學學)。咱們首先簡單介紹了XMPP及其開源實現Openfire + Smack,並使用Smack三方庫來改寫了客戶端登錄、註冊功能的邏輯;接着實現了簡易版微信的主界面,逐一介紹了ActionBar、ViewPager + Fragment和底部導航。介紹ActionBar時,引入了在系統Style的基礎上自定義Style,實現系統組件的定製;實現底部導航時,介紹了自定義控件的基本實現步驟。
雖然這些東西看着不難,可是做爲初學者,從頭至尾一步步走下來仍是須要一些精力的,尤爲是Android的碎片化問題,有些問題更是讓初學者一時摸不着頭腦。不過沒事,一點點學SDK文檔、源代碼和互聯網資料,一點點敲代碼,總有一天可以學會不少的,下次學習筆記講介紹好友的添加及好友列表的顯示!