实现禁止一题多答,按题记录作弊状态、偷看次数限制、横竖屏切换依旧保存状态数据
个人思维的项目分析
android {
....
buildFeatures {
viewBinding true//kotlin数据绑定
}
}
dependencies {
//生命周期
implementation 'androidx.constraintlayout:constraintlayout:1.1.2'
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
}
import android.app.Activity
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityOptionsCompat
import androidx.lifecycle.ViewModelProvider
import com.example.myapplication.databinding.ActivityMainBinding
private const val TAG = "MainActivity"
private const val KEY_INDEX = "index"
const val EXTRA_ANSWER_SHOW = "extra_answer_show"
const val DEFAULT_CAN_CHEAT_NUM: Int = 3
const val KEY_CHEAT_NUM = "cheat_num"
class MainActivity : AppCompatActivity() {
private lateinit var mBinding: ActivityMainBinding
private val quizViewModel by lazy { ViewModelProvider(this)[QuizViewModel::class.java] }
/**
* 当启动一个新的Activity并等待其结果时,如果这个新Activity表明用户可能作弊(通过显示答案),那么就增加用户的作弊次数并更新这个数值
*/
private val startForResult =
//启动一个新Activity并等待其结果的函数。
// ActivityResultContracts.StartActivityForResult()是一个预定义的合约,用于处理StartActivityForResult的请求和结果
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
//检查新Activity的返回结果是否为"OK"的代码。
// "it"是一个包含Activity结果的Bundle对象。
// 如果新Activity成功完成(即没有错误),那么它的返回码将是Activity.RESULT_OK
if (it.resultCode == Activity.RESULT_OK) {
quizViewModel.isCheater =
//尝试从返回的Bundle中获取名为EXTRA_ANSWER_SHOW的布尔值,如果这个值不存在,那么就返回默认值false
it.data?.getBooleanExtra(EXTRA_ANSWER_SHOW, false) ?: false
if (quizViewModel.isCheater) {
// 如果偷看了答案
quizViewModel.cheatNum++
updateCheatNum()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(mBinding.root)
if (savedInstanceState != null) {
quizViewModel.currentIndex = savedInstanceState.getInt(KEY_INDEX, 0)
quizViewModel.cheatNum = savedInstanceState.getInt(KEY_CHEAT_NUM, 0)
}
Log.i(TAG, "onCreate(savedInstanceState: Bundle?) called")
// 回答问题
mBinding.trueButton.setOnClickListener { checkAnswer(true) }
mBinding.falseButton.setOnClickListener { checkAnswer(false) }
// 下个问题
mBinding.nextButton.setOnClickListener {
quizViewModel.moveToNext()
updateQuestion()
}
// 上个问题
mBinding.preButton.setOnClickListener {
quizViewModel.moveToPre()
updateQuestion()
}
// 点文字下个问题
mBinding.questionTextView.setOnClickListener {
quizViewModel.moveToNext()
updateQuestion()
}
// 偷看答案
mBinding.btnCheat.setOnClickListener {
val answer = quizViewModel.currentQuestionAnswer
val option =
//创建一个ClipReveal动画
ActivityOptionsCompat.makeClipRevealAnimation(it, 0, 0, it.width, it.height)
//启动一个新的Activity。startForResult是一个方法,用于启动一个新的Activity并等待其结果
startForResult.launch(CheatActivity.newIntent(this, answer), option)
}
mBinding.tvResult.setOnClickListener {
getScoreResult()
}
// 更新问题
updateQuestion()
//更新查看答案次数
updateCheatNum()
}
/**
* 更新问题
*/
private fun updateQuestion() {
val questionTextResId = quizViewModel.currentQuestionText
mBinding.questionTextView.setText(questionTextResId)
//如果quizViewModel.mQuestionsAnswered不为null,
//并且其第quizViewModel.currentIndex个元素不为null,则将按钮设置为启用状态(即true),否则设置为禁用状态(即false)
setBtnEnabled(!quizViewModel.mQuestionsAnswered?.get(quizViewModel.currentIndex)!!)
}
private fun updateCheatNum() {
var canCheatNum = DEFAULT_CAN_CHEAT_NUM - quizViewModel.cheatNum
mBinding.tvCheatNum.text = "还可以偷看答案 $canCheatNum 次"
if (canCheatNum == 0) {
mBinding.btnCheat.isEnabled = false
}
}
/**
*检测选的答案 里面还需要更新回答正确的题目数,以及已经回答过的题目index
*/
private fun checkAnswer(userAnswer: Boolean) {
// 得到当前题目的答案
val correctAnswer = quizViewModel.currentQuestionAnswer
val messageResId = when {
//如果偷看了答案答题
quizViewModel.isCheater -> R.string.judgment_toast
//没有作弊答题
userAnswer == correctAnswer -> {
// 回答正确的题目数量
quizViewModel.mTrueAnswerCount++
R.string.correct_toast
}
else ->
R.string.incorrect_toast
}
Toast.makeText(this, messageResId, Toast.LENGTH_SHORT).show()
setBtnEnabled(false)
//如果用户已经回答了当前的问题(即在mQuestionsAnswered列表的currentIndex位置已经有值),那么就将这个值设置为true
quizViewModel.mQuestionsAnswered?.set(quizViewModel.currentIndex, true)
// 重置一下是否偷看了答案,此题回答过了,一来不可重复回答,二来解决回答下个问题时此参数还是原来的
quizViewModel.isCheater = false
}
//成绩
private fun getScoreResult() {
//检查用户是否回答了所有的问题,对于每个问题,如果用户没有回答,就将isAllAnswered设置为false,并立即结束循环。
// 如果对所有问题用户都已回答(即isAllAnswered保持为true
var isAllAnswered = true
// for (i in 0 until quizViewModel.questionSize) {
// if (!quizViewModel.mQuestionsAnswered?.get(i)!!) {
// isAllAnswered = false
// return
// }
// }
if (isAllAnswered) {
Toast.makeText(
this,
"${quizViewModel.mTrueAnswerCount * 100 / quizViewModel.questionSize}",
Toast.LENGTH_LONG
).show()
//mBinding.tvResult.text = "评分:${quizViewModel.mTrueAnswerCount * 100 / quizViewModel.questionSize} "
}
}
override fun onStart() {
super.onStart()
Log.i(TAG, "onStart() called")
}
override fun onResume() {
super.onResume()
overridePendingTransition(0, 0);
Log.i(TAG, "onResume() called")
}
override fun onPause() {
super.onPause()
Log.i(TAG, "onPause() called")
}
override fun onStop() {
super.onStop()
Log.i(TAG, "onStop() called")
}
override fun onDestroy() {
super.onDestroy()
Log.i(TAG, "onDestroy() called")
}
//横竖屏切换时调用方法,保存数据,在create中取出
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
Log.i(TAG, "onSaveInstanceState")
savedInstanceState.putInt(KEY_INDEX, quizViewModel.currentIndex)// 当前显示的题目的index
savedInstanceState.putInt(KEY_CHEAT_NUM, quizViewModel.cheatNum)// 偷看答案次数
}
// 禁止一题多答,设置button状态
private fun setBtnEnabled(enabled: Boolean) {
mBinding.trueButton.isEnabled = enabled
mBinding.falseButton.isEnabled = enabled
}
}
在 Kotlin 中,?
是一个可空类型标记符,用于表示某个变量可以为空(null)。
可以使用 ?
标记来声明一个可空类型的变量,例如:
var nullableString: String? = null
在上面的例子中,我们声明了一个类型为 String?
的变量 nullableString
,它可以存储一个字符串或空值(null)。
当我们使用可空类型时,需要注意使用安全调用运算符 ?.
避免空指针异常。
例如,以下代码将仅在 nullableString
不为 null 时打印字符串:
nullableString?.let { println(it) }
还可以使用非空断言运算符 !!
来操作可空类型的变量,但是需要注意,如果该变量为 null,将会触发空指针异常。因此,应该尽量避免使用 !!
。
总之,在 Kotlin 中,使用 ?
标记符可以确保代码的健壮性,避免空指针异常。
在 Kotlin 中,符号 .
用于引用类的成员,例如:
val list = listOf(1, 2, 3)
list.size // 使用 . 符号访问 list 对象的 size 属性
list.get(0) // 等价于 list[0],使用 [] 符号访问 list 对象的成员函数或下标运算符
另外,Kotlin 中还有一些其他的符号:
=
:用于赋值或复制操作,例如:val b = a
。注意,它不同于 Java 中的 ==
相等运算符。?:
: Elvis 运算符,用于当一个变量为 null 时提供一个默认值,例如:val result = nullableVariable ?: defaultValue
!!
:非空断言(Not-null Assertion)运算符,用于断言一个变量不为 null,并强制进行类型转换,例如:val strLength = str!!.length
?
:可空类型标记符,用于表示某个变量可以为空(null),例如:var nullableString: String? = null
::
:引用一个类或函数的引用,例如:val functionRef = :: functionName
表示 functionRef
是一个对 functionName
函数的引用。以上是 Kotlin 中的一些常用符号,当然,还有其他的一些符号,如Lambda表达式中的->
、区间运算符..
等,都是 Kotlin 中的基础语法符号,需要深入了解。
使用ViewModel,可以把所有要显示在用户界面上的数据汇集在一处,统一格式化加工处理供其他对象获取
import android.util.Log
import androidx.lifecycle.ViewModel
import com.pyn.androidguide.Question
private const val TAG = "QuizViewModel"
class QuizViewModel : ViewModel() {
// 当前显示的题目的index
var currentIndex = 0
// 回答正确的题目数量
var mTrueAnswerCount = 0
// 是否偷看了答案
var isCheater = false
// 偷看答案次数,默认0
var cheatNum = 0
// 题目库
private val questionBank = listOf(
Question(R.string.question_australia, true),
Question(R.string.question_oceans, true),
Question(R.string.question_mideast, false),
Question(R.string.question_africa, false),
Question(R.string.question_americas, true),
Question(R.string.question_asia, true)
)
// 已经回答过的问题
var mQuestionsAnswered: BooleanArray? = BooleanArray(questionBank.size)
// 得到当前题目的答案
val currentQuestionAnswer: Boolean get() = questionBank[currentIndex].answer
// 得到当前题目文本
val currentQuestionText: Int get() = questionBank[currentIndex].textResId
// 得到当前总题目数量
val questionSize: Int get() = questionBank.size
// 移动下一个题目
fun moveToNext() {
currentIndex = (currentIndex + 1) % questionBank.size
}
// 上一个题目
fun moveToPre(){
currentIndex = (currentIndex + questionBank.size - 1) % questionBank.size
}
// test
init {
Log.i(TAG, "ViewModel instance created")
}
/**
* On cleared
* onCleared()函数的调用恰好在ViewModel被销毁之前。适合做一些善后清理工作,比如解绑某个数据源。
*/
override fun onCleared() {
super.onCleared()
Log.i(TAG, "ViewModel instance about to destroyed")
}
}
data class Question(@StringRes val textResId: Int, val answer: Boolean)
data
是一个关键字,用于在类中自动生成一些特殊的方法。当你在类声明中使用 data
关键字时,Kotlin 会自动为这个类生成以下方法:
equals()
: 这个方法用于比较两个对象是否相等。在 data 类中,Kotlin 会自动将所有的属性用于 equals() 方法的比较。hashCode()
: 这个方法返回对象的哈希码。在 data 类中,Kotlin 会自动为每个属性计算哈希码,并将其组合以产生最终的哈希码。toString()
: 这个方法返回对象的字符串表示形式。在 data 类中,Kotlin 会自动将所有属性用于 toString()
方法的生成。data
类生成一个 copy()
方法,该方法用于创建一个新对象,其属性值与原始对象相同import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import com.example.myapplication.databinding.ActivityCheatBinding
private const val EXTRA_ANSWER = "extra_answer"
private const val IS_SHOW_ANSWER = "is_show_answer"
class CheatActivity : AppCompatActivity() {
private lateinit var mBinding: ActivityCheatBinding
private var answer = false
private val cheatViewModel by lazy { ViewModelProvider(this)[CheatViewModel::class.java] }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityCheatBinding.inflate(layoutInflater)
setContentView(mBinding.root)
if (savedInstanceState != null) {
cheatViewModel.isShowAnswer = savedInstanceState.getBoolean(IS_SHOW_ANSWER, false)
}
answer = intent.getBooleanExtra(EXTRA_ANSWER, false)
mBinding.btnShowAnswer.setOnClickListener {
cheatViewModel.isShowAnswer = true
val answerText = when {
answer -> R.string.true_button
else -> R.string.false_button
}
mBinding.tvAnswer.setText(answerText)
}
}
// 每次返回的时候,把结果带回去,如果看了答案,则作弊机会-1
override fun onBackPressed() {
setAnswerShowResult(cheatViewModel.isShowAnswer)
super.onBackPressed()
}
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
savedInstanceState.putBoolean(IS_SHOW_ANSWER, cheatViewModel.isShowAnswer)
}
/**
* 给第一个activity返回是否偷看了答案
*/
fun setAnswerShowResult(isAnswerShown: Boolean) {
//创建了一个新的Intent对象,并通过apply函数添加了一个额外的数据
val data = Intent().apply { putExtra(EXTRA_ANSWER_SHOW, isAnswerShown) }
//将上述创建的Intent设置为这个Activity的返回结果。
// Activity.RESULT_OK表示这个Activity的执行结果是成功的,而data则是与这个结果相关的数据
setResult(Activity.RESULT_OK, data)
}
companion object {
fun newIntent(packageContext: Context, answerIsTrue: Boolean): Intent {
//Intent被初始化为以CheatActivity类作为目标(即这个Intent将被用来启动CheatActivity)。
// apply是一个Kotlin函数,它允许在一个对象上执行一系列操作
return Intent(packageContext, CheatActivity::class.java).apply {
//使用putExtra方法将一个名为EXTRA_ANSWER_IS_TRUE的键和对应的值(即answerIsTrue)添加到Intent中
putExtra(IS_SHOW_ANSWER, answerIsTrue)
}
}
}
}
onBackPressed()
方法是Android中的一个方法,用于处理用户按下设备上的“返回”按钮时的操作
。当用户按下“返回”按钮时,系统会自动调用此方法。onBackPressed()方法通常被覆盖,以提供自定义的返回行为。例如,您可以使用onBackPressed()方法来关闭一个活动或片段,或在退出应用程序之前显示确认对话框。
在Kotlin中,每个类都可以包含一个称为伴生对象的对象。关键字“companion
”用于定义伴生对象。伴生对象类似于Java中的静态方法和变量。
伴生对象在类的内部定义,但是它的成员可以直接访问类的私有成员。它们还可以访问其伴生对象的私有成员。
伴生对象的使用如下:
class MyClass {
// 外部无法访问,只能在该类的成员内部访问的属性或方法
private val myPrivateVar = 10
companion object {
// 外部可直接访问该属性
val myPublicVar = 20
// 外部可通过该方法访问该类的私有成员
fun accessPrivateVar() = MyClass().myPrivateVar
}
}
在上面的示例中,我们定义了一个名为“MyClass
”的类和一个伴生对象。在伴生对象中,我们定义了一个名为“myPublicVar
”的公共属性,它可以直接从类外部访问。我们还定义了一个名为“accessPrivateVar
”的公共方法,它可以从类的外部访问该类的私有成员“myPrivateVar
”。
伴生对象与类相关联,因此它们可以像类一样调用,例如:
val myVar = MyClass.myPublicVar // 直接访问伴生对象的公共属性
val myPrivateVar = MyClass.accessPrivateVar() // 通过伴生对象访问类的私有成员
import androidx.lifecycle.ViewModel
class CheatViewModel : ViewModel() {
// 是否偷看了答案
var isShowAnswer = false
}
<resources>
<string name="app_name">MyApplication</string>
<string name="true_button">正确</string>
<string name="false_button">错误</string>
<string name="next_button">下一题</string>
<string name="pre_button">PRE</string>
<string name="correct_toast">答对了</string>
<string name="incorrect_toast">答错了!</string>
<string name="warning_text">你确定吗</string>
<string name="show_answer_button">显示答案</string>
<string name="cheat_button">作弊</string>
<string name="judgment_toast">作弊不对</string>
<string name="question_australia">1、Canberra is the capital of Australia.</string>
<string name="question_oceans">2、The Pacific Ocean is larger than the Atlantic Ocean.</string>
<string name="question_mideast">3、The Suez Canal connects the Red Sea and the Indian Ocean.</string>
<string name="question_africa">4、The source of the Nile River is in Egypt.</string>
<string name="question_americas">5、The Amazon River is the longest river in the Americas.</string>
<string name="question_asia">6、Lake Baikal is the world\'s oldest and deepest freshwater lake.</string>
</resources>