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


1 概述

從這篇博文開始,正式進入簡易版微信的開發。深入學習前,想談談個人對Android程序開發一些理解,不一定正確,只是自己的一點想法。Android程序開發不像我們在大學時候寫C控制台程序那樣,需要從main開始寫代碼邏輯,大部分邏輯控制代碼都由自己來實現。事實上,Android已經為我們提供了一個程序運行的框架,我們只需要往框架中填入我們所需的內容即可,這里的內容主要是:四大組件——ActivityServiceContentProviderBroadCast。在這四大組件中,可以實現前端界面顯示和后端數據處理相關的代碼控制邏輯。關於前端界面顯示主要涉及到:組件的生命周期回調管理、注冊視圖(View)的事件監聽器、集合類型視圖的數據適配器(Adapter)、不同窗口界面的跳轉等等。關於后台數據的交互處理,主要涉及到:異步任務(AsyncTask)、Handler/Message、網絡編程(HTTPSocket)、數據庫操作(SQLiteOpenHelperContentProvider)等等。所以,對我們初學者來說,學習Android主要就是學習Android框架中各個類的作用和使用方法。

好,下面開始本文內容。當第一次使用微信(或其他常用的Android應用)的時候,首先就是注冊、登錄,本文就來實現這兩個基本功能。由於剛接觸Android開發,所以需要了解很多基礎知識點。我們將通過這兩個功能的實現,學習以下幾個Android開發的知識點:

Layout布局:制作用戶界面,Android中使用XML文件描述UI布局,類似HTML+CSS方式的界面組件方式。對后端的童鞋來說,按UI設計稿進行布局或按需求來定制一個控件或許是學習前端最大的障礙之一。關於UI布局,本文起一個頭,隨着我們的簡易版微信應用深入開發,我們就會慢慢熟悉Android的UI布局了;關於自己動手開發一個視圖(View),這應該也是Android開發中的難點,我們將在后續文章中慢慢深入學習。

Activity概念及其生命周期:布局完成后,要將布局得到的UI界面顯示出來,這就需要引入Activity組件——負責UI界面的顯示和用戶的交互。Activity應該是Android應用最重要的組件了 —— 一個應用可以沒有四大組件中的其他三大組件(即:內容提供者ContentProvider、服務Service、廣播BroadCast),但不能沒有Activity —— 這個組件類似Windows編程中的窗口,在Windows中如果沒有窗口怎么與用戶交互?

登錄、注冊功能的實現:講完Activity后,就需要通過Activity來加入我們需要的邏輯。Android應用程序一般都是C(客戶端)/S(服務端)結構的,注冊、登陸功能的實現包括客戶端邏輯的編寫和服務器端邏輯的編寫,我們將在第4小節介紹這兩個功能的客戶端和服務端的邏輯實現。

最后總結本篇博文內容,並預告下篇博文內容,那就讓我們開啟Android學習的第二課吧!

2 Android的MVC結構

當學習一門新技術時,我們很少會思考這門技術重點學習什么,應該怎么去學習之類的問題。大多數童鞋常常會一開始就一頭扎到知識點的海洋中,最后自己也搞不清學會了什么。比如學習Java,一上來就從變量命名開始學、接着學習表達式、控制流、面向對象,如果初學者也許這是合適的,但如果你已經學會C或C++,有些知識點似乎就不需要學習了。比如我之前包括現在主要用的是C++,那一上來就會學習I/O流、集合類這些常用的知識點,就可以開發一些小程序了。有時間的話再去看看多線程、垃圾收集以及源代碼。

學習Android也一樣,首先應該弄清楚應該學一些什么,這就要從高一些的層次來看Android。從架構上來說,和很多UI框架一樣,Android用的是主流的MVC結構,這應該是比較成熟的前端框架了。MVC框架結構如下圖:

 

MVC結構分為三部分:

控制器(Controller)部分:接收用戶輸入,通過事件分發機制確定接收者。這部分在Android中已有框架完成,我們只需在Activity中向View視圖實例對象注冊特定監聽器即可,監聽器實現的具體邏輯由我們來寫;而且監聽器只需要知道有這么回事就行,用到去API查就可以。

模型(Model)部分:這部分主要實現業務邏輯的處理和數據的更新。這部分應該是Android編程的重點,四大組件中的Service(服務)、ContentProvide(內容提供者)都是Model(模型)有關的,另外數據存儲,如數據庫、文件等也屬於Model范疇,這部分應該是Android學習的重點。

