本篇笔记给QuizDemo新增一个HelpActivity,用户点击Help按钮,会跳转到HelpActivity屏幕,并选择是否查看答案。查看答案之后,返回到答题屏幕,但是如果已经看了答案,这一题的作答就无效了。如果只是点开了HelpActivity屏幕,却没有查看答案,则本题回答依旧有效。当然,不管怎么旋转屏幕,界面状态都会保存。效果如下方动图所示。
1.新建HelpActivity
在quizdemo包处右键,依次选择New->Activity->Empty Activity,启动新建activity向导。如下图所示将新activity命名为HelpActivity。
可以发现多了一个HelpActivity.java文件、一个activity_help.xml文件,并且在AndroidManifest.xml文件中多了一个activity节点。
<activity android:name=".HelpActivity" android:exported="false" />
先在strings.xml中添加所需的字符串,然后设计Help页面的布局。
strings.xml代码清代:
<string name="btn_show_answer">Show Answer</string> <string name="msg_warning">Are you sure you want to do this?</string>
activity_help.xml代码清单:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/msg_warning" android:padding="25dp"/> <TextView android:id="@+id/tx_answer" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="25dp" tools:text="Answer"/> <Button android:id="@+id/btn_show_answer" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/btn_show_answer"/> </LinearLayout> </LinearLayout>
注意第二个TextView中的text属性使用的是tools命名空间中的,而不是android命名空间中的。与android命名空间不同的是,tools命名空间中的text只是起到了占位的作用。别看现在屏幕上显示出了“Answer”字符串,但是运行QuizDemo的时候,就是空白了。
2.从MainActivity启动HelpActivity https://developer.android.google.cn/guide/components/activities/activity-lifecycle?hl=zh-cn#tba
一个activity启动另一个activity最简单的方式是使用startActivity(Intent)方法。activity调用startActivity(Intent)方法时,调用请求实际发给了操作系统的ActivityManager。ActivityManager会先确认指定的新Activity类是否已在AndroidManifest.xml文件中声明。如果已经声明了,则ActivityManager负责创建Intent参数指定的新Activity实例并调用其onCreate(Bundle)方法。如果没有声明,则抛出ActivityNotFoundException 异常,应用崩溃。因此必须在AndroidManifest.xml配置文件中声明应用的全部activity。
Intent是一个消息传递对象,用于Android各组件之间的通信(启动Activity,启动服务,传递广播)。Intent分为显示Intent和隐式Intent:
- 显示Intent,通过提供目标应用的软件包名称或完全限定的组件类名来指定可处理Intent的应用。从MainActivity启动HelpActivity使用的就是显示Intent,本篇笔记只学习显示Intent。
- 隐式Intent,不会指定特定的组件,而是声明要执行的常规操作,从而允许其他应用中的组件来处理。例如,如需使用发送短信或者拍照功能,则可以使用隐式Intent请求相应的应用。因此隐式Intent一般用于不同应用之间传递消息。后面将会学习隐式Intent的使用。不过其实我们早已经使用过隐式Intent了。打开ApplicationManifest.xml文件,看到<intent-filter>标签了吗,”ACTION_MAIN”操作指示这是主要入口点,且不要求输入任何Intent数据。“CATEGORY_LAUNCHER”类别指示此Activity的图标应放入系统的应用启动器。这两个元素必须配对使用,以保证Activity会显示在应用启动器中。
根据官方文档,Intent类有6种构造方法,能满足不同的使用需求。这篇笔记使用第五种构造方法:Intent (Context packageContext, Class<?> cls)。第一个参数指定包上下文(QuizDemo中就是MainActivity),第二个参数指定要启动的Activity(QuizDemo中就是HelpActivity)。
注意到,从MainActivity启动HelpActivity时,应该把当前题目的状态传递给HelpActivity,以便HelpActivity能够显示出答案。可以把当前题目的索引或者直接把答案传递给HelpActivity。也就是说,要在已构造的Intent类的实例中附加上数据(这里选择直接添加问题的答案)。可以使用Intent.putExtra()方法。
Intent.putExtra方法有多种调用方式以便附加不同类型的数据。不变的是,它总是有两个参数,一个参数是固定为String类型的键,一个参数是多种数据类型的键值。下图随便从源代码中截取了一些putExtra()方法。
万事俱备,现在可以写代码启动HelpActivity了。
MainActivity.java代码清单:
public class MainActivity extends AppCompatActivity implements View.OnClickListener{ private static final String TAG="MainActivity"; private static final String ANSWER_KEY="ANSWER_KEY"; private ActivityMainBinding mActivityMainBinding; private QuizViewModel mQuizViewModel; @Override protected void onCreate(Bundle savedInstanceState) { ...
// 给MainActivity的ActionBar添加title ActionBar actionBar=getSupportActionBar(); actionBar.setTitle(TAG); mActivityMainBinding.btnFalse.setOnClickListener(this); mActivityMainBinding.btnTrue.setOnClickListener(this); mActivityMainBinding.btnHelp.setOnClickListener(this); showQuestion(); } @Override public void onClick(View view){ if(view.getId()==R.id.btn_true){ if(mQuizViewModel.isCurrentQuestionAnswer()){ mQuizViewModel.mCount++; } updateQuestion(); }else if(view.getId()==R.id.btn_false){ if(!mQuizViewModel.isCurrentQuestionAnswer()) mQuizViewModel.mCount++; updateQuestion(); }else if(view.getId()==R.id.btn_help){
// 新建Intent实例,指定context和目标class Intent intent=new Intent(MainActivity.this,HelpActivity.class);
// 给intent附加答案数据 intent.putExtra(ANSWER_KEY,mQuizViewModel.isCurrentQuestionAnswer()); startActivity(intent); } } private void updateQuestion(){ mQuizViewModel.moveToNext(); showQuestion(); } ... }
HelpActivity.java代码清单:
public class HelpActivity extends AppCompatActivity{ private static final String TAG="HelpActivity"; private static final String CLICK_KEY="CLICK"; private static final String ANSWER_KEY="ANSWER_KEY"; private static final String RESULT_KEY="RESULT_KEY"; private ActivityHelpBinding mActivityHelpBinding; private boolean mAnswer; private boolean mHasClicked;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.d(TAG,"onCreate() called"); mActivityHelpBinding=ActivityHelpBinding.inflate(getLayoutInflater()); setContentView(mActivityHelpBinding.getRoot()); // 给HelpActivity的ActionBar指定title ActionBar actionBar=getSupportActionBar(); actionBar.setTitle(TAG); mActivityHelpBinding.btnShowAnswer.setOnClickListener(view -> { showAnswer(); }); // 使用key值从intent中获取答案数据 mAnswer=getIntent().getBooleanExtra(ANSWER_KEY,true); if(savedInstanceState!=null){ mHasClicked=savedInstanceState.getBoolean(CLICK_KEY); if(mHasClicked) showAnswer(); } } private void showAnswer(){ mActivityHelpBinding.txAnswer.setText(String.valueOf(mAnswer)); mActivityHelpBinding.btnShowAnswer.setEnabled(false); mHasClicked=true; } @Override protected void onSaveInstanceState(Bundle savedInstanceState){ super.onSaveInstanceState(savedInstanceState); savedInstanceState.putBoolean(CLICK_KEY,mHasClicked); } // 省略了记录生命周期日志的方法 }
Run QuizDemo,点击HELP按钮时,可以跳转到HelpActivity了。
现在来通过观察日志看一下从一个activity启动另一个activity时,生命周期是怎么转换的。如下图所示。启动QuizDemo的时候,MainActivity位于前台,然后点击HELP按钮后,MainActivity失去焦点并进入已暂停状态,onPause()方法被调用。HelpActivity被创建并位于前台。此时MainActivity已经变得不可见了,onStop()方法被调用。但是MainActivity并没有被销毁。用户此时可以与位于前台的HelpActivity交互,直到用户按下返回键,HelpActivity失去焦点并进入已暂停状态。MainActivity的onStart()、onResume()方法则依次被调用,使MainActivity重新位于前台。随后HelpActivity被销毁。
ActivityManager使用Activity栈来完成这些。大多数任务都从设备主屏幕上启动。当用户点击图标时,该应用的任务(任务是一系列Activity的集合)就会转到前台运行。如果该应用没有任务存在(应用最近没有使用过),则会创建一个新任务,并且该应用的主Activity将作为栈的根Activity打开。在当前Activity启动另一个Activity时,新的Activity将被push进栈,并位于栈顶,获得焦点。之前位于栈顶的Activity仍然位于栈中,但会停止,其界面当前状态将被保留。当用户按返回按钮时,当前Activity从栈顶弹出,并被销毁,栈内下一个Activity则位于栈顶并被恢复。栈中的Activity永远不会重新排列,只会被push和pop。整个过程如下图所示。如果用户持续按返回键,则栈中的Activity会逐个弹出,直到用户返回到主屏幕(一般为任务开始时的主Activity)。栈中所有的Activity都弹出后,该任务将不复存在。
任务是一个整体单元,当用户开始一个新任务或通过HOME键返回主屏幕时,任务可移至后台。在后台时,任务中的所有Activity都会停止,但任务的返回堆栈(back stack)保持不变,当其他任务启动时,当前任务只是失去了焦点。值得注意的是,如果任务A和任务B都涉及到一个名为C的Activity,那么在任务A中,C会实例化一次,在任务B中,C也会实例化一次。也就是说,Activity可以多次实例化。甚至在同一个任务中,一个Activity也可以被多次实例化,每次实例化的Activity都会位于栈顶,如下图所示。
一般情况下,Activity多次实例化是没问题的,但是有些情况也许并不希望已经实例化的Activity被再次实例化。这就涉及到了管理任务的内容。
3.管理内容之定义启动模式
在某些情况下,应用中的某个Activity在启动时需要开启一个新的任务,而不是放入当前的任务中,或者启动某个Activity时,需要调用它的一个现有实例,而不是在堆栈顶部创建一个新实例,或者用户离开任务时清楚返回堆栈中除了根Activity以外的所有Activity。这些需求可以通过在ApplicationManifest.xml文件中的<activity>节点添加相应属性实现,或者通过启动Activity时创建的Intent实例中添加标记实现。
如果Activity A启动Activity B, Activity B可以在ApplicationManifest.xml文件中使用launchMode属性定义如何与当前任务相关联。launchMode属性说明了Activity应如何启动到任务中:
- 默认模式 “standard”,系统在启动该Activity的任务中创建Activity的新实例,并将intent传送给该实例。Activity可以多次实例化,每个实例可以属于不同的任务,一个任务可以拥有多个实例。
- 栈顶复用模式 ”singleTop“,如果当前任务的顶部已存在Activity的实例,则系统会通过调用其onNewIntent()方法来将intent转送给该实例,而不是创建Activity的新实例。Activity可以多次实例化,每个实例可以属于不同的任务,一个任务可以拥有多个实例,前提是栈顶的Activity不是该Activity的现有实例。例如,当前堆栈内的Activity自底向上是ABCD的话,收到启动D的intent时,当前这个D的实例会通过onNewIntent()方法接收该intent,堆栈仍为ABCD。但是如果收到启动B的intent,那么即便B的启动模式是singleTop,也依旧会新建B的实例并push到栈顶,当前栈内是ABCDB。
- 栈内复用模式 ”singleTask“,系统会创建新任务,并实例化新任务的根activity。但是如果另外的任务中已存在该activity的实例,则系统会通过调用其onNewIntent()方法将intent转送到该现有实例,而不是创建新实例。Activity一次只能有一个实例存在。例如,当前堆栈内是任务A,包含两个activity,分别是B,C。现在有个启动Activity X的intent,由于Activity X的启动模式设置为singleTask,那么就会新建一个根activity为Activity X的任务B(这也就意味着Activity X需要被设置为ACTION_MAIN和CATEGORY_LAUNCHER),并push进栈,因此任务B中的Activity X位于栈顶。但是如果当前后台已经有一个包含Activity X实例的任务C了,那么任务C就会重新置于栈顶。
- 全局唯一模式 ”singleInstance“,与singleTask相似,唯一不同的是系统不会将任何其他Activity启动到包含该实例的任务中。该Activity始终是其任务唯一成员。由该activity启动的任何activity都会在其他的任务中打开。例如,前面的例子中,如果Activity X的启动模式设置为singleInstance,那么包含Activity X的任务C只能包含这一个activity。
standard和singleTop模式适用于大多数类型的activity。另外两种模式不适用大多数应用。具体可参见 https://developer.android.google.cn/guide/topics/manifest/activity-element?hl=zh-cn。
此外,Activity A也可以在intent中定义Activity B该如何与当前任务关联。并且Activity A定义的优先级更高。Intent中可添加的标记包括:
- FLAG_ACTIVITY_NEW_TASK,相当于singleTask,
- FLAG_ACTIVITY_SINGLE_TOP,相当于singleTop,
- FLAG_ACTIVITY_CLEAR_TOP,如果要启动的Activity已经在当前任务中运行了,则不会启动该activity的新实例,而是会销毁位于它之上的所有其他activity的实例,并通过onNewIntent()将此intent传送给它的已恢复实例。该标记通常与FLAG_ACTIVITY_NEW_TASK结合使用,查找其他任务中的现有activity,并将其置于能够响应intent的位置。
除了定义启动模式外,管理内容还包括处理亲和性、清除返回堆栈,请自行查阅官方文档。
4.从HelpActivity获取返回数据
在第二部分,已经实现了从MainActivity启动HelpActivity,按下返回键之后,HelpActivity被销毁,MainActivity重新位于前台。但是HelpActivity被销毁前,应该把用户是否查看答案告知MainActivity,也就是说MainActivity不仅启动了HelpActivity,还希望从HelpActivity获取结果。根据官网文档,如果希望在Activity完成后收到结果,应该调用startActivityForResult()方法替代startActivity()方法,然后在Activity的onActivityResult()回调中,Activity将结果作为单独的Intent对象接收。但是现在startActivityForResult()方法已经废弃了,如下图所示。虽然还能用,但是已经废弃的方法,我们就不学习它了。根据提示,推荐使用registerForActivityResult()方法。
registerForActivityResult()有两种调用形式,我们用需要两个参数的那个。即
该方法的返回值是ActivityResultLauncher类型,该方法所需的参数是:
- ActivityResultContract类型,指定启动activity的输入类型和输出类型的契约,这是一个抽象类。但是Android提供了一些标准的契约,在ActivityResultForContracts类中,这里我们可以使用ActivityResultContracts.StartActivityForResult,根据描述,这个契约不做任何类型的转换,采用一个给定的Intent对象作为输入,ActivityResult类型作为输出。
- ActivityResultCallback类型,从名字就能看出来了,这是一个类型安全的ActivityResult回调接口。
在此之前,我们先在HelpActivity中实现返回结果。可以调用Activity.setResult(int resultCode, Intent data)方法实现。在HelpActivity.java中添加setActivityResult()方法,该方法新建一个Intent,附加上用户是否查看答案的数据,然后调用Activity.setResult()。用户点击了SHOW ANSWER的按钮,就调用setActivityResult()方法返回结果。
HelpActivity.java代码清单:
public class HelpActivity extends AppCompatActivity{
private static final String RESULT_KEY="RESULT_KEY"; ... @Override protected void onCreate(Bundle savedInstanceState) { ... } private void showAnswer(){ mActivityHelpBinding.txAnswer.setText(String.valueOf(mAnswer)); mActivityHelpBinding.btnShowAnswer.setEnabled(false); mHasClicked=true; setActivityResult(); } ... private void setActivityResult(){ Log.d(TAG,"setActivityResult() called"); Intent intent =new Intent(); intent.putExtra(RESULT_KEY,mHasClicked); setResult(RESULT_OK,intent); } }
MainActivity不仅需要从HelpActivity处接收到用户是否查看答案的值,还需要将这个值的状态保存在ViewModel中。因此先在QuizViewModel.java中添加相关字段。
QuizViewModel.java代码清单:
public class QuizViewModel extends ViewModel { ... private static final String IS_HELP_KEY="is_help"; ...public void setHelpKey(boolean value){ mSavedStateHandle.set(IS_HELP_KEY,value); } public boolean getHelpKey(){ return mSavedStateHandle.get(IS_HELP_KEY)==null? false: mSavedStateHandle.get(IS_HELP_KEY); } ... }
好了,现在可以修改MainActivity.java了。MainActivity.java代码清单:
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
... private static final String RESULT_KEY="RESULT_KEY"; private ActivityResultLauncher mHelpLauncher= registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),result -> { // 实现ActivityResult的回调接口ActivityResultCallback // 当resultCode为RESULT_OK时,从ActivityResult中获取用户是否查看答案的数据,并写入QuizViewModel中 if(result.getResultCode()==Activity.RESULT_OK) mQuizViewModel .setHelpKey(result.getData()==null? false:result.getData().getBooleanExtra(RESULT_KEY,false)); // 从QuizViewModel中获取数据,如果用户在HelpActivity页面查看了数据,则此题无效 if(mQuizViewModel.getHelpKey()) Toast.makeText(this,"This question is invalid",Toast.LENGTH_LONG).show(); }); ... @Override public void onClick(View view){ if(view.getId()==R.id.btn_true){ if(mQuizViewModel.isCurrentQuestionAnswer() && !mQuizViewModel.getHelpKey()){ mQuizViewModel.mCount++; } updateQuestion(); }else if(view.getId()==R.id.btn_false){ if(!mQuizViewModel.isCurrentQuestionAnswer() && !mQuizViewModel.getHelpKey()) mQuizViewModel.mCount++; updateQuestion(); }else if(view.getId()==R.id.btn_help){ Intent intent=new Intent(MainActivity.this,HelpActivity.class); intent.putExtra(ANSWER_KEY,mQuizViewModel.isCurrentQuestionAnswer()); mHelpLauncher.launch(intent); } }
private void updateQuestion(){
mQuizViewModel.moveToNext();
mQuizViewModel.setHelpKey(false);
showQuestion();
}
... }
至此,本篇开头的QuizDemo已经实现了。源代码见:https://gitee.com/larissaLiu/quiz-demo_v4
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/282215.html