请参考教材,全面理解和完成本章节内容... ...
复制第四章的工程ch4,将工程目录改名为ch5。
之前的GeoQuiz应用只有一个activity,它负责管理着主界面。本章,将为应用增加名为CheatActivity的第二个activity,它管理第二个用户界面,可利用此界面查看当前问题的答案,如图5-1所示。
图5-1 CheatActivity提供了偷看答案的机会
如果用户选择先查看答案,然后再返回QuizActivity回答问题,则会收到一条新的信息,如图5-2所示。
图5-2 有没有偷看答案,别想瞒过QuizActivity
5.1 创建第二个 activity
要创建新的activity,接下来要做的事不少。首先创建CheatActivity
所需的布局文件,然后创建CheatActivity
类本身。
不过,现在我们还是先打开strings.xml文件,添加本章需要的所有字符串资源,如代码清单5-1所示。
代码清单5-1 添加字符串资源(strings.xml)
5.1.1 创建新布局
本章开头的屏幕截图(图5-1)展示了CheatActivity
视图的大致样貌。图5-3描述了它的组件定义。
图5-3 CheatActivity的布局图示
为创建布局文件, 在项目导航视图中打开app eslayout目录, 右击layout目录, 选择New->Layout Resource file。(New->XML也行)
图5-4 创建新的布局文件
在接下来弹出的对话框中,输入布局文件名activity_cheat.xml并选择LinerLayout
作为根元素(Root Tag),最后单击Finish按钮完成,如图5-5所示。
图5-5 命名新布局文件
布局向导已经添加了LinerLayout
根元素。接下来只需添加一个android:gravity属性和其他三个子元素即可。
以后,我们将不再列示大段的XML代码,而仅以图5-3的方式给出布局组件图示。最好现在开始习惯参照图5-3创建布局XML文件。完成创建activity_cheat.xml布局文件后,记得对照代码清单5-2进行检查核对。
代码清单5-2 第二个activity的布局组件定义(activity_cheat.xml)
保存布局文件,切换到图形工具模式预览新建布局。虽然没有创建供设备横屏使用的布局文件,不过,借助开发工具,我们可以预览默认布局横屏时的显示效果。
在图形布局工具中,找到预览界面上方工具栏里的一个设备(红框)模样的按钮。单击该按钮切换布局预览方位,如图5-6所示。
图5-6 水平方位预览布局(activity_cheat.xml)
可以看到,默认布局在竖直与水平方位下效果都不错。布局搞定了,接下来我们来创建新的activity子类CheatActivity
类。
5.1.2 创建新的activity子类
在项目导航视图中,展开appjava目录,右键单击com.xq.geoquiz包,选择New → Class菜单项。在随后弹出的对话框中,将类命名为 CheatActivity
, 点击OK按钮完成创建,如图5-7所示。
图5-7 创建CheatActivity类
让CheatActivity 继承AppCompatActivity, 覆盖onCreate()
方法,将定义在activity_cheat.xml文件中的布局资源ID传入setContentView()
方法,如代码清单5-3所示。
代码清单5-3 覆盖onCreate()
方法(CheatActivity.java)
CheatActivity
还有更多任务需要在onCreate()
方法中完成。不过现在我们先进入下一环节,即在应用的manifest配置文件中声明CheatActivity
。
5.1.3 在manifest配置文件中声明activity
manifest配置文件是一个包含元数据的XML文件,用来向Android操作系统描述应用。该文件总是以AndroidManifest.xml命名,可在项目的根目录找到它。通过项目导航视图,在项目的根目录中找到并打开它。
应用的所有activity都必须在manifest配置文件中声明,这样操作系统才能够使用它们。
创建QuizActivity
时,因使用了新建应用向导,向导会自动完成声明工作。而CheatActivity
则需手工完成声明工作。(也可使用向导新建CheatActivity
, 同时自动完成声明工作)
在AndroidManifest.xml配置文件中,完成CheatActivity
的声明,如代码清单5-4所示。
代码清单5-4 在manifest配置文件中声明CheatActivity
(AndroidManifest.xml)
这里的android:name属性是必需的。属性值前面的“.”可告知OS:在manifest配置文件头部包属性值指定的包路径下,可以找到CheatActivity的类文件。
manifest配置文件里还有很多有趣的东西。不过,我们现在还是先集中精力把CheatActivity
配置并运行起来吧。在后续章节中,我们还将学习到更多有关manifest配置文件的知识。
5.1.4 为QuizActivity添加cheat按钮
按照开发设想,用户在QuizActivity
用户界面上点击某个按钮,应用立即产生CheatActivity
实例,并显示其用户界面。因此,我们需要在layout/activity_quiz.xml以及layout-land/activity_quiz.xml布局文件中定义需要的按钮。
在默认的垂直布局中,添加新按钮定义并设置其为根LinearLayout
的直接子类。新按钮应该定义在Next按钮之前,按钮添加方法如代码清单5-5所示。
代码清单5-5 默认布局中添加cheat按钮(layout/activity_quiz.xml)
同样,修改水平布局文件,将新按钮定义在根FrameLayout
的底部居中位置,如代码清单5-6所示。
代码清单5-6 水平布局中添加cheat按钮(layout-land/activity_quiz.xml)
保存修改后的布局文件。然后重新打开QuizActivity.java文件,添加新按钮变量以及资源引用代码。最后再添加View.onClickListener监听器代码存根。启用新按钮的做法如代码清单5-7所示。
代码清单5-7 启用Cheat按钮(QuizActivity.java)
准备工作完成了,下面我们来学习如何启动CheatActivity
。
5.2 启动 activity
一个activity启动另一个activity最简单的方式是使用以下Activity
方法:
public void startActivity(Intent intent)
我们可能“会以为”startActivity()
方法是一个类方法,启动activity就是针对Activity
子类调用该方法。实际并非如此, activity调用startActivity()
方法时,调用请求实际发给了操作系统。
准确地说,该方法调用请求是发送给操作系统的ActivityManager
。ActivityManager
负责创建Activity
实例并调用其onCreate()
方法。activity的启动示意图如图5-8所示。
图5-8 启动activity
ActivityManager
如何知道该启动哪一个Activity
呢? 答案就在于传入startActivity()
方法的Intent
参数。
基于intent的通信
提示:
Intent代表一个应用"想去做什么事",你可以用它做各种各样的任务,不过大部分的时候他们被用来启动另一个Activity。
intent对象是组件用来与操作系统通信的一种媒介工具。目前为止,我们唯一见过的组件(component)就是activity。实际上还有其他一些component,如service、broadcast receiver以及content provider。
Intent是一种多功能通信工具。
Intent
类提供了多个构造方法,以满足不同的使用需求。
在GeoQuiz应用中,我们使用intent告知ActivityManager
该启动哪一个activity,因此可使用以下构造方法(构造一个Intent对象):
public Intent(Context packageContext, Class<?> cls)
传入该方法的Class
对象指定 ActivityManager
应该启动的activity;Context
对象告知ActivityManager
在哪一个包里可以找到Class
对象,关系图如图5-9所示。
图5-9 intent:ActivityManager的信使
在mCheatButton
的监听器代码中,创建包含CheatActivity
类的Intent
实例,然后将其传入startActivity(
Intent
)
方法。代码清单5-8中,QuizActivity.this 代表启动方,之所以用this是因为当前Activity是Context的子类。
代码清单5-8 启动CheatActivity
(QuizActivity.java)
在启动activity前,ActivityManager
会检查确认指定的Class
是否已在配置文件中声明。如已完成声明,则启动activity,应用正常运行。反之,则抛出ActivityNotFoundException
异常。这就是我们必须在manifest配置文件中声明应用全部activity的原因所在。
显式与隐式intent
如果通过指定Context
与Class
对象,然后调用intent的构造方法来创建Intent
,则创建的是显式intent。通常,在同一个应用中,我们使用显式intent来启动activity。
同一个应用里的两个activity间,通信却要借助于应用外部的ActivityManager
,这可能看起来有点啰嗦。不过,这种模式会使不同应用间的activity交互变得容易很多。
一个应用的activity如需启动另一个应用的activity,可通过创建隐式intent来处理。我们会在第21章学习到隐式intent的使用。
运行GeoQuiz应用。单击Cheat按钮,新activity实例的用户界面将显示在屏幕上。单击后退按钮,CheatActivity
实例会被销毁,继而返回到QuizActivity
实例的用户界面中。
5.3 activity 间的数据传递
既然CheatActivity
与QuizActivity
都已经就绪,接下来就可以考虑它们之间的数据传递了。图5-10展示了两个activity间传递的数据信息。
图5-10 QuizActivity与CheatActivity间的对话
CheatActivity
启动后,QuizActivity
会将当前问题的答案通知给它。
用户知道答案后,单击后退键回到QuizActivity
,CheatActivity
随即会被销毁。在被销毁前的瞬间,它会将用户是否作弊的数据传递给QuizActivity
。
接下来,我们首先要学习如何将数据从QuizActivity
传递到CheatActivity
。
5.3.1 使用intent extra
为将当前问题答案通知给CheatActivity
,需将以下语句的返回值传递给它:
mQuestionBank[mCurrentIndex].isTrueQuestion();
该值将作为extra信息,附加在传入startActivity(Intent)
方法的Intent
上发送出去。
extra信息可以是任意数据,它包含在Intent
中,由启动方activity发送出去。接受方activity接收到操作系统转发的intent后,访问并获取包含在其中的extra数据信息, 其关系如图5-11所示。
图5-11 Intent extra:activity间的通信与数据传递
如同 QuizActivity.onSaveInstanceState(Bundle)
方法中用 来保存 mCurrentIndex
值的key-value结构,extra也同样是一种key-value结构。
将extra数据信息添加给intent,我们需要调用Intent.putExtra()
方法, 方法原型如下:
public Intent putExtra(String name, boolean value)
Intent.putExtra()
方法有多种形式。不变的是,它总是有两个参数。一个参数是固定为String
类型的key,另一个参数值可以是多种数据类型。
在CheatActivity.java中,为extra数据信息新增key-value对中的key,如代码清单5-9所示。
代码清单5-9 添加extra常量(CheatActivity.java)
activity可能启动自不同的地方,我们应该为activity获取和使用的extra定义一个key。如代码清单5-9所示,使用包名来修饰extra数据信息,这样可以避免来自不同应用的extra间发生命名冲突。
接下来,再回到QuizActivity
,将extra附加到intent上,如代码清单5-10所示。
代码清单5-10 将extra附加到intent上(QuizActivity.java)
要从extra获取数据,会用到如下方法:
public boolean getBooleanExtra(String name, boolean defaultValue)
第一个参数是extra的名字。getBooleanExtra()
方法的第二个参数是指定默认值(默认答案),它在无法获得有效key值时使用。
在CheatActivity
代码中,编写代码实现从extra中获取信息,然后将信息存入成员变量中,如代码清单5-11所示。
代码清单5-11 获取extra信息(CheatActivity.java)
请注意,Activity.getIntent()
方法返回了由startActivity(Intent)
方法转发的Intent
对象。
最后,在CheatActivity
代码中,编码实现单击“显示答案”按钮后可获取答案并将其显示在TextView
上,如代码清单5-12所示。
代码清单5-12 启用作弊模式(CheatActivity.java)
TextView
相关的代码还是很直观的。可通过使用 TextView.setText(int)
方法来设置TextView
要显示的文字。TextView.setText(int)
方法有多种变体。这里,我们通过传入资源ID来调用该方法。
运行GeoQuiz应用。单击Cheat按钮弹出CheatActivity
的用户界面。然后单击“显示答案”按钮查看当前问题的答案。
5.3.2 从子activity获取返回结果
现在用户可以毫无顾忌地偷看答案了。如果CheatActivity
可以把用户是否偷看过答案的情况通知给QuizAcitivity
就更好了。下面我们来修正这个问题。
若需要从子activity获取返回信息时,可调用以下Activity
方法:
public void startActivityForResult(Intent intent, int requestCode)
该方法的第一个参数同前述的intent。第二个参数是请求代码(是一个用户定义的整数)是先发送给子activity,然后再返回给父activity。当一个activity启动多个不同类型的子activity,且需要判断区分消息回馈方时,我们通常会用到该请求代码。
在QuizActivity
中,修改mCheatButton
的监听器,调用startActivityForResult(Intent, int)
方法,如代码清单5-13所示。
代码清单5-13 调用startActivityForResult()
方法(QuizActivity.java)
QuizActivity
只会启动一个类型的子activity。具体发送信息是什么都无所谓,因此对于需要的请求代码参数,传入0即可。
1. 设置返回结果
实现子activity发送返回信息给父activity,有以下两种方法可供调用:
public final void setResult(int resultCode)
public final void setResult(int resultCode, Intent data)
通常来说,参数result code可以是以下两个预定义常量中的任何一个:
Activity.RESULT_OK
Activity.RESULT_CANCELED
(如需自己定义结果代码,还可使用另一个常量:RESULT_FIRST_USER)
在父activity需要依据子activity的完成结果采取不同操作时,设置结果代码resultCode很有帮助。
例如,假设子activity有一个OK按钮及一个Cancel按钮,并且为每个按钮的单击动作分别设置了不同的结果代码。根据不同的结果代码,父activity会采取不同的操作。
子activity可以不调用setResult()
方法。如不需要区分附加在intent上的结果或其他信息,可让操作系统发送默认的结果代码。
如果子activity是以调用startActivityForResult()
方法启动的,结果代码则总是会返回给父activity。在没有调用setResult()
方法的情况下,如果用户单击了后退按钮,父activity则会收到Activity.RESULT_CANCELED
的结果代码。
2. 返还intent
GeoQuiz应用中,数据信息需要回传给QuizActivity
。因此,我们需要创建一个Intent
,附加上extra信息后,调用Activity.setResult(
int, Intent
)
方法将信息回传给QuizActivity
。
前面,我们已经为CheatActivity
接收的extra定义了常量。CheatActivity
要回传信息给QuizActivity
,我们同样需要为回传的extra做类似的定义。
为什么不在接收信息的父activity中定义extra常量呢?这是因为,传入及传出extra针对CheatActivity
定义了统一的接口。这样,如果在应用的其他地方使用CheatActivity
,我们只需要关注和使用定义在CheatActivity
中的那些常量。
在CheatActivity
代码中,为extra增加常量key,再创建一个私有方法,用来创建intent,附加extra并设置结果值。然后在Show Answer按钮的监听器代码中调用该方法。设置结果值的方法如代码清单5-14所示。
代码清单5-14 设置结果值(CheatActivity.java)
用户单击“显示答案”按钮时,CheatActivity
调用setResult(int, Intent)
方法将结果代码以及intent打包。
然后,在用户单击后退键回到QuizActivity
时,ActivityManager
调用父activity的以下方法:
protected void onActivityResult(int requestCode, int resultCode, Intent data)
该方法的参数来自于QuizActivity
的原始请求代码以及传入SetResult()
方法的结果代码和intent。图5-12展示了应用内部的交互时序。
图5-12 GeoQuiz应用内部的交互时序图
最后覆盖QuizActivity
的onActivityResult(int, int, Intent)
方法来处理返回结果。
3. 处理返回结果
在 QuizActivity.java 中,新增一个成员变量保存 CheatActivity
回传的值。然后覆盖onActivityResult()
方法获取它。onActivityResult()
方法的实现如代码清单5-15所示。
代码清单5-15 onActivityResult()
方法的实现(QuizActivity.java)
观察onActivityResult()
方法的实现代码,我们发现,QuizActivity
并不关心请求代码或结果代码是什么。不过,在其他情况下,某些条件判断编码会使用到这些代码值。
最后,修改QuizActivity
中的checkAnswer(boolean)
方法,确认用户是否偷看答案并给出相应的反应。基于mIsCheater
变量值改变toast消息的做法如代码清单5-16所示。
代码清单5-16 基于mIsCheater
变量值改变toast消息(QuizActivity.java)
运行GeoQuiz应用。偷看下答案,看看会发生什么。
5.4 activity 的使用与管理
来看看当我们在各activity间往返的时候,操作系统层面到底发生了什么。首先,在桌面启动器中点击GeoQuiz应用时,操作系统并没有启动应用,而只是启动了应用中的一个activity。确切地说,它启动了应用的launcher activity。在GeoQuiz应用中,QuizActivity就是它的launcher activity。
使用应用向导创建GeoQuiz应用以及QuizActivity时,QuizActivity默认被设置为launcher activity。
在配置文件中,QuizActivity声明的intent-filter元素节点下,可看到QuizActivity被指定为launcher activity,如代码清单5-17所示。
代码清单5-17 QuizActivity被指定为launcher activity(AndroidManifest.xml)
QuizActivity 实例出现在屏幕上后,用户可单击 Cheat!按钮。CheatActivity 实例在QuizActivity实例上被启动。此时,它们都处于activity栈中,如图5-13所示。
图5-13 GeoQuiz的回退栈
单击后退键,CheatActivity实例被弹出栈外,QuizActivity重新回到栈顶部,如图5-13所示。