視圖(View)部分:這部分就是用於顯示模型數據。這部分在Android中就是使用View視圖進行UI布局,有時框架提供的View部件不滿足需求時,得根據需求重寫View,實現我們需要的效果。

這樣划分之后,我們就大體上知道了一個Android軟件由哪些部分組成以及它們之間如何是交互的,Android框架已經為我們實現了哪些功能 ,哪些功能需要我們擴展的,這樣我們學習起來才會有的放矢。

3 Layout布局及分析

關於做軟件UI,博主曾經有一段比較痛苦的回憶。記得那是在大三上學期學習完《數據庫系統概論》這門課程之后,老師要求用ASP.NET做一個網站。當時博主做的是一個在線購書系統,不懂怎么制作網頁界面,於是就在Visual Studio中以拖拽控件的方式來布局,最后雖然把系統倒騰出來了(過程可以說是十分痛苦),但界面看了實在無法讓人產生購買的欲望。經歷過這么一出之后,博主對前端界面產生了恐懼感和厭惡感。不過,進入公司參加工作以來,慢慢接觸到了軟件UI的設計與實現過程,同時自己也動手實現了一些界面布局后,才讓這種恐懼感和厭惡感慢慢減少了。在這里,博主想來一句經驗之談:要想做一個漂亮的UI布局,不是通過拖拽控件能拖出來的。當然,對初學者來說,可以通過通過拖拽控件的方式來學習Android框架。

Android制作UI界面有兩種方式:

(1)通過XML配置文件的方式,博主一般稱它為“聲明式布局”(不知對不對):這種方式就是把UI要顯示的控件及這些控件的顯示方式聲明在XML文件中,然后通過ActivitySetContentView接口將布局的描述文件設置給Activity

(2)通過Java類來添加布局控件,並設置顯示相關的屬性,博主一般稱這一布局方式為“命令式布局”。

第(1)種布局方式,即聲明式布局,一般用於變化不大UI的布局;第(2)種布局方式,即命令式布局,一般用於程序運行時不斷變化的UI界面的布局。本篇博文將實現的登陸、注冊功能采用的是聲明式布局,所以本小節僅介紹聲明式布局,命令式布局將在后續博文中用到時再做詳細闡述。

好了,理論的東西就不扯太多了,搞軟件開發的最怕聽到一大堆理論了,下面讓我們來看看登陸和注冊的布局界面的實現效果吧(可能還不是很完美,以后邊學習邊完善吧!)。首先是登陸頁面(這也是打開軟件后的第一個頁面):

注冊頁面:

注冊、登錄之間交互與登錄成功后的界面,這里登錄成功后的界面上什么都沒有,所以在此就沒單獨貼出來了。圖片有點糊,湊合看看哈~

下面以登錄界面的代碼,來看看Android中如何實現界面布局的,整個UI布局代碼如下(代碼路徑:$res/layout/activity_login.xml):

  1 <?xml version="1.0" encoding="utf-8"?>
  2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3     android:layout_width="match_parent"
  4     android:layout_height="match_parent"
  5     android:orientation="vertical">
  6 
  7     <!--Top Panel-->
  8     <TextView
  9         android:layout_width="match_parent"
 10         android:layout_height="50dp"
 11         android:background="@color/colorTopPanelBackground"
 12         android:gravity="center"
 13         android:text="@string/string_login"
 14         android:textSize="@dimen/font_size_large"
 15         android:textColor="@color/colorSpecialWhite" />
 16 
 17     <LinearLayout
 18         android:layout_width="match_parent"
 19         android:layout_height="match_parent"
 20         android:layout_margin="@dimen/activity_horizontal_margin"
 21         android:orientation="vertical">
 22 
 23         <LinearLayout
 24             android:layout_width="match_parent"
 25             android:layout_height="50dip"
 26             android:orientation="horizontal"
 27             android:layout_marginTop="50dp">
 28 
 29             <TextView
 30                 android:layout_width="50dip"
 31                 android:layout_height="50dip"
 32                 android:gravity="center_vertical|right"
 33                 android:text="+86"
 34                 android:textColor="@color/colorSpecialBlack"
 35                 android:textSize="@dimen/font_size_medium" />
 36 
 37             <EditText
 38                 android:id="@+id/edt_login_cellphone_number"
 39                 android:layout_width="0dp"
 40                 android:layout_height="50dip"
 41                 android:layout_weight="1"
 42                 android:layout_marginLeft="25dp"
 43                 android:background="@null"
 44                 android:hint="你的手機號碼"
 45                 android:textSize="@dimen/font_size_medium"
 46                 android:textColorHint="@color/colorHintText"/>
 47 
 48         </LinearLayout>
 49 
 50         <View
 51             android:id="@+id/dvd_login_username"
 52             android:layout_width="match_parent"
 53             android:layout_height="2px"
 54             android:background="@color/colorDefault" />
 55 
 56         <LinearLayout
 57             android:layout_width="match_parent"
 58             android:layout_height="50dip"
 59             android:orientation="horizontal">
 60 
 61             <TextView
 62                 android:layout_width="50dip"
 63                 android:layout_height="50dip"
 64                 android:gravity="center_vertical|right"
 65                 android:text="@string/string_pass_word"
 66                 android:textColor="@color/colorSpecialBlack"
 67                 android:textSize="@dimen/font_size_medium" />
 68 
 69             <EditText
 70                 android:id="@+id/edt_login_password"
 71                 android:layout_width="0dp"
 72                 android:layout_height="50dip"
 73                 android:layout_weight="1"
 74                 android:layout_marginLeft="25dp"
 75                 android:background="@null"
 76                 android:inputType="textPassword"
 77                 android:textSize="@dimen/font_size_medium"
 78                 android:hint="填入密碼"
 79                 android:textColorHint="@color/colorHintText"/>
 80 
 81         </LinearLayout>
 82 
 83         <View
 84             android:id="@+id/dvd_login_password"
 85             android:layout_width="match_parent"
 86             android:layout_height="2px"
 87             android:background="@color/colorDefault" />
 88 
 89         <Button
 90             android:id="@+id/btn_login"
 91             android:layout_width="match_parent"
 92             android:layout_height="@dimen/button_general_height"
 93             android:layout_marginTop="50dip"
 94             android:background="@drawable/btn_common_selector"
 95             android:text="@string/string_login"
 96             android:textSize="@dimen/font_size_medium"
 97             android:textColor="@color/colorSpecialWhite"/>
 98 
 99         <Button
100             android:id="@+id/btn_register"
101             android:layout_width="match_parent"
102             android:layout_height="@dimen/button_general_height"
103             android:layout_marginTop="20dip"
104             android:background="@drawable/btn_implicit_selector"
105             android:text="@string/string_register"
106             android:textSize="@dimen/font_size_medium" />
107 
108     </LinearLayout>
109 
110 </LinearLayout>
View Code

上述代碼一層套一層,最終形成一個樹狀結構,如下圖所示:

圖中每個矩形就是一個控件(或稱為視圖),每個控件都有一套與它相關的外觀屬性(類似Web編程中的CSS),控制着該控件的顯示效果。下面對逐個控件及其外觀屬性進行深入分析,從根節點開始:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

根節點是一個布局控件,在這里用的是線性布局(LinearLayout),其它的布局控件還有相對布局(RelativeLayout)和幀布局(FrameLayout)。布局控件的作用就是堆砌(專業點的說法叫布置Arrange)控件:線性布局,顧名思義,布局方式只能按一個方向(水平horizontal或垂直vertical)堆砌控件,如上述代碼塊中,android:orientation屬性用於說明LinearLayout是水平橫向的線性布局;相對布局,這一布局方式比線性布局要復雜一下,控件之間位置關系不像線性布局那樣只能沿着一個方向,這種布局下,控件的位置根據已有的其他控件來確定的(該布局的具體實例將在后續博文中闡述);幀布局。另外,上述代碼塊中還有兩個屬性:android:layout_widthandroid:layout_height,用來描述該控件的寬與高,這也是每個控件都要填的屬性。這兩個屬性的值指定的是一個長度值,可以用像素(px)、點(pt)、設備獨立像素(dp或dip),這里用的是一個特殊值:match_parent——匹配父窗口,即長或寬和父窗口一樣;另外一個特殊值是:wrap_content——內容包裹,即長或寬和空間中內容匹配,內容所占區域有多大,控件的長或寬就是多大。

<!--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_Topandroid:margin_Buttomandroid:margin_Leftandroid:margin_Right。接下來就是表單的內容區域了,首先要顯示兩個輸入框及其說明文字,輸入框使用的是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設置為numberphone時,則軟鍵盤為:

剩下的兩個屬性都和輸入框都和提示文字有關,分別為android:hintandroid:textColorHint,分別用於描述提示文字的文本內容和文本顏色,如下:

上圖中輸入框下面有一條綠色的橫線,這里采用的做法是設置一個高度為2px(一般來說,1px就可以,不過個人感覺在這里不夠明顯,所以就設為2px了)、寬度為match_parentView,如下:

<View
            android:id="@+id/dvd_login_password"
            android:layout_width="match_parent"
            android:layout_height="2px"
            android:background="@color/colorDefault" />

再接下來就是兩個Button按鈕,分別用於觸發登錄、注冊操作:

 代碼如下:

<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)的資源——因為按鈕按下和彈起的時候,其背景是不一樣的,如下為按下狀態:

和前面的彈起時的狀態比較一下,背景色變深了,這就用選擇器資源了。該資源為$res\color目錄下(注:創建Android工程時,默認是沒有這個文件夾的,需要手動創建),內容如下:

<?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>

分別指定了按下和默認的顏色。到此為止,我們將登陸布局頁面講完了,注冊頁面和登陸頁面沒什么區別,在這里就不在闡述了。最后,這里還想分享一些個人經驗

(1)布局其實就是兩步:第一步:確定用什么控件;第二步:為控件配上屬性;

(2)這么多屬性,書寫時最好要有一個次序,我的做法是:第一個是id屬性;其次是必要的寬、高屬性,接下來是布局相關的屬性,如內外邊距等,再接着是背景色、內容的對齊方式等,最后是控件內容,如文字內容、顏色、大小等,這是一個由外到內、從通用屬性到特殊屬性的一個書寫次序。

(3)盡量將一些屬性值定義在資源文件中,便於代碼的后期維護和復用。

3 Activity概念及其生命周期

第2小結詳細講述了登錄界面,介紹了涉及到的View組件及相關外觀樣式屬性。定義在xml中的UI布局文件只是一個靜態文件,需要加載到應用程序中,才能被渲染並顯示出來,這就要用到本節所講的Activity(活動)。本節主要總結Activity的一些理論知識,包括Activity的概念和生命周期。

3.1 什么是Activity

很多關於Android編程的書籍對Activity的概念都或多或少有一些闡述,但個人感覺都不是很系統。本文在這里拋出一塊磚,總結一下自己對Android中Activity組件的理解(當然有很多不完善的地方,以后我會慢慢補充)如下:

(I)從MVC模式的角度看,Activity相當於Controller——一邊是接收用戶請求,另一邊將請求分發(dispatch)到各處理單元中;也就是在一個Android中,Activity起到一個核心的作用:

圖中標出了通過Activity啟動各大組件的APIs,這些APIs都定義在Activity中。當然,上述圖示僅是一個簡單的模型,在接收用戶請求時可能還會用到其他組件,在圖中並沒有一一給出,這些組件會在后續博文中深入學習。

(II)從設計模式模式的角度看,Activity可以看做是一個門面模式(Facade Pattern)。在Activity中聚合了很多組件,見下圖:

對內封裝復雜組件,對外提供簡單的接口,同時也能獨立獲取這些組件實例,這就是門面模式的典型應用吧!這里需要注意的是,Activity繼承自ContextThemeWrapper,在ContextThemeWrapper中聚合了Resource,通過它訪問程序資源;而ContextThemeWrapper繼承自ContextWrapper,通過它可以得到內容解析器(ContentResolver)等組件。總的來說,Activity提供了很多功能,封裝了很多組件,使用起來也非常靈活。

(III)從一個Android開發者角度看,Activity是一個狀態機。Activity定義了管理一個活動的生命周期的一系列事件,通過這些事件可以保存應用程序的狀態,這些事件將在3.2中闡述。

3.2 Activity的生命周期

Activity的生命周期是每一本講Android編程的書必講的內容,也是Android程序設計的重點。Android的四大組件都有生命周期的概念,但Activity的生命周期最復雜,下圖是來自Android SDK文檔的一張Activity生命周期事件回調函數的調用次序:

 這個圖有點像操作系統課中進程狀態轉換圖——各種狀態切來切去。初看這張圖的時候,應該會感覺有點亂,其實理清楚了的話,圖中顯示了也就是四條狀態變換路徑:

(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狀態下被Kill掉和Stop狀態下被Kill掉。

正因為有這么多狀態變換路徑,就是因為用戶交互復雜導致。下面,對生命周期的事件回調做以下簡單說明:

(1)onPauseonResume對應,onStoponStart/onRestart對應;

(2)執行過onPauseonStoponDestroyActivity在系統內存不足的情況下都有可能被Kill掉,當然Kill的優先級不同,Destroy的最先被Kill,其次是Stop的,實在沒轍了才Kill Pause狀態的Activity。

(3)對於被Kill掉的Activity,如果用戶重新回到那個Activity的話,需要再次調用onCreate方法,創建Activity實例;

(4)onStop不一定始終都被執行,如Pause狀態的Activity也可能被Kill掉,所以保存應用程序狀態數據的代碼,應該寫在onPause中;

(5)各事件回調函數的調用時機及Activity狀態如下:

函數名稱 調用時機及Activity狀態
onCreate 首次創建時調用(用於設置UI),Activity不可見
onStart Activity對用戶可見時調用,Activity可見但不可交互
onResume Activity可以與用戶交互時調用,Activity可見且可交互
onPause Acitivity被暫停時調用,Activity可見但不可交互
onStop Activity對用戶不可見時調用,Activity不可見且不可交互
onDestroy Activity被系統銷毀前調用
onRestart 處於Stop的Activity重新被啟動時調用

4 登錄功能的實現詳解

從這節開始,我們正式進入登錄、注冊功能的實現。由於使用C/S結構,所以分為客戶端和服務器端兩個部分,客戶端和服務器端之間交互采用HTTP協議,如下:

4.1 客戶端邏輯 

客戶端繼承Activity得到兩個子類,LoginActivityResgiterActivity。注冊功能的客戶端代碼與登錄類似,在此不再贅述。下面來看LoginActivity的代碼邏輯,在LoginActivity中主要重寫了onCreate方法,這一方法中首先加載該Activity的UI,即$res/layout/activity_login.xml(在第2小節已經詳細分析過了),然后對注冊、登陸兩個按鈕添加監聽器,代碼邏輯如下:

 1 @Override
 2     protected void onCreate(Bundle savedInstanceState) {
 3         super.onCreate(savedInstanceState);
 4 
 5         setContentView(R.layout.activity_login);
 6 
 7         mLoginButton = (Button) findViewById(R.id.btn_login);
 8         mRegisterButton = (Button) findViewById(R.id.btn_register);
 9         mEditTextUserName = (EditText) findViewById(R.id.edt_login_cellphone_number);
10         mEditTextPassword = (EditText) findViewById(R.id.edt_login_password);
11         mDvdPassword = findViewById(R.id.dvd_login_password);
12         mDvdUserName = findViewById(R.id.dvd_login_username);
13 
14         mLoginButton.setOnClickListener(new View.OnClickListener() {
15             @Override
16             public void onClick(View v) {
17 
18                 Log.d("OnClick", "Enter the click callback of Login Button");
19 
20                 String cellphone_number = mEditTextUserName.getText().toString().trim();
21                 String pass_word        = mEditTextPassword.getText().toString().trim();
22 
23                 Map<String, String> params = new HashMap<String, String>();
24                 params.put("url", LOGIN_PATH);
25                 params.put("cellphone_number", cellphone_number);
26                 params.put("pass_word", pass_word);
27 
28                 new LoginAsyncTask(LoginActivity.this).execute(params);
29             }
30         });
31 
32         mRegisterButton.setOnClickListener(new View.OnClickListener() {
33             @Override
34             public void onClick(View v) {
35                 // Enter into the register activity
36                 Intent intent = new Intent(LoginActivity.this, RegisterActivity.class);
37                 startActivity(intent);
38             }
39         });
40 
41         mEditTextPassword.setOnFocusChangeListener(new View.OnFocusChangeListener() {
42             @Override
43             public void onFocusChange(View v, boolean hasFocus) {
44                 if (hasFocus) {
45                     mDvdPassword.setBackgroundColor(getResources().getColor(R.color.colorPrimary));
46                 } else {
47                     mDvdPassword.setBackgroundColor(getResources().getColor(R.color.colorDefault));
48                 }
49             }
50         });
51 
52         mEditTextUserName.setOnFocusChangeListener(new View.OnFocusChangeListener() {
53             @Override
54             public void onFocusChange(View v, boolean hasFocus) {
55                 if (hasFocus) {
56                     mDvdUserName.setBackgroundColor(getResources().getColor(R.color.colorPrimary));
57                 } else {
58                     mDvdUserName.setBackgroundColor(getResources().getColor(R.color.colorDefault));
59                 }
60             }
61         });
62     }
View Code

 說明:

(1)第5行,調用setContentView設置UI布局文件,這句代碼必須先寫上,否則后面的View視圖都沒法獲得;

(2)第7~12行,調用findViewById方法得到對應ID的View,此處的ID在編寫UI布局時指定;

(3)通過匿名內部類的方式,向登錄按鈕注冊Click事件監聽器,在該事件監聽器的內部邏輯中,首先從手機號的編輯框和密碼的編輯框中獲得文本內容,然后通過一個異步任務(AsyncTask)將內容發送到服務器端,異步任務的代碼如下:

 1 public class LoginAsyncTask extends AsyncTask<Map<String, 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 
11         mContext = context;
12     }
13 
14     @Override
15     protected Boolean doInBackground(Map<String, String>... params) {
16         String url = params[0].get("url");
17 
18         Map<String, String> mapParams = new Hashtable();
19         for (Map.Entry<String, String> entry : params[0].entrySet()) {
20             if (!entry.getKey().equals("url")) {
21                 mapParams.put(entry.getKey(), entry.getValue());
22             }
23         }
24 
25         String result = null;
26         try {
27             result = HttpUtil.sendPostRequest(url, mapParams, "utf-8");
28         } catch (Exception e) {
29             e.printStackTrace();
30         }
31 
32         return result.equals("True") ? true : false;
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 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

異步任務,顧名思義就是要開啟一個線程來執行的代碼邏輯。在Android中,顯示應用程序界面和接受用戶輸入的代碼都是在UI線程中執行,所以UI線程一般不允許阻塞,否則會造成用戶體驗差。對一些耗時操作,需要由非UI線程來執行,執行完成后的結果由UI線程來更新。在這里,因為提交用戶名和密碼的網絡操作耗時較長,如果直接在UI線程中執行的話,會導致UI線程阻塞,引起Android Not Responding異常(見下圖,很熟悉吧)所以得放在異步任務中執行。

Android框架中在Java多線程框架之上,引入了AsyncTask(異步任務)和Handler/Message/Loop兩種機制來實現多線程編程。下面對異步任務做一個簡單說明(詳細講的話可能需要一小節的內容),Handler/Message/Loop機制較復雜,后續用到了我們再介紹。AsyncTask是一個抽象類,必須要重寫的方法為doInBackground方法,這個方法運行在后台線程中,在上述代碼中就是執行發送網絡請求,並對返回結果進行解析。另外還有兩個函數也比較重要,onPreExecuteonPostExecuteonPreExecute運行在UI線程中,並且在doInBackground之前調用,主要用於異步任務的初始化,例如顯示進度對話框等;onPostExecute在doInBackground之后調用,也是運行於UI線程,主要用於異步任務結果在UI中的更新顯示。上述代碼中主要是根據返回結果,判斷登陸是否成功,如果成功,則跳轉到MainActivity,即通過startActivity開啟一個新的Activity,即應用程序的主界面;如果失敗,則彈出一個Toast提示用戶登錄失敗。

另外可以看到AsyncTask是一個模板類,有三個模板參數,如上述程序中AsyncTask<Map<String, String>, Void, Boolean>,其中第一個模板參數,如Map<String, String>,用於指定doInBackground的入參類型;第三個模板參數,如此處的Boolean,是doInBackground的返回值類型,同時是onPostExecute的入參類型;第二個模板參數,用於指定進度值的類型(可以為IntegerFloat等),也就是說,異步任務可以將進度值更新到UI線程中顯示,在這里由於沒有用到進度條刻度信息,所以類型設為Void

發送命令,獲取服務器的返回結果的邏輯,我們封裝在HttpUtil類中,如下:

 1 public class HttpUtil {
 2 
 3     public static String sendPostRequest(
 4             String path, Map<String, String> params, String encoding)
 5             throws Exception {
 6 
 7         StringBuilder sb = new StringBuilder();
 8         if (params != null && !params.isEmpty()) {
 9             for (Map.Entry<String, String> entry : params.entrySet()) {
10                 sb.append(entry.getKey()).append("=");
11                 sb.append(URLEncoder.encode(entry.getValue(), encoding));
12                 sb.append("&");
13             }
14             sb.deleteCharAt(sb.length() - 1);
15         }
16 
17         HttpURLConnection conn = (HttpURLConnection) new URL(path).openConnection();
18         conn.setConnectTimeout(5000);
19         conn.setRequestMethod("POST");
20         conn.setDoOutput(true);
21 
22         OutputStream os = conn.getOutputStream();
23         os.write(sb.toString().getBytes());
24         os.flush();
25 
26         if (conn.getResponseCode() == 200) {
27             String result = StreamTool.readStream(conn.getInputStream());
28             return result;
29         } else {
30             return null;
31         }
32     }
33 }
View Code

主要邏輯就是把命令參數用&符號鏈接起來,寫到HttpURLConnection中,並通過HttpURLConnection發送到服務器端;服務器返回后,讀取狀態碼,如果為200,則表明連接執行成功,此時讀取從服務器返回的值,通過StreamTool從返回的流中讀取,代碼邏輯如下:

 1 public class StreamTool {
 2 
 3     public static String readStream(InputStream stream) throws IOException {
 4 
 5         StringBuilder sb = new StringBuilder();
 6         BufferedReader in = new BufferedReader(new InputStreamReader(stream));
 7 
 8         String line;
 9         while ((line = in.readLine()) != null) {
10             sb.append(line);
11             System.out.println("===>" + line);
12         }
13 
14         return sb.toString();
15     }
16 
17 }
View Code

至此,登陸功能的客戶端代碼就寫完了。有興趣的童鞋可以到雲盤上去下載源代碼:http://pan.baidu.com/s/1skHOkxB(有空去注冊一個github,感覺用百度雲盤分享太low,^__^

4.2 服務端邏輯

服務器端就是寫兩個ServletLoginServletRegisterServlet。我們將數據庫查詢和修改操作封裝在UserDAOImpl中,在LoginServletRegisterServlet中調用UserDAOImpl的接口,實現用戶信息的驗證和添加,下面仍然以LoginServlet為例。

 1 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 2         request.setCharacterEncoding("utf-8");
 3         response.setCharacterEncoding("utf-8");
 4         
 5         // parse parameters from client.
 6         String cellphone_number = request.getParameter("cellphone_number");
 7         String pass_word         = request.getParameter("pass_word");
 8         
 9         PrintWriter writer = response.getWriter();
10         
11         try {
12             UserDAOInterface userDAO = new UserDAOImpl();
13             User user = userDAO.queryByCellPhoneNumber(cellphone_number);
14             
15             if (user != null && user.getPassWord().equals(pass_word))
16             {
17                 writer.append("True");
18                 System.out.println("True");
19             }
20             else
21             {
22                 writer.append("False");
23                 System.out.println("False");
24             }
25         } catch (SQLException | ClassNotFoundException e) {
26             // TODO Auto-generated catch block
27             e.printStackTrace();
28         } finally {
29             
30         }
31     }
View Code

簡單解釋一下:獲取從客戶端提交的參數,分別是手機號和用戶密碼,通過手機號從數據庫查詢對應的用戶(封裝在User實例中)記錄,如果用戶不為空,且用戶密碼正確,則返回True,否則返回為空。服務器端的代碼就是Java Web編程,所以在這里就不詳細討論了。

5 總結

最后總結一下,本文我們主要學習了Layout布局以及展示布局的組件——Activity,對涉及到的View進行較詳細的分析,對Activity的概念和生命周期回調也進行了介紹,最后以介紹了登錄功能的代碼實現,注冊功能和登陸功能類似,感興趣的童鞋可以把代碼down下來瞅瞅。

下一次博文將介紹好友列表功能的實現,敬請關注^__^

 

附:

源代碼地址:http://pan.baidu.com/s/1skHOkxB

點進去之后有客戶端代碼和服務器端代碼~


免責聲明!

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



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