第 3 章 activity的生命周期

第 3 章 activity的生命周期

新葡京娱乐场:  哈萨克斯坦驻上海总领事克拉把叶夫·佐齐汉与上海市商务委员会主任尚玉英、副主任杨朝,上海市人大常委、上海海派旗袍文化促进会会长张丽丽,中国国际商会世博部部长葛杰,中国(上海)自由贸易试验区管理委员会世博管理局副主任陆敏之等应邀出席。

你在第2章章末已看到,只要一旋转设备,地理知识问题就回到第一道初始题。用户旋转设备,应用状态就重置,是什么原因呢?要想搞明白,并解决这类常见的旋转问题,首先要学习activity生命周期的基础知识。

每个Activity实例都有其生命周期。在其生命周期内,activity在运行、暂停、停止和不存在这四种状态间转换。每次状态转换时,都有相应的Activity方法发消息通知activity。图3-1显示了activity的生命周期、状态以及状态切换时系统调用的方法。

图3-1 activity的状态图解

内存中有没有activity的实例,用户是否看得到,是否活跃在前台(等待或接受用户输入中),看图3-1的各种状态就知道了。完整总结如表3-1所示。

表3-1 activity的状态

状态

有内存实例

用户可见

处于前台

不存在

停止

暂停

是或部分*

运行

* 某些场景下,暂停状态的activity可能会部分或完全可见,详见3.1.3节。

用户可以与当前运行状态下的activity交互。设备上有很多应用,但是,任何时候只能有一个activity处于用户能交互的运行状态。

借助图3-1所示的方法,Activity的子类可以在activity的生命周期状态发生关键性转换时完成某些工作。这些方法通常被称为生命周期回调方法。

我们已熟悉这些方法中的onCreate(Bundle)方法。在创建activity实例后,但在此实例出现在屏幕上之前,Android操作系统会调用该方法。

通常,通过覆盖onCreate(Bundle)方法,activity可以预处理以下UI相关工作:

  • 实例化组件并将它们放置在屏幕上(调用setContentView(int)方法);

  • 引用已实例化的组件;

  • 为组件设置监听器以处理用户交互;

  • 访问外部模型数据。

切记,千万不要自己去调用onCreate(Bundle)方法或任何其他activity生命周期方法。为通知activity状态变化,你只需在Activity子类里覆盖这些方法,Android会适时调用它们(看当前用户状态以及系统运行情况)。

3.1 日志跟踪理解activity生命周期

本节,我们会覆盖一些activity生命周期方法,以此一窥究竟,学习并理解QuizActivity的生命周期。这些覆盖方法会输出日志,告诉我们操作系统何时调用了它们。这样,伴随用户操作,QuizActivity的状态如何变化,就很清楚了。

3.1.1 输出日志信息

Android的android.util.Log类能够向系统级共享日志中心发送日志信息。Log类有好几个日志记录方法。本书用得最多的是以下方法:

public static int d(String tag, String msg)

d代表“debug”,用来表示日志信息的级别。(本章最后一节会详细讲解有关Log级别的内容。)第一个参数是日志的来源,第二个参数是日志的具体内容。

该方法的第一个参数通常以类名为值的TAG常量传入。这样,就很容易看出日志信息的来源。

在QuizActivity.java中,为QuizActivity类新增一个TAG常量,如代码清单3-1所示。

代码清单3-1 新增一个TAG常量(QuizActivity.java)

public class QuizActivity extends AppCompatActivity {

    private static final String TAG = "QuizActivity";
    ...
}

然后,在onCreate(Bundle)方法里调用Log.d(...)方法记录日志,如代码清单3-2所示。

代码清单3-2 为onCreate(Bundle)方法添加日志输出代码(QuizActivity.java)

public class QuizActivity extends AppCompatActivity {
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate(Bundle) called");
        setContentView(R.layout.activity_quiz);
        ...
    }
}

接下来,在QuizActivity类的onCreate(Bundle)之后,覆盖其他五个生命周期方法,如代码清单3-3所示。

代码清单3-3 覆盖更多生命周期方法(QuizActivity.java)

public class QuizActivity extends AppCompatActivity {
    ...
    @Override
    protected void onCreate(Bundle savedinstanceState) {
        ...
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.d(TAG, "onStart() called");
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.d(TAG, "onResume() called");
    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.d(TAG, "onPause() called");
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.d(TAG, "onStop() called");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy() called");
    }
    ...
}

