本章,是为GeoQuiz应用添加第二个activity。
创建一个新的Activity,取名为CheatActivity,它对应的布局文件名为 activity_cheat,当然还有 activity 需要在 AndroidManifest.xml 注册。
create activity
预览横屏效果
startActivity(Intent)函数,调用请求实际上是发送给了操作系统的ActivityManager。ActivityManager负责创建Activity实例并调用其onCreate(Bundle?)函数,如图所示:
启动activity
intent 对象是 component (activity、service、broadcast receiver、content provider)用来与操作系统通信的一种媒介工具。
// 启动 CheatActivity
mBinding.btnCheat.setOnClickListener {
val intent = Intent(this, CheatActivity::class.java)
startActivity(intent)
}
在启动 activity 前,ActivityManager会确认指定的Class是否已在manifest配置文件中声明。如果已完成声明,则启动activity,应用正常运行。反之,则抛出ActivityNotFoundException异常,应用崩溃。这就是必须在manifest配置文件中声明应用的全部activity的原因。
intent extra:activity间的通信与数据传递
在CheatActivity.kt中,写个伴生对象,拿到Intent,这种写法会比较方便,就是被打开这会告诉打开者是否需要携带参数,参数是什么。
private const val EXTRA_ANSWER_IS_TRUE = "answer_is_true";
class CheatActivity : AppCompatActivity() {
...
companion object {
fun newIntent(packageContext: Context, answerIsTrue: Boolean): Intent {
return Intent(packageContext, CheatActivity::class.java).apply {
putExtra(EXTRA_ANSWER_IS_TRUE, answerIsTrue)
}
}
}
}
这里涉及到了Kotlin伴生对象的概念,参考:https://www.kotlincn.net/docs/reference/object-declarations.html
GeoQuiz应用内部的交互时序图
这里 startActivityForResult 已经被弃用了,当前 google 推荐registerForActivityResult 来替换它。具体详情参考官方文档:
https://developer.android.com/training/basics/intents/result?hl=zh-cn
因此,模仿案例,我的代码作了一点小修改。
MainActivity.kt 中:
class MainActivity : AppCompatActivity() {
···
private val startForResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
quizViewModel.isCheater =
it.data?.getBooleanExtra(EXTRA_ANSWER_SHOW, false) ?: false
}
}
···
override fun onCreate(savedInstanceState: Bundle?) {
···
mBinding.btnCheat.setOnClickListener {
val answer = quizViewModel.currentQuestionAnswer
startForResult.launch(CheatActivity.newIntent(this, answer))
}
}
}
CheatActivity.kt 中:
override fun onCreate(savedInstanceState: Bundle?) {
···
mBinding.btnShowAnswer.setOnClickListener {
val answerText = when {
answer -> R.string.true_button
else -> R.string.false_button
}
mBinding.tvAnswer.setText(answerText)
val data = Intent().apply { putExtra(EXTRA_ANSWER_SHOW, isAnswerShown) }
setResult(Activity.RESULT_OK, data)
}
}
这里代码还涉及到了 kotlin 中 apply 的使用 有关 kotlin 作用域函数语法详情参考:https://www.kotlincn.net/docs/reference/scope-functions.html
本小结要表达的就是,Android 管理任务和返回堆栈的方式是将所有接连启动的 Activity 放到同一任务和一个“后进先出”堆栈中。
然后从桌面点击应用图标启动的第一个activity,是在配置文件中,intent-filter元素节点被指定为launcher activity 的那个activity。
根据此特性,在我们的大多的项目中,都会封装一个统一管理acitivity的工具类,可以随时管理自己已打开的所有的activity,比如:https://www.jianshu.com/p/ed897d567b02
关于任务和返回堆栈详情参考:https://developer.android.com/guide/components/activities/tasks-and-back-stack?hl=zh-cn
既然用户可以通过旋转CheatActivity来清除作弊痕迹,那么要解决此问题,当然就是利用前置知识,在设备旋转或者app被销毁也保存好此作弊痕迹数据就可以啦,其实跟前面也是一样的,用 ViewModel + onSaveInstanceState()的方式就OK。
当前,哪怕用户只在一道题上作弊,应用都会认为他们题题作弊。完善GeoQuiz应用,按题记录用户作弊情况。也就是说,如果用户偷看了某道题的答案,那就在他回答那道题时,弹出作弊警告消息。然后在继续答题过程中,如果用户不再作弊了,就给出答案正确与否的评判。
据我的审题噢,警告 Toast 在示例中就已经做了的,因此这个附加练习题,应该是本就有的功能。
在之前的章节,有个评分的挑战练习,我这里再改改评分逻辑,就是作弊的题目即使答对了,也不算作答对的题目数去计分,也就是作弊答对计0分。
挑战练习都没有贴源码了,解决方案思路在此了。当然练习Demo和练习题都是要做一遍的。
个人实践代码地址:https://github.com/visiongem/AndroidGuideApp/tree/master/GeoQuiz