1 概述
从这篇博文开始,正式进入简易版微信的开发。深入学习前,想谈谈个人对Android程序开发一些理解,不一定正确,只是自己的一点想法。Android程序开发不像我们在大学时候写C控制台程序那样,需要从main开始写代码逻辑,大部分逻辑控制代码都由自己来实现。事实上,Android已经为我们提供了一个程序运行的框架,我们只需要往框架中填入我们所需的内容即可,这里的内容主要是:四大组件——Activity、Service、ContentProvider、BroadCast。在这四大组件中,可以实现前端界面显示和后端数据处理相关的代码控制逻辑。关于前端界面显示主要涉及到:组件的生命周期回调管理、注册视图(View)的事件监听器、集合类型视图的数据适配器(Adapter)、不同窗口界面的跳转等等。关于后台数据的交互处理,主要涉及到:异步任务(AsyncTask)、Handler/Message、网络编程(HTTP或Socket)、数据库操作(SQLiteOpenHelper或ContentProvider)等等。所以,对我们初学者来说,学习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文件中,然后通过Activity的SetContentView接口将布局的描述文件设置给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>
上述代码一层套一层,最终形成一个树状结构,如下图所示:
图中每个矩形就是一个控件(或称为视图),每个控件都有一套与它相关的外观属性(类似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_width和android: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_Top、android:margin_Buttom、android:margin_Left和android: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设置为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按钮,分别用于触发登录、注册操作:
代码如下:
<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)onPause和onResume对应,onStop和onStart/onRestart对应;
(2)执行过onPause、onStop和onDestroy的Activity在系统内存不足的情况下都有可能被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得到两个子类,LoginActivity和ResgiterActivity。注册功能的客户端代码与登录类似,在此不再赘述。下面来看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 }
说明:
(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 }
异步任务,顾名思义就是要开启一个线程来执行的代码逻辑。在Android中,显示应用程序界面和接受用户输入的代码都是在UI线程中执行,所以UI线程一般不允许阻塞,否则会造成用户体验差。对一些耗时操作,需要由非UI线程来执行,执行完成后的结果由UI线程来更新。在这里,因为提交用户名和密码的网络操作耗时较长,如果直接在UI线程中执行的话,会导致UI线程阻塞,引起Android Not Responding异常(见下图,很熟悉吧),所以得放在异步任务中执行。
Android框架中在Java多线程框架之上,引入了AsyncTask(异步任务)和Handler/Message/Loop两种机制来实现多线程编程。下面对异步任务做一个简单说明(详细讲的话可能需要一小节的内容),Handler/Message/Loop机制较复杂,后续用到了我们再介绍。AsyncTask是一个抽象类,必须要重写的方法为doInBackground方法,这个方法运行在后台线程中,在上述代码中就是执行发送网络请求,并对返回结果进行解析。另外还有两个函数也比较重要,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。
发送命令,获取服务器的返回结果的逻辑,我们封装在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 }
主要逻辑就是把命令参数用&符号链接起来,写到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 }
至此,登陆功能的客户端代码就写完了。有兴趣的童鞋可以到云盘上去下载源代码:http://pan.baidu.com/s/1skHOkxB(有空去注册一个github,感觉用百度云盘分享太low,^__^)。
4.2 服务端逻辑
服务器端就是写两个Servlet,LoginServlet和RegisterServlet。我们将数据库查询和修改操作封装在UserDAOImpl中,在LoginServlet和RegisterServlet中调用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 }
简单解释一下:获取从客户端提交的参数,分别是手机号和用户密码,通过手机号从数据库查询对应的用户(封装在User实例中)记录,如果用户不为空,且用户密码正确,则返回True,否则返回为空。服务器端的代码就是Java Web编程,所以在这里就不详细讨论了。
5 总结
最后总结一下,本文我们主要学习了Layout布局以及展示布局的组件——Activity,对涉及到的View进行较详细的分析,对Activity的概念和生命周期回调也进行了介绍,最后以介绍了登录功能的代码实现,注册功能和登陆功能类似,感兴趣的童鞋可以把代码down下来瞅瞅。
下一次博文将介绍好友列表功能的实现,敬请关注^__^
附:
源代码地址:http://pan.baidu.com/s/1skHOkxB
点进去之后有客户端代码和服务器端代码~