注意,我们先是调用了超类的实现方法,然后才调用具体的日志记录方法。这些超类方法的调用不可或缺。从以上代码可以看出,在回调覆盖实现方法里,超类实现方法总在第一行调用。也就是说,应首先调用超类实现方法,然后再调用其他方法。

知道为什么要使用@Override注解吗?使用@Override注解,就是要求编译器保证当前类拥有你要覆盖的方法。例如,对于如下拼写错误的方法,编译器会发出警告:

public class QuizActivity extends AppCompatActivity {

    @Override
    public void onCreat(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
    }
  ...
}

AppCompatActivity父类没有onCreat(Bundle)方法,因此编译器发出了警告。这样,你就能及时改正拼写错误,而不是等到应用运行时,才发现奇怪的现象,再去查找问题在哪。

3.1.2 使用LogCat

应用运行时,可以使用LogCat工具查看日志。LogCat是Android SDK工具中的一款日志查看器。

运行GeoQuiz应用时,应该能在Android Studio底部看见LogCat,如图3-2所示。如果看不到,请切换至Android Monitor工具窗口模式,并确保已选中logcat选项页。

{%}

图3-2 Android Studio中的LogCat

运行GeoQuiz应用,立刻可看到LogCat窗口中的各类混杂信息。这些日志中,有些是以应用包名为默认名的应用类信息,有些是系统输出信息。

为方便查找,可使用TAG常量过滤日志输出。单击LogCat面板右上角写着Show only selected application的下拉列表。这是过滤项下拉列表。当前选项控制只显示来自应用的日志信息。如果选No Filters,则会看到系统的所有输出信息。

要创建过滤设置,选择Edit Filter Configuration选项。单击绿色+按钮,创建一个消息过滤器。在Filter Name处输入QuizActivity,Log Tag处同样输入QuizActivity,如图3-3所示。

{%}

图3-3 在LogCat中创建过滤器

单击OK按钮。现在,LogCat窗口仅显示Tag为QuizActivity的日志信息,如图3-4所示。

{%}

图3-4 应用启动后,被调用的三个生命周期方法

3.1.3 activity生命周期实例解析

如图3-4所示,GeoQuiz应用启动并创建QuizActivity初始实例后,onCreate(Bundle)onStart()onResume()这三个生命周期方法被调用了。QuizActivity实例现在处于运行状态(在内存里,用户可见,活动在前台)。

(如果看不到过滤后的信息列表,请选择LogCat过滤器下拉列表中的QuizActivity过滤项。)

下面我们来做个有趣的实验。在设备上单击后退键,再查看LogCat。可以看到,日志显示QuizActivityonPause()onStop()onDestroy()方法被调用了,如图3-5所示。QuizActivity实例处于不存在的状态(不在内存里,显然不可见,自然不会活动在前台)。

{%}

图3-5 单击后退键销毁activity

单击设备的后退键,相当于告诉Android系统:“activity已用完,现在不需要它了。”随即,系统就销毁了该activity的视图及其内存里的相关信息。这实际是Android系统节约使用设备有限资源的一种方式。

点击GeoQuiz应用图标,再次运行它。日志显示,Android创建了全新的QuizActivity实例,然后调用onCreate()onStart()onResume()方法。QuizActivity从不存在变为运行状态。

现在,单击主屏幕键,随即,主屏界面出现了,QuizActivity视图不见了。QuizActivity处于啥状态呢?查看LogCat,可以看到系统调用了QuizActivityonPause()onStop()方法,但并没有调用onDestroy()方法,如图3-6所示。

{%}

图3-6 单击主屏幕键停止activity

单击主屏幕键,相当于告诉Android系统:“我去别处看看,稍后可能回来。”此时,Android系统会先暂停,再停止当前activity。这表明,QuizActivity实例已处于停止状态(在内存中,但不可见,不会活动在前台)。这样,Android系统就能快速响应,重启QuizActivity,回到用户离开时的状态。

(需要注意的是,停止的activity能够存在多久,谁也无法保证。系统需要回收内存时,它将首先销毁那些停止的activity。)

现在,我们调出设备的概览屏。如果是比较新的设备,可单击主屏幕键旁的最近应用键,调出概览屏,如图3-7所示。(如果设备没有最近应用键,则长按主屏幕键调出概览屏。)

图3-7 主屏幕键、后退键以及最近应用键

概览屏的每张卡片代表用户之前交互过的一个应用,如图3-8所示。(用户常把概览屏称作最近应用屏或任务管理器。不过,既然Google开发者文档将其称作概览屏,本书从之。)

图3-8 概览屏

在概览屏中,单击GeoQuiz应用,QuizActivity视图随即出现。

LogCat日志显示,系统没有调用onCreate()方法(因为Activity实例还在内存里,自然不用重建了),而是调用了onStart()onResume()方法。用户按了主屏幕键后,QuizActivity最后进入停止状态,再次调出应用时,QuizActivity只需要重新启动(进入暂停状态,用户可见),然后继续运行(进入运行状态,活动在前台)。

activity有时也会一直处于暂停状态(用户完全或部分可见,但不在前台)。可能出现部分可见暂停状态的场景:在一个activity之上启动带透明背景视图或小于屏幕尺寸视图的新activity时。可能出现完全可见暂停状态的场景:应用多窗口模式下(Android 6.0及更高系统版本才支持),当前activity在一个窗口完全可见,而用户在不包含当前activity的另一个窗口操作时。

在本书的后续学习过程中,为完成各种现实任务,我们还会覆盖一些其他生命周期方法,进一步学习更多生命周期方法的用法。

3.2 设备旋转与activity生命周期

现在,可以处理第2章结束时发现的应用缺陷了。启动GeoQuiz应用,单击NEXT按钮显示第二道地理知识问题,然后旋转设备。(模拟器的旋转,使用Command+右方向键/Ctrl+右方向键,或点击工具栏上的旋转按钮。)

设备旋转后,GeoQuiz应用又回到了第一道问题。查看LogCat日志看看发生了什么,如图3-9所示。

{%}

图3-9 QuizActivity已死,QuizActivity万岁

设备旋转时,系统会销毁当前QuizActivity实例,然后创建一个新的QuizActivity实例。再次旋转设备,又一次见证这个销毁与再创建的过程。

这就是问题所在。每次旋转设备,当前QuizActivity实例会完全销毁,实例中的mCurrentIndex当前值会从内存里被抹掉。旋转后,Android重新创建了QuizActivity新实例,mCurrentIndexonCreate(Bundle)方法中被初始化为0。一切重头再来,用户又看到第一道题。

稍后会修正这个缺陷。不要停留在问题表面,接下来,由表及里,深入分析为什么有此问题。

设备配置与备选资源

旋转设备会改变设备配置(device configuration)。设备配置实际是一系列特征组合,用来描述设备当前状态。这些特征有:屏幕方向、屏幕像素密度、屏幕尺寸、键盘类型、底座模式以及语言等。

通常,为匹配不同的设备配置,应用会提供不同的备选资源。为适应不同分辨率的屏幕,向项目添加多套箭头图标就是这样一个使用案例。

设备的屏幕像素密度是个固定的设备配置,无法在运行时发生改变。然而,屏幕方向等特征可以在应用运行时改变。

运行时配置变更(runtime configuration change)发生时,可能会有更合适的资源来匹配新的设备配置。于是,Android销毁当前activity,为新配置寻找最佳资源,然后创建新实例使用这些资源。眼见为实,下面为设备配置变更新建备选资源,只要设备旋转至水平方位,Android就会自动发现并使用它。

创建水平模式布局

在项目工具窗口中,右键单击res目录后选择New → Android resource directory菜单项。创建资源目录界面列出了资源类型及其对应的资源特征,如图3-10所示。从资源类型(Resource type)列表中选择layout,保持Source set的main选项不变。

{%}

图3-10 创建新的资源目录

接下来选中待选资源特征列表中的Orientation,然后单击>>按钮将其移动至已选资源特征(Chosen qualifiers)区域。

最后,确认选中Screen orientation下拉列表中的Landscape选项,并确保目录名(Directory name)显示为layout-land,如图3-11所示。这个窗口看起来有模有样,但实际用途仅限于设置目录名。点击OK按钮让Android Studio创建res/layout-land。

{%}

图3-11 创建res/layout-land

这里的-land后缀名是配置修饰符的另一个使用例子。Android依靠res子目录的配置修饰符定位最佳资源以匹配当前设备配置。访问Android开发网页http://www.wking-china.com/xpjylc/guide/topics/resources/providing-resources.html,可查看Android的配置修饰符列表及其代表的设备配置信息。

设备处于水平方向时,Android会找到并使用res/layout-land目录下的布局资源。其他情况下,它会默认使用res/layout目录下的布局资源。然而,目前在res/layout-land目录下并没有布局资源。让我们解决这个问题。

从res/layout目录复制activity_quiz.xml文件至res/layout-land目录。(如果在Android工具窗口看不到res/layout-land目录,请从Android视图切换到Project视图。完成后,记得切回。如果喜欢,你也可以直接使用文件管理器或终端应用复制粘贴文件。)

现在我们有了一个水平模式布局以及一个默认布局(竖直模式)。注意,两个布局文件的文件名必须相同,这样它们才能以同一个资源ID被引用。

为了与默认的布局文件相区别,还需修改水平模式布局文件。请参照图3-12进行相应修改。

图像说明文字

图3-12 备选的水平模式布局

如图3-12所示,FrameLayout替换了最上层的LinearLayoutFrameLayout是最简单的ViewGroup组件,它一概不管如何安排其子视图的位置。FrameLayout子视图的位置排列取决于它们各自的android:layout_gravity属性。

因而,TextViewLinearLayoutButton都需要一个android:layout_gravity属性。这里,LinearLayout里的Button子元素保持不变。

参照图3-12,打开layout-land/activity_quiz.xml文件修改。完成后可同代码清单3-4做对比检查。

代码清单3-4 水平模式布局修改(layout-land/activity_quiz.xml)

<LinearLayout xmlns:android="http://www.wking-china.com/xpjylc/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical" >

<FrameLayout xmlns:android="http://www.wking-china.com/xpjylc/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TextView
        android:id="@+id/question_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:padding="24dp" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical|center_horizontal"
        android:orientation="horizontal" >
      ...
    </LinearLayout>

    <Button
        android:id="@+id/next_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|right"
        android:text="@string/next_button"
        android:drawableRight="@drawable/arrow_right"
        android:drawablePadding="4dp"
        />

</LinearLayout>
</FrameLayout>

再次运行GeoQuiz应用。旋转设备至水平方位,查看新的布局界面,如图3-13所示。当然,这不仅是一个新的布局界面,也是一个新的QuizActivity

{%}

图3-13 处于水平方位的QuizActivity

设备旋转回竖直方位,可看到默认的布局界面以及另一个新的QuizActivity

3.3 保存数据以应对设备旋转

适时使用备选资源,Android的这个点子不错,但是,设备旋转导致的activity销毁与新建有时也令人头疼。比如,设备旋转后,GeoQuiz应用将回到第一道题。

要修复这个缺陷,旋转后新建的QuizActivity需要知道mCurrentIndex变量的原值。显然,在设备运行中发生配置变更时,若设备旋转,需想个办法保存以前的数据。覆盖以下Activity方法就是一种解决方案:

protected void onSaveInstanceState(Bundle outState)

该方法通常在onStop()方法之前由系统调用,除非用户按后退键。(记住,按后退键就是告诉Android,activity用完了。随后,该activity就完全从内存中被抹掉,自然,也就没有必要为重建保存数据了。)

方法onSaveInstanceState(Bundle)的默认实现要求所有activity视图将自身状态数据保存在Bundle对象中。Bundle是存储字符串键与限定类型值之间映射关系(键-值对)的一种结构。

之前已用过Bundle,如下列代码所示,它作为参数传入onCreate(Bundle)方法:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...
}

覆盖onCreate(Bundle)方法时,我们实际是在调用activity超类的onCreate(Bundle)方法,并传入收到的bundle。在超类代码实现里,通过取出保存的视图状态数据,activity的视图层级结构得以重建。

覆盖onSaveInstanceState(Bundle)方法

可通过覆盖onSaveInstanceState(Bundle)方法,将一些数据保存在bundle中,然后在onCreate(Bundle)方法中取回这些数据。处理设备旋转问题时,将采用这种方式保存mCurrentIndex变量值。

首先,打开QuizActivity.java文件,新增一个常量作为将要存储在bundle中的键-值对的键,如代码清单3-5所示。

代码清单3-5 新增键-值对的键(QuizActivity.java)

public class QuizActivity extends AppCompatActivity {

    private static final String TAG = "QuizActivity";
    private static final String KEY_INDEX = "index";

    private Button mTrueButton;

然后,覆盖onSaveInstanceState(Bundle)方法,以刚才新增的常量值作为键,将mCurrentIndex变量值保存到bundle中,如代码清单3-6所示。

代码清单3-6 覆盖onSaveInstanceState(Bundle)方法(QuizActivity.java)

public class QuizActivity extends AppCompatActivity {
    ...
    @Override
    protected void onPause() {
        ...
    }

    @Override
    protected void onSaveInstanceState(Bundle savedInstanceState) {
        super.onSaveInstanceState(savedInstanceState);
        Log.i(TAG, "onSaveInstanceState");
        savedInstanceState.putInt(KEY_INDEX, mCurrentIndex);
    }

    @Override
    protected void onStop() {
        ...
    }
    ...
}

最后,在onCreate(Bundle)方法中确认是否成功获取该数值。如果获取成功,就将它赋值给变量mCurrentIndex,如代码清单3-7所示。

代码清单3-7 在onCreate(Bundle)方法中检查存储的bundle信息(QuizActivity.java)

public class QuizActivity extends AppCompatActivity {
    ...
    @Override
    protected void onCreate(Bundle savedinstanceState) {
        super.onCreate(savedinstanceState);
        Log.d(TAG, "onCreate(Bundle) called ");
        setContentView(R.layout.activity_quiz);

        if (savedInstanceState != null) {
            mCurrentIndex = savedInstanceState.getInt(KEY_INDEX, 0);
        }
        ...
    }
    ...
}

运行GeoQuiz应用,单击NEXT按钮。现在,无论设备怎么旋转,新创建的QuizActivity都能记住当前正在回答的题目。

注意,在Bundle中存储和恢复的数据类型只能是基本类型(primitive type)以及可以实现SerializableParcelable接口的对象。在Bundle中保存定制类对象不是个好主意,因为你取回的对象可能已经没用了。比较好的做法是,通过其他方式保存定制类对象,而在Bundle中保存标识对象的基本类型数据。

3.4 再探activity生命周期

覆盖onSaveInstanceState(Bundle)方法并不仅仅用于处理与设备旋转相关的问题。用户离开当前activity用户界面,或Android需要回收内存时,activity也会被销毁。(例如,用户按了主屏幕键,然后播放视频或玩起游戏。)

基于用户体验考虑,Android从不会为了回收内存,而去销毁可见的activity(处于暂停或运行状态)。只有在调用过onStop()并执行完成后,activity才会被标为可销毁。

系统随时会销毁掉已停止的activity。不用担心数据丢失,activity停止时,会调用onSaveInstanceState(Bundle)方法的。所以,解决旋转数据丢失问题,就是抢在系统销毁activity之前保存数据。

保存在onSaveInstanceState(Bundle)的数据该如何幸免于难呢?调用该方法时,用户数据随即被保存在Bundle对象中,然后操作系统将Bundle对象放入activity记录中。

为便于理解activity记录,我们增加一个暂存状态(stashed state)到activity生命周期,如图3-14所示。

activity暂存后,Activity对象不再存在,但操作系统会将activity记录对象保存起来。这样,在需要恢复activity时,操作系统可以使用暂存的activity记录重新激活activity。

注意,activity进入暂存状态并不一定需要调用onDestroy()方法。不过,onStop()onSaveInstanceState(Bundle)是两个可靠的方法(除非设备出现重大故障)。因而,常见的做法是,覆盖onSaveInstanceState(Bundle)方法,在Bundle对象中,保存当前activity的小的或暂存状态的数据;覆盖onStop()方法,保存永久性数据,如用户编辑的文字等。onStop()方法调用完,activity随时会被系统销毁,所以用它保存永久性数据。

图3-14 完整的activity生命周期

那么暂存的activity记录到底可以保留多久?前面说过,用户按了后退键后,系统会彻底销毁当前的activity。此时,暂存的activity记录同时被清除。此外,系统重启的话,暂存的activity记录也会被清除。

3.5 深入学习:activity内存清理现状

撰写本书时,低内存状态下,Android直接从内存清除整个应用进程,连带应用的所有activity。目前,Android还做不到只销毁单个activity。(Android应用都有自己的进程。进程的相关知识详见24.7节。)

相比其他进程,有前台(运行状态)或可见(暂停状态)activity的进程的优先级更高。需要释放资源时,Android系统的首选目标是低优先级进程。用户体验至上,理论上,操作系统不会杀掉带有可见activity的进程。当然出现重启或死机这样的大故障就难说了。(若真的出现这种情况,用户也没空深究具体是哪个应用被干掉了。)

覆盖onSaveInstanceState(Bundle)方法时,应测试activity状态是否如预期般正确保存和恢复。旋转设备好测,低内存状况也好测。亲自试试吧。

在设备的应用列表中找到“设置”(Settings)。启动Settings,点击Developer options选项,找到并启用Don't keep activities选项,如图3-15所示。

图3-15 启用Don't keep activities选项

现在运行应用,单击主屏幕键。如前所述,点击主屏幕键会暂停并停止当前的activity。随后就像Android操作系统回收内存那样,停止的activity被系统销毁了。重新运行应用,验证activity状态是否如期得到保存。测试完毕,记得关闭Don't keep activities选项,否则将导致系统和应用出现性能问题。

和单击主屏幕键不一样的是,单击后退键后,无论是否启用Don't keep activities选项,系统总是会销毁当前的activity。单击后退键相当于告诉系统“用户不再需要使用当前的activity”。

3.6 深入学习:日志记录的级别与方法

使用android.util.Log类记录日志,不仅可以控制日志的内容,还可以控制用来区分信息重要程度的日志级别。Android支持如表3-2所示的五种日志级别。每一个级别对应一个Log类方法。要输出什么级别的日志,调用对应的Log类方法就可以了。

表3-2 日志级别与方法

日志级别

方法

说明

ERROR

Log.e(...)

错误

WARNING

Log.w(...)

警告

INFO

Log.i(...)

信息型消息

DEBUG

Log.d(...)

调试输出(可能被过滤掉)

VERBOSE

Log.v(...)

仅用于开发

需要说明的是,所有的日志记录方法都有两种参数签名:string类型的tag参数和msg参数;除tagmsg参数外再加上Throwable实例参数。附加的Throwable实例参数为应用抛出异常时记录异常信息提供了方便。代码清单3-8展示了两种方法不同参数签名的使用实例。对于输出的日志信息,可使用常用的Java字符串连接操作拼接出需要的信息,或者使用String.format对输出日志信息进行格式化操作,以满足个性化的使用要求。

代码清单3-8 Android的各种日志记录方式

// Log a message at "debug" log level
Log.d(TAG, "Current question index: " + mCurrentIndex);

Question question;
try {
    question = mQuestionBank[mCurrentIndex];
} catch (ArrayIndexOutOfBoundsException ex) {
    // Log a message at "error" log level, along with an exception stack trace
    Log.e(TAG, "Index was out of bounds", ex);
}

3.7 挑战练习:禁止一题多答

用户答完某道题,就禁掉那道题对应的按钮,防止用户一题多答。

3.8 挑战练习:评分

用户答完全部题后,显示一个toast消息,给出百分比形式的评分。

目录

  • 版权声明
  • 献词
  • 致谢
  • 如何学习Android开发
  • 开发必备工具
  • 第 1 章 Android开发初体验
  • 第 2 章 Android与MVC设计模式
  • 第 3 章 activity的生命周期
  • 第 4 章 Android应用的调试
  • 第 5 章 第二个activity
  • 第 6 章 Android SDK版本与兼容
  • 第 7 章 UI fragment与fragment管理器
  • 第 8 章 使用RecyclerView显示列表
  • 第 9 章 使用布局与组件创建用户界面
  • 第 10 章 使用fragment argument
  • 第 11 章 使用ViewPager
  • 第 12 章 对话框
  • 第 13 章 工具栏
  • 第 14 章 SQLite数据库
  • 第 15 章 隐式intent
  • 第 16 章 使用intent拍照
  • 第 17 章 双版面主从用户界面
  • 第 18 章 应用本地化
  • 第 19 章 Android辅助功能
  • 第 20 章 数据绑定与MVVM
  • 第 21 章 音频播放与单元测试
  • 第 22 章 样式与主题
  • 第 23 章 XML drawable
  • 第 24 章 深入学习intent和任务
  • 第 25 章 HTTP与后台任务
  • 第 26 章 Looper、Handler和HandlerThread
  • 第 27 章 搜索
  • 第 28 章 后台服务
  • 第 29 章 broadcast intent
  • 第 30 章 网页浏览
  • 第 31 章 定制视图与触摸事件
  • 第 32 章 属性动画
  • 第 33 章 地理位置和Play服务
  • 第 34 章 使用地图
  • 第 35 章 material design
  • 第 36 章 编后语
合作: 和记娱乐 澳门乐8 新葡京娱乐场