Android學習項目之簡易版微信為例(二)
1 概述
從這篇開始,正式進入簡易版微信的開發(fā)。深入學習前,想談談個人對Android程序開發(fā)一些理解,不一定正確,只是自己的一點想法。Android程序開發(fā)不像我們在大學時候寫C控制臺程序那樣,需要從main開始寫代碼邏輯,大部分邏輯控制代碼都由自己來實現。事實上,Android已經為我們提供了一個程序運行的框架,我們只需要往框架中填入我們所需的內容即可,這里的內容主要是:四大組件——Activity、Service、ContentProvider、BroadCast。在這四大組件中,可以實現前端界面顯示和后端數據處理相關的代碼控制邏輯。關于前端界面顯示主要涉及到:組件的生命周期回調管理、注冊視圖(View)的事件監(jiān)聽器、集合類型視圖的數據適配器(Adapter)、不同窗口界面的跳轉等等。關于后臺數據的交互處理,主要涉及到:異步任務(AsyncTask)、Handler/Message、網絡編程(HTTP或Socket)、數據庫操作(SQLiteOpenHelper或ContentProvider)等等。所以,對我們初學者來說,學習Android主要就是學習Android框架中各個類的作用和使用方法。
好,下面開始本文內容。當第一次使用微信(或其他常用的Android應用)的時候,首先就是注冊、登錄,本文就來實現這兩個基本功能。由于剛接觸Android開發(fā),所以需要了解很多基礎知識點。我們將通過這兩個功能的實現,學習以下幾個Android開發(fā)的知識點:
Layout布局:制作用戶界面,Android中使用XML文件描述UI布局,類似HTML+CSS方式的界面組件方式。對后端的童鞋來說,按UI設計稿進行布局或按需求來定制一個控件或許是學習前端最大的障礙之一。關于UI布局,本文起一個頭,隨著我們的簡易版微信應用深入開發(fā),我們就會慢慢熟悉Android的UI布局了;關于自己動手開發(fā)一個視圖(View),這應該也是Android開發(fā)中的難點,我們將在后續(xù)文章中慢慢深入學習。
Activity概念及其生命周期:布局完成后,要將布局得到的UI界面顯示出來,這就需要引入Activity組件——負責UI界面的顯示和用戶的交互。Activity應該是Android應用最重要的組件了 —— 一個應用可以沒有四大組件中的其他三大組件(即:內容提供者ContentProvider、服務Service、廣播BroadCast),但不能沒有Activity —— 這個組件類似Windows編程中的窗口,在Windows中如果沒有窗口怎么與用戶交互?
登錄、注冊功能的實現:講完Activity后,就需要通過Activity來加入我們需要的邏輯。Android應用程序一般都是C(客戶端)/S(服務端)結構的,注冊、登陸功能的實現包括客戶端邏輯的編寫和服務器端邏輯的編寫,我們將在第4小節(jié)介紹這兩個功能的客戶端和服務端的邏輯實現。
最后總結本篇博文內容,并預告下篇博文內容,那就讓我們開啟Android學習的第二課吧!
2 Android的MVC結構
當學習一門新技術時,我們很少會思考這門技術重點學習什么,應該怎么去學習之類的問題。大多數童鞋常常會一開始就一頭扎到知識點的海洋中,最后自己也搞不清學會了什么。比如學習Java,一上來就從變量命名開始學、接著學習表達式、控制流、面向對象,如果初學者也許這是合適的,但如果你已經學會C或C++,有些知識點似乎就不需要學習了。比如我之前包括現在主要用的是C++,那一上來就會學習I/O流、集合類這些常用的知識點,就可以開發(fā)一些小程序了。有時間的話再去看看多線程、垃圾收集以及源代碼。
學習Android也一樣,首先應該弄清楚應該學一些什么,這就要從高一些的層次來看Android。從架構上來說,和很多UI框架一樣,Android用的是主流的MVC結構,這應該是比較成熟的前端框架了。MVC框架結構如下圖:
MVC結構分為三部分:
控制器(Controller)部分:接收用戶輸入,通過事件分發(fā)機制確定接收者。這部分在Android中已有框架完成,我們只需在Activity中向View視圖實例對象注冊特定監(jiān)聽器即可,監(jiān)聽器實現的具體邏輯由我們來寫;而且監(jiān)聽器只需要知道有這么回事就行,用到去API查就可以。
模型(Model)部分:這部分主要實現業(yè)務邏輯的處理和數據的更新。這部分應該是Android編程的重點,四大組件中的Service(服務)、ContentProvide(內容提供者)都是Model(模型)有關的,另外數據存儲,如數據庫、文件等也屬于Model范疇,這部分應該是Android學習的重點。
視圖(View)部分:這部分就是用于顯示模型數據。這部分在Android中就是使用View視圖進行UI布局,有時框架提供的View部件不滿足需求時,得根據需求重寫View,實現我們需要的效果。
這樣劃分之后,我們就大體上知道了一個Android軟件由哪些部分組成以及它們之間如何是交互的,Android框架已經為我們實現了哪些功能 ,哪些功能需要我們擴展的,這樣我們學習起來才會有的放矢。
3 Layout布局及分析
關于做軟件UI,博主曾經有一段比較痛苦的回憶。記得那是在大三上學期學習完《數據庫系統(tǒng)概論》這門課程之后,老師要求用ASP.NET做一個網站。當時博主做的是一個在線購書系統(tǒng),不懂怎么制作網頁界面,于是就在Visual Studio中以拖拽控件的方式來布局,最后雖然把系統(tǒng)倒騰出來了(過程可以說是十分痛苦),但界面看了實在無法讓人產生購買的欲望。經歷過這么一出之后,博主對前端界面產生了恐懼感和厭惡感。不過,進入公司參加工作以來,慢慢接觸到了軟件UI的設計與實現過程,同時自己也動手實現了一些界面布局后,才讓這種恐懼感和厭惡感慢慢減少了。在這里,博主想來一句經驗之談:要想做一個漂亮的UI布局,不是通過拖拽控件能拖出來的。當然,對初學者來說,可以通過通過拖拽控件的方式來學習Android框架。
Android制作UI界面有兩種方式:
(1)通過XML配置文件的方式,博主一般稱它為“聲明式布局”(不知對不對):這種方式就是把UI要顯示的控件及這些控件的顯示方式聲明在XML文件中,然后通過Activity的SetContentView接口將布局的描述文件設置給Activity;
(2)通過Java類來添加布局控件,并設置顯示相關的屬性,博主一般稱這一布局方式為“命令式布局”。
第(1)種布局方式,即聲明式布局,一般用于變化不大UI的布局;第(2)種布局方式,即命令式布局,一般用于程序運行時不斷變化的UI界面的布局。本篇博文將實現的登陸、注冊功能采用的是聲明式布局,所以本小節(jié)僅介紹聲明式布局,命令式布局將在后續(xù)博文中用到時再做詳細闡述。
好了,理論的東西就不扯太多了,搞軟件開發(fā)的最怕聽到一大堆理論了,下面讓我們來看看登陸和注冊的布局界面的實現效果吧(可能還不是很完美,以后邊學習邊完善吧?。?。首先是登陸頁面(這也是打開軟件后的第一個頁面):
注冊頁面:
注冊、登錄之間交互與登錄成功后的界面,這里登錄成功后的界面上什么都沒有,所以在此就沒單獨貼出來了。圖片有點糊,湊合看看哈~
下面以登錄界面的代碼,來看看Android中如何實現界面布局的,整個UI布局代碼如下(代碼路徑:$res/layout/activity_login.xml):
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <!--Top Panel--> <TextView android:layout_width="match_parent" android:layout_height="50dp" android:background="@color/colorTopPanelBackground" android:gravity="center" android:text="@string/string_login" android:textSize="@dimen/font_size_large" android:textColor="@color/colorSpecialWhite" /> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="@dimen/activity_horizontal_margin" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="50dip" android:orientation="horizontal" android:layout_marginTop="50dp"> <TextView android:layout_width="50dip" android:layout_height="50dip" android:gravity="center_vertical|right" android:text="+86" android:textColor="@color/colorSpecialBlack" android:textSize="@dimen/font_size_medium" /> <EditText android:id="@+id/edt_login_cellphone_number" android:layout_width="0dp" android:layout_height="50dip" android:layout_weight="1" android:layout_marginLeft="25dp" android:background="@null" android:hint="你的手機號碼" android:textSize="@dimen/font_size_medium" android:textColorHint="@color/colorHintText"/> </LinearLayout> <View android:id="@+id/dvd_login_username" android:layout_width="match_parent" android:layout_height="2px" android:background="@color/colorDefault" /> <LinearLayout android:layout_width="match_parent" android:layout_height="50dip" android:orientation="horizontal"> <TextView android:layout_width="50dip" android:layout_height="50dip" android:gravity="center_vertical|right" android:text="@string/string_pass_word" android:textColor="@color/colorSpecialBlack" android:textSize="@dimen/font_size_medium" /> <EditText android:id="@+id/edt_login_password" android:layout_width="0dp" android:layout_height="50dip" android:layout_weight="1" android:layout_marginLeft="25dp" android:background="@null" android:inputType="textPassword" android:textSize="@dimen/font_size_medium" android:hint="填入密碼" android:textColorHint="@color/colorHintText"/> </LinearLayout> <View android:id="@+id/dvd_login_password" android:layout_width="match_parent" android:layout_height="2px" android:background="@color/colorDefault" /> <Button android:id="@+id/btn_login" android:layout_width="match_parent" android:layout_height="@dimen/button_general_height" android:layout_marginTop="50dip" android:background="@drawable/btn_common_selector" android:text="@string/string_login" android:textSize="@dimen/font_size_medium" android:textColor="@color/colorSpecialWhite"/> <Button android:id="@+id/btn_register" android:layout_width="match_parent" android:layout_height="@dimen/button_general_height" android:layout_marginTop="20dip" android:background="@drawable/btn_implicit_selector" android:text="@string/string_register" android:textSize="@dimen/font_size_medium" /> </LinearLayout> </LinearLayout>
上述代碼一層套一層,最終形成一個樹狀結構,如下圖所示:
圖中每個矩形就是一個控件(或稱為視圖),每個控件都有一套與它相關的外觀屬性(類似Web編程中的CSS),控制著該控件的顯示效果。下面對逐個控件及其外觀屬性進行深入分析,從根節(jié)點開始:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">
根節(jié)點是一個布局控件,在這里用的是線性布局(LinearLayout),其它的布局控件還有相對布局(RelativeLayout)和幀布局(FrameLayout)。布局控件的作用就是堆砌(專業(yè)點的說法叫布置Arrange)控件:線性布局,顧名思義,布局方式只能按一個方向(水平horizontal或垂直vertical)堆砌控件,如上述代碼塊中,android:orientation屬性用于說明LinearLayout是水平橫向的線性布局;相對布局,這一布局方式比線性布局要復雜一下,控件之間位置關系不像線性布局那樣只能沿著一個方向,這種布局下,控件的位置根據已有的其他控件來確定的(該布局的具體實例將在后續(xù)博文中闡述);幀布局。另外,上述代碼塊中還有兩個屬性:android:layout_width和android:layout_height,用來描述該控件的寬與高,這也是每個控件都要填的屬性。這兩個屬性的值指定的是一個長度值,可以用像素(px)、點(pt)、設備獨立像素(dp或dip),這里用的是一個特殊值:match_parent——匹配父窗口,即長或寬和父窗口一樣;另外一個特殊值是:wrap_content——內容包裹,即長或寬和空間中內容匹配,內容所占區(qū)域有多大,控件的長或寬就是多大。
<!--Top Panel--> <TextView android:layout_width="match_parent" android:layout_height="50dp" android:background="@color/colorTopPanelBackground" android:gravity="center" android:text="@string/string_login" android:textSize="@dimen/font_size_large" android:textColor="@color/colorSpecialWhite" />
這是第一個可視控件,為文本視圖(TextView),可以看到有很多屬性控制它的外觀顯示,如之前講過的寬度和高度,android:background描述該控件的背景色(很多Android控件也有這一屬性),這里采用的是引用資源的方式,采用這種方式可以提高代碼的可維護性,顏色資源具體定義在$res/values/colors.xml文件中(可以把它理解成程序設計中的常量),除了上述背景色資源,我們還定義了其他一些顏色資源,在下面的代碼中會用到:
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="colorPrimary">#11D31D</color> <color name="colorPrimaryDark">#308E0E</color> <color name="colorAccent">#FF4081</color> <!--color in default status--> <color name="colorDefault">#999999</color> <!--color in active status--> <color name="colorActive">@color/colorPrimary</color> <!--background color of top panel--> <color name="colorTopPanelBackground">#525252</color> <!--color of hint text--> <color name="colorHintText">#DDDDDD</color> <!--some special color--> <color name="colorSpecialBlack">#000000</color> <color name="colorSpecialWhite">#FFFFFF</color> </resources>
之后一個屬性android:grivity用于描述控件中內容的對齊方式,此處就是TextView中文本的對齊方式(為居中對齊)。再接下來一個屬性是android:text,用來指定TextView中的文本內容,這里同樣是引用另一個資源文件中的字符串資源,文件位于$res\values\string。xml中,這個文件專門用來定義字符串常量,除了上述字符串外,還定義了一些其他字符串資源:
<resources> <string name="app_name">MyChat</string> <!-- TODO: Remove or change this placeholder text --> <string name="hello_blank_fragment">Hello blank fragment</string> <!--constant string used in resource--> <string name="string_nick_name">昵稱</string> <string name="string_pass_word">密碼</string> <string name="string_login">登錄</string> <string name="string_register">注冊</string> <string name="string_dialog_title">提示</string> <string name="string_dialog_tips_prefix">正在</string> <string name="string_dialog_tips_suffix">,請稍等...</string> </resources>
接下來兩個屬性分別定義了文本的大小和顏色,同樣使用索引資源的方式,其中文本顏色使用的是前面已經講過的顏色資源,文本大小的資源定義在$res\valuse\dimens.xml文件中,這一文件就是用來定義和尺寸有關的資源(如長度、大小等),在這個文件中還定義了其它一些尺寸資源,如下:
<resources> <!-- Default screen margins, per the Android Design guidelines. --> <dimen name="activity_horizontal_margin">16dp</dimen> <dimen name="activity_vertical_margin">16dp</dimen> <dimen name="horizontal_line_margin">20dp</dimen> <dimen name="contact_image_width">50dp</dimen> <dimen name="contact_image_height">50dp</dimen> <dimen name="context_image_top_buttom_margin">15dp</dimen> <dimen name="activity_registration_vertical_margin">16dp</dimen> <!--following tags define font size--> <dimen name="font_size_medium">16sp</dimen> <dimen name="font_size_small">14sp</dimen> <dimen name="font_size_large">18sp</dimen> <dimen name="font_size_xsmall">12sp</dimen> <dimen name="font_size_xlarge">20sp</dimen> <!--following tags defi--> <dimen name="button_general_height">40dp</dimen> </resources>
到這里,我們就把第一個控件——頂部標題的TextView控件——分析完了??梢钥吹?,為了提高代碼的可維護性和復用性,我們將大多數屬性值都定義在相應的資源文件中。下面的控件分析起來應該就簡單多了,接下來又是一個布局控件,里面存放的是一個登陸表單:
<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="@dimen/activity_horizontal_margin" android:orientation="vertical">
這里沒什么可以講的,這里有一個地方需要注意,新增了一個android:layout_margin屬性,用于描述控件的上、下、左、右外邊距,如下:
上、下、左、右外邊距也可以獨立控制,對于的屬性分別為:android:margin_Top、android:margin_Buttom、android:margin_Left和android:margin_Right。接下來就是表單的內容區(qū)域了,首先要顯示兩個輸入框及其說明文字,輸入框使用的是EditText控件,說明文本使用的是TextView,它們是水平排列的,所以需要用線性布局把它們套起來,代碼如下:
<LinearLayout android:layout_width="match_parent" android:layout_height="50dip" android:orientation="horizontal" android:layout_marginTop="50dp"> <TextView android:layout_width="50dip" android:layout_height="50dip" android:gravity="center_vertical|right" android:text="+86" android:textColor="@color/colorSpecialBlack" android:textSize="@dimen/font_size_medium" /> <EditText android:id="@+id/edt_login_cellphone_number" android:layout_width="0dp" android:layout_height="50dip" android:layout_weight="1" android:layout_marginLeft="25dp" android:background="@null" android:hint="你的手機號碼" android:textSize="@dimen/font_size_medium" android:textColorHint="@color/colorHintText"/> </LinearLayout> <View android:id="@+id/dvd_login_username" android:layout_width="match_parent" android:layout_height="2px" android:background="@color/colorDefault" /> <LinearLayout android:layout_width="match_parent" android:layout_height="50dip" android:orientation="horizontal"> <TextView android:layout_width="50dip" android:layout_height="50dip" android:gravity="center_vertical|right" android:text="@string/string_pass_word" android:textColor="@color/colorSpecialBlack" android:textSize="@dimen/font_size_medium" /> <EditText android:id="@+id/edt_login_password" android:layout_width="0dp" android:layout_height="50dip" android:layout_weight="1" android:layout_marginLeft="25dp" android:background="@null" android:inputType="textPassword" android:textSize="@dimen/font_size_medium" android:hint="填入密碼" android:textColorHint="@color/colorHintText"/> </LinearLayout> <View android:id="@+id/dvd_login_password" android:layout_width="match_parent" android:layout_height="2px" android:background="@color/colorDefault" />
上述代碼中的大部分屬性在前面都已經介紹過了,新增的屬性有只有三個,下面分別介紹。android:inputType用于描述輸入框的輸入類型,如這里用到的是密碼類型:textPassword,這樣就可以將輸入的字母變成一個個小點點,如下:
對于EditText編輯框控件,還有其他輸入類型(input type),如下:
(1)text
(2)textEmailAddress
(3)textUri
(4)number
(5)phone
設置不同的輸入類型,運行時效果就是輸入文本時,彈出的軟鍵盤不同,如inputType設置為textEmailAddress時,則鍵盤上多了一個@符號,
當inputType設置為number或phone時,則軟鍵盤為:
剩下的兩個屬性都和輸入框都和提示文字有關,分別為android:hint和android:textColorHint,分別用于描述提示文字的文本內容和文本顏色,如下:
上圖中輸入框下面有一條綠色的橫線,這里采用的做法是設置一個高度為2px(一般來說,1px就可以,不過個人感覺在這里不夠明顯,所以就設為2px了)、寬度為match_parent的View,如下:
<View android:id="@+id/dvd_login_password" android:layout_width="match_parent" android:layout_height="2px" android:background="@color/colorDefault" />
再接下來就是兩個Button按鈕,分別用于觸發(fā)登錄、注冊操作:
代碼如下:
<Button android:id="@+id/btn_login" android:layout_width="match_parent" android:layout_height="@dimen/button_general_height" android:layout_marginTop="50dip" android:background="@drawable/btn_common_selector" android:text="@string/string_login" android:textSize="@dimen/font_size_medium" android:textColor="@color/colorSpecialWhite"/> <Button android:id="@+id/btn_register" android:layout_width="match_parent" android:layout_height="@dimen/button_general_height" android:layout_marginTop="20dip" android:background="@drawable/btn_implicit_selector" android:text="@string/string_register" android:textSize="@dimen/font_size_medium" />
這些代碼在之前都有所涉及,在這里也不再贅述了。這里有一點特殊:設置背景用了一個稱為選擇器(selector)的資源——因為按鈕按下和彈起的時候,其背景是不一樣的,如下為按下狀態(tài):
和前面的彈起時的狀態(tài)比較一下,背景色變深了,這就用選擇器資源了。該資源為$res\color目錄下(注:創(chuàng)建Android工程時,默認是沒有這個文件夾的,需要手動創(chuàng)建),內容如下:
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true" android:color="#308E0E"/> <!-- pressed --> <item android:color="#11D31D"/> <!-- default --> </selector>
分別指定了按下和默認的顏色。到此為止,我們將登陸布局頁面講完了,注冊頁面和登陸頁面沒什么區(qū)別,在這里就不在闡述了。最后,這里還想分享一些個人經驗:
(1)布局其實就是兩步:第一步:確定用什么控件;第二步:為控件配上屬性;
(2)這么多屬性,書寫時最好要有一個次序,我的做法是:第一個是id屬性;其次是必要的寬、高屬性,接下來是布局相關的屬性,如內外邊距等,再接著是背景色、內容的對齊方式等,最后是控件內容,如文字內容、顏色、大小等,這是一個由外到內、從通用屬性到特殊屬性的一個書寫次序。
(3)盡量將一些屬性值定義在資源文件中,便于代碼的后期維護和復用。
3 Activity概念及其生命周期
第2小結詳細講述了登錄界面,介紹了涉及到的View組件及相關外觀樣式屬性。定義在xml中的UI布局文件只是一個靜態(tài)文件,需要加載到應用程序中,才能被渲染并顯示出來,這就要用到本節(jié)所講的Activity(活動)。本節(jié)主要總結Activity的一些理論知識,包括Activity的概念和生命周期。
3.1 什么是Activity
很多關于Android編程的書籍對Activity的概念都或多或少有一些闡述,但個人感覺都不是很系統(tǒng)。本文在這里拋出一塊磚,總結一下自己對Android中Activity組件的理解(當然有很多不完善的地方,以后我會慢慢補充)如下:
(I)從MVC模式的角度看,Activity相當于Controller——一邊是接收用戶請求,另一邊將請求分發(fā)(dispatch)到各處理單元中;也就是在一個Android中,Activity起到一個核心的作用:
圖中標出了通過Activity啟動各大組件的APIs,這些APIs都定義在Activity中。當然,上述圖示僅是一個簡單的模型,在接收用戶請求時可能還會用到其他組件,在圖中并沒有一一給出,這些組件會在后續(xù)博文中深入學習。
(II)從設計模式模式的角度看,Activity可以看做是一個門面模式(Facade Pattern)。在Activity中聚合了很多組件,見下圖:
對內封裝復雜組件,對外提供簡單的接口,同時也能獨立獲取這些組件實例,這就是門面模式的典型應用吧!這里需要注意的是,Activity繼承自ContextThemeWrapper,在ContextThemeWrapper中聚合了Resource,通過它訪問程序資源;而ContextThemeWrapper繼承自ContextWrapper,通過它可以得到內容解析器(ContentResolver)等組件??偟膩碚f,Activity提供了很多功能,封裝了很多組件,使用起來也非常靈活。
(III)從一個Android開發(fā)者角度看,Activity是一個狀態(tài)機。Activity定義了管理一個活動的生命周期的一系列事件,通過這些事件可以保存應用程序的狀態(tài),這些事件將在3.2中闡述。
3.2 Activity的生命周期
Activity的生命周期是每一本講Android編程的書必講的內容,也是Android程序設計的重點。Android的四大組件都有生命周期的概念,但Activity的生命周期最復雜,下圖是來自Android SDK文檔的一張Activity生命周期事件回調函數的調用次序:
這個圖有點像操作系統(tǒng)課中進程狀態(tài)轉換圖——各種狀態(tài)切來切去。初看這張圖的時候,應該會感覺有點亂,其實理清楚了的話,圖中顯示了也就是四條狀態(tài)變換路徑:
(I)中間垂直方向走下來:Activity launched → onCreate→ onStart→ onResume→ running→ onPause→ onStop→ onDestroy→ Activity shutdown
注:這種情況下,用戶打開應用程序首頁,做了事情后就退出應用了。
(2)內圈:Activity launched → onCreate→ onStart→ onResume→ running→ onPause→ User navigates to the activity→ onResume→ ……
(3)中圈:Activity launched → onCreate→ onStart→ onResume→ running→ onPause→ onStop→ User navigates to the activity→ onRestart→ onResume→ ……
(4)外圈:Activity launched → onCreate→ onStart→ onResume→ running→ onPause [→ onStop→]App Process Killed→User navigates to the activity→ onCreate→ onRestart→ onResume→ ……
注:標紅加方括號的onStop表示可有可無,也就是外圈有兩條路線——Pause狀態(tài)下被Kill掉和Stop狀態(tài)下被Kill掉。
正因為有這么多狀態(tài)變換路徑,就是因為用戶交互復雜導致。下面,對生命周期的事件回調做以下簡單說明:
(1)onPause和onResume對應,onStop和onStart/onRestart對應;
(2)執(zhí)行過onPause、onStop和onDestroy的Activity在系統(tǒng)內存不足的情況下都有可能被Kill掉,當然Kill的優(yōu)先級不同,Destroy的最先被Kill,其次是Stop的,實在沒轍了才Kill Pause狀態(tài)的Activity。
(3)對于被Kill掉的Activity,如果用戶重新回到那個Activity的話,需要再次調用onCreate方法,創(chuàng)建Activity實例;
(4)onStop不一定始終都被執(zhí)行,如Pause狀態(tài)的Activity也可能被Kill掉,所以保存應用程序狀態(tài)數據的代碼,應該寫在onPause中;
(5)各事件回調函數的調用時機及Activity狀態(tài)如下:
4 登錄功能的實現詳解
從這節(jié)開始,我們正式進入登錄、注冊功能的實現。由于使用C/S結構,所以分為客戶端和服務器端兩個部分,客戶端和服務器端之間交互采用HTTP協(xié)議,如下:
4.1 客戶端邏輯
客戶端繼承Activity得到兩個子類,LoginActivity和ResgiterActivity。注冊功能的客戶端代碼與登錄類似,在此不再贅述。下面來看LoginActivity的代碼邏輯,在LoginActivity中主要重寫了onCreate方法,這一方法中首先加載該Activity的UI,即$res/layout/activity_login.xml(在第2小節(jié)已經詳細分析過了),然后對注冊、登陸兩個按鈕添加監(jiān)聽器,代碼邏輯如下:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); mLoginButton = (Button) findViewById(R.id.btn_login); mRegisterButton = (Button) findViewById(R.id.btn_register); mEditTextUserName = (EditText) findViewById(R.id.edt_login_cellphone_number); mEditTextPassword = (EditText) findViewById(R.id.edt_login_password); mDvdPassword = findViewById(R.id.dvd_login_password); mDvdUserName = findViewById(R.id.dvd_login_username); mLoginButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.d("OnClick", "Enter the click callback of Login Button"); String cellphone_number = mEditTextUserName.getText().toString().trim(); String pass_word = mEditTextPassword.getText().toString().trim(); Map<String, String> params = new HashMap<String, String>(); params.put("url", LOGIN_PATH); params.put("cellphone_number", cellphone_number); params.put("pass_word", pass_word); new LoginAsyncTask(LoginActivity.this).execute(params); } }); mRegisterButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Enter into the register activity Intent intent = new Intent(LoginActivity.this, RegisterActivity.class); startActivity(intent); } }); mEditTextPassword.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (hasFocus) { mDvdPassword.setBackgroundColor(getResources().getColor(R.color.colorPrimary)); } else { mDvdPassword.setBackgroundColor(getResources().getColor(R.color.colorDefault)); } } }); mEditTextUserName.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (hasFocus) { mDvdUserName.setBackgroundColor(getResources().getColor(R.color.colorPrimary)); } else { mDvdUserName.setBackgroundColor(getResources().getColor(R.color.colorDefault)); } } }); }
說明:
(1)第5行,調用setContentView設置UI布局文件,這句代碼必須先寫上,否則后面的View視圖都沒法獲得;
(2)第7~12行,調用findViewById方法得到對應ID的View,此處的ID在編寫UI布局時指定;
(3)通過匿名內部類的方式,向登錄按鈕注冊Click事件監(jiān)聽器,在該事件監(jiān)聽器的內部邏輯中,首先從手機號的編輯框和密碼的編輯框中獲得文本內容,然后通過一個異步任務(AsyncTask)將內容發(fā)送到服務器端,異步任務的代碼如下:
public class LoginAsyncTask extends AsyncTask<Map<String, String>, Void, Boolean> { private ProgressDialog mDialog; private Context mContext; public LoginAsyncTask(Context context) { mDialog = new ProgressDialog(context); mDialog.setTitle("提示信息"); mDialog.setMessage("正在登錄,請稍等..."); mContext = context; } @Override protected Boolean doInBackground(Map<String, String>... params) { String url = params[0].get("url"); Map<String, String> mapParams = new Hashtable(); for (Map.Entry<String, String> entry : params[0].entrySet()) { if (!entry.getKey().equals("url")) { mapParams.put(entry.getKey(), entry.getValue()); } } String result = null; try { result = HttpUtil.sendPostRequest(url, mapParams, "utf-8"); } catch (Exception e) { e.printStackTrace(); } return result.equals("True") ? true : false; } @Override protected void onPostExecute(Boolean result) { super.onPostExecute(result); if (mDialog.isShowing()) mDialog.dismiss(); if (result) { // jump to Main page Intent intent = new Intent(mContext, MainActivity.class); mContext.startActivity(intent); } else { Toast.makeText(mContext, "登錄失敗!", Toast.LENGTH_LONG).show(); } } }
異步任務,顧名思義就是要開啟一個線程來執(zhí)行的代碼邏輯。在Android中,顯示應用程序界面和接受用戶輸入的代碼都是在UI線程中執(zhí)行,所以UI線程一般不允許阻塞,否則會造成用戶體驗差。對一些耗時操作,需要由非UI線程來執(zhí)行,執(zhí)行完成后的結果由UI線程來更新。在這里,因為提交用戶名和密碼的網絡操作耗時較長,如果直接在UI線程中執(zhí)行的話,會導致UI線程阻塞,引起Android Not Responding異常(見下圖,很熟悉吧),所以得放在異步任務中執(zhí)行。
Android框架中在Java多線程框架之上,引入了AsyncTask(異步任務)和Handler/Message/Loop兩種機制來實現多線程編程。下面對異步任務做一個簡單說明(詳細講的話可能需要一小節(jié)的內容),Handler/Message/Loop機制較復雜,后續(xù)用到了我們再介紹。AsyncTask是一個抽象類,必須要重寫的方法為doInBackground方法,這個方法運行在后臺線程中,在上述代碼中就是執(zhí)行發(fā)送網絡請求,并對返回結果進行解析。另外還有兩個函數也比較重要,onPreExecute和onPostExecute。onPreExecute運行在UI線程中,并且在doInBackground之前調用,主要用于異步任務的初始化,例如顯示進度對話框等;onPostExecute在doInBackground之后調用,也是運行于UI線程,主要用于異步任務結果在UI中的更新顯示。上述代碼中主要是根據返回結果,判斷登陸是否成功,如果成功,則跳轉到MainActivity,即通過startActivity開啟一個新的Activity,即應用程序的主界面;如果失敗,則彈出一個Toast提示用戶登錄失敗。
另外可以看到AsyncTask是一個模板類,有三個模板參數,如上述程序中AsyncTask<Map<String, String>, Void, Boolean>,其中第一個模板參數,如Map<String, String>,用于指定doInBackground的入參類型;第三個模板參數,如此處的Boolean,是doInBackground的返回值類型,同時是onPostExecute的入參類型;第二個模板參數,用于指定進度值的類型(可以為Integer或Float等),也就是說,異步任務可以將進度值更新到UI線程中顯示,在這里由于沒有用到進度條刻度信息,所以類型設為Void。
發(fā)送命令,獲取服務器的返回結果的邏輯,我們封裝在HttpUtil類中,如下:
public class HttpUtil { public static String sendPostRequest( String path, Map<String, String> params, String encoding) throws Exception { StringBuilder sb = new StringBuilder(); if (params != null && !params.isEmpty()) { for (Map.Entry<String, String> entry : params.entrySet()) { sb.append(entry.getKey()).append("="); sb.append(URLEncoder.encode(entry.getValue(), encoding)); sb.append("&"); } sb.deleteCharAt(sb.length() - 1); } HttpURLConnection conn = (HttpURLConnection) new URL(path).openConnection(); conn.setConnectTimeout(5000); conn.setRequestMethod("POST"); conn.setDoOutput(true); OutputStream os = conn.getOutputStream(); os.write(sb.toString().getBytes()); os.flush(); if (conn.getResponseCode() == 200) { String result = StreamTool.readStream(conn.getInputStream()); return result; } else { return null; } } }
主要邏輯就是把命令參數用&符號鏈接起來,寫到HttpURLConnection中,并通過HttpURLConnection發(fā)送到服務器端;服務器返回后,讀取狀態(tài)碼,如果為200,則表明連接執(zhí)行成功,此時讀取從服務器返回的值,通過StreamTool從返回的流中讀取,代碼邏輯如下:
public class StreamTool { public static String readStream(InputStream stream) throws IOException { StringBuilder sb = new StringBuilder(); BufferedReader in = new BufferedReader(new InputStreamReader(stream)); String line; while ((line = in.readLine()) != null) { sb.append(line); System.out.println("===>" + line); } return sb.toString(); } }
至此,登陸功能的客戶端代碼就寫完了。有興趣的童鞋可以到云盤上去下載源代碼:http://pan.baidu.com/s/1skHOkxB(有空去注冊一個github,感覺用百度云盤分享太low,^__^)。
4.2 服務端邏輯
服務器端就是寫兩個Servlet,LoginServlet和RegisterServlet。我們將數據庫查詢和修改操作封裝在UserDAOImpl中,在LoginServlet和RegisterServlet中調用UserDAOImpl的接口,實現用戶信息的驗證和添加,下面仍然以LoginServlet為例。
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); response.setCharacterEncoding("utf-8"); // parse parameters from client. String cellphone_number = request.getParameter("cellphone_number"); String pass_word = request.getParameter("pass_word"); PrintWriter writer = response.getWriter(); try { UserDAOInterface userDAO = new UserDAOImpl(); User user = userDAO.queryByCellPhoneNumber(cellphone_number); if (user != null && user.getPassWord().equals(pass_word)) { writer.append("True"); System.out.println("True"); } else { writer.append("False"); System.out.println("False"); } } catch (SQLException | ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { } }
簡單解釋一下:獲取從客戶端提交的參數,分別是手機號和用戶密碼,通過手機號從數據庫查詢對應的用戶(封裝在User實例中)記錄,如果用戶不為空,且用戶密碼正確,則返回True,否則返回為空。服務器端的代碼就是Java Web編程,所以在這里就不詳細討論了。
5 總結
最后總結一下,本文我們主要學習了Layout布局以及展示布局的組件——Activity,對涉及到的View進行較詳細的分析,對Activity的概念和生命周期回調也進行了介紹,最后以介紹了登錄功能的代碼實現,注冊功能和登陸功能類似,感興趣的童鞋可以把代碼down下來瞅瞅。
下一次將介紹好友列表功能的實現,敬請關注^__^
源碼下載:http://xiazai.jb51.net/201606/yuanma/MyChat(jb51.net).rar
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
Android日期選擇器對話框DatePickerDialog使用詳解
這篇文章主要為大家詳細介紹了Android日期選擇器對話框DatePickerDialog的使用,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-01-01Android實現中軸旋轉特效 Android制作別樣的圖片瀏覽器
這篇文章主要為大家詳細介紹了Android實現中軸旋轉特效,制作別樣的圖片瀏覽器,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-11-11android:descendantFocusability方法介紹
開發(fā)中很常見的一個問題,項目中的listview不僅僅是簡單的文字,常常需要自己定義listview,問題就出現了,可能會發(fā)生點擊每一個item的時候沒有反應,無法獲取的焦點2012-11-11