本篇笔记给QuizDemo新增一个HelpActivity,用户点击Help按钮,会跳转到HelpActivity屏幕,并选择是否查看答案。查看答案之后,返回到答题屏幕,但是如果已经看了答案,这一题的作答就无效了。如果只是点开了HelpActivity屏幕,却没有查看答案,则本题回答依旧有效。当然,不管怎么旋转屏幕,界面状态都会保存。效果如下方动图所示。
1.新建HelpActivity
2.从MainActivity启动HelpActivity
3.管理任务之定义启动模式
4.从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类有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.管理内容之定义启动模式
https://developer.android.google.cn/guide/components/activities/tasks-and-back-stack?hl=zh-cn#ManagingTasks在某些情况下,应用中的某个Activity在启动时需要开启一个新的任务,而不是放入当前的任务中,或者启动某个Activity时,需要调用它的一个现有实例,而不是在堆栈顶部创建一个新实例,或者用户离开任务时清楚返回堆栈中除了根Activity以外的所有Activity。这些需求可以通过在ApplicationManifest.xml文件中的<activity>节点添加相应属性实现,或者通过启动Activity时创建的Intent实例中添加标记实现。
如果Activity A启动Activity B, Activity B可以在ApplicationManifest.xml文件中使用launchMode属性定义如何与当前任务相关联。launchMode属性说明了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中可添加的标记包括:
除了定义启动模式外,管理内容还包括处理亲和性、清除返回堆栈,请自行查阅官方文档。
4.从HelpActivity获取返回数据
在第二部分,已经实现了从MainActivity启动HelpActivity,按下返回键之后,HelpActivity被销毁,MainActivity重新位于前台。但是HelpActivity被销毁前,应该把用户是否查看答案告知MainActivity,也就是说MainActivity不仅启动了HelpActivity,还希望从HelpActivity获取结果。根据官网文档,如果希望在Activity完成后收到结果,应该调用startActivityForResult()方法替代startActivity()方法,然后在Activity的onActivityResult()回调中,Activity将结果作为单独的Intent对象接收。但是现在startActivityForResult()方法已经废弃了,如下图所示。虽然还能用,但是已经废弃的方法,我们就不学习它了。根据提示,推荐使用registerForActivityResult()方法。
registerForActivityResult()有两种调用形式,我们用需要两个参数的那个。即
该方法的返回值是ActivityResultLauncher类型,该方法所需的参数是:
在此之前,我们先在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