文章目录
- 前言
- 效果图
- 依赖
- Mainactivity
- Kotlin的“ ?”
- kotlin的符号
- QuizViewModel
- Question类
- CheatActivity
- onBackPressed()
- companion
- CheatViewModel
- string.xml
前言
实现禁止一题多答,按题记录作弊状态、偷看次数限制、横竖屏切换依旧保存状态数据
个人思维的项目分析
效果图
依赖
android {
....buildFeatures {viewBinding true//kotlin数据绑定}
}
dependencies {//生命周期implementation 'androidx.constraintlayout:constraintlayout:1.1.2'implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
}
Mainactivity
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.ActivityMainBindingprivate 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: ActivityMainBindingprivate 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_OKif (it.resultCode == Activity.RESULT_OK) {quizViewModel.isCheater =//尝试从返回的Bundle中获取名为EXTRA_ANSWER_SHOW的布尔值,如果这个值不存在,那么就返回默认值falseit.data?.getBooleanExtra(EXTRA_ANSWER_SHOW, false) ?: falseif (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.currentQuestionAnswerval 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.currentQuestionTextmBinding.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.cheatNummBinding.tvCheatNum.text = "还可以偷看答案 $canCheatNum 次"if (canCheatNum == 0) {mBinding.btnCheat.isEnabled = false}}/***检测选的答案 里面还需要更新回答正确的题目数,以及已经回答过的题目index*/private fun checkAnswer(userAnswer: Boolean) {// 得到当前题目的答案val correctAnswer = quizViewModel.currentQuestionAnswerval 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位置已经有值),那么就将这个值设置为truequizViewModel.mQuestionsAnswered?.set(quizViewModel.currentIndex, true)// 重置一下是否偷看了答案,此题回答过了,一来不可重复回答,二来解决回答下个问题时此参数还是原来的quizViewModel.isCheater = false}//成绩private fun getScoreResult() {//检查用户是否回答了所有的问题,对于每个问题,如果用户没有回答,就将isAllAnswered设置为false,并立即结束循环。// 如果对所有问题用户都已回答(即isAllAnswered保持为truevar 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)// 当前显示的题目的indexsavedInstanceState.putInt(KEY_CHEAT_NUM, quizViewModel.cheatNum)// 偷看答案次数}// 禁止一题多答,设置button状态private fun setBtnEnabled(enabled: Boolean) {mBinding.trueButton.isEnabled = enabledmBinding.falseButton.isEnabled = enabled}
}
Kotlin的“ ?”
在 Kotlin 中,?
是一个可空类型标记符,用于表示某个变量可以为空(null)。
可以使用 ?
标记来声明一个可空类型的变量,例如:
var nullableString: String? = null
在上面的例子中,我们声明了一个类型为 String?
的变量 nullableString
,它可以存储一个字符串或空值(null)。
当我们使用可空类型时,需要注意使用安全调用运算符 ?.
避免空指针异常。
例如,以下代码将仅在 nullableString
不为 null 时打印字符串:
nullableString?.let { println(it) }
还可以使用非空断言运算符 !!
来操作可空类型的变量,但是需要注意,如果该变量为 null,将会触发空指针异常。因此,应该尽量避免使用 !!
。
总之,在 Kotlin 中,使用 ?
标记符可以确保代码的健壮性,避免空指针异常。
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 中的基础语法符号,需要深入了解。
QuizViewModel
使用ViewModel,可以把所有要显示在用户界面上的数据汇集在一处,统一格式化加工处理供其他对象获取
import android.util.Log
import androidx.lifecycle.ViewModel
import com.pyn.androidguide.Questionprivate const val TAG = "QuizViewModel"class QuizViewModel : ViewModel() {// 当前显示的题目的indexvar currentIndex = 0// 回答正确的题目数量var mTrueAnswerCount = 0// 是否偷看了答案var isCheater = false// 偷看答案次数,默认0var 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}// testinit {Log.i(TAG, "ViewModel instance created")}/*** On cleared* onCleared()函数的调用恰好在ViewModel被销毁之前。适合做一些善后清理工作,比如解绑某个数据源。*/override fun onCleared() {super.onCleared()Log.i(TAG, "ViewModel instance about to destroyed")}
}
Question类
data class Question(@StringRes val textResId: Int, val answer: Boolean)
data
是一个关键字,用于在类中自动生成一些特殊的方法。当你在类声明中使用 data
关键字时,Kotlin 会自动为这个类生成以下方法:
equals()
: 这个方法用于比较两个对象是否相等。在 data 类中,Kotlin 会自动将所有的属性用于 equals() 方法的比较。hashCode()
: 这个方法返回对象的哈希码。在 data 类中,Kotlin 会自动为每个属性计算哈希码,并将其组合以产生最终的哈希码。toString()
: 这个方法返回对象的字符串表示形式。在 data 类中,Kotlin 会自动将所有属性用于toString()
方法的生成。- Kotlin 还会为
data
类生成一个copy()
方法,该方法用于创建一个新对象,其属性值与原始对象相同
CheatActivity
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.ActivityCheatBindingprivate const val EXTRA_ANSWER = "extra_answer"
private const val IS_SHOW_ANSWER = "is_show_answer"class CheatActivity : AppCompatActivity() {private lateinit var mBinding: ActivityCheatBindingprivate var answer = falseprivate 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 = trueval answerText = when {answer -> R.string.true_buttonelse -> R.string.false_button}mBinding.tvAnswer.setText(answerText)}}// 每次返回的时候,把结果带回去,如果看了答案,则作弊机会-1override 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()
onBackPressed()
方法是Android中的一个方法,用于处理用户按下设备上的“返回”按钮时的操作
。当用户按下“返回”按钮时,系统会自动调用此方法。onBackPressed()方法通常被覆盖,以提供自定义的返回行为。例如,您可以使用onBackPressed()方法来关闭一个活动或片段,或在退出应用程序之前显示确认对话框。
companion
在Kotlin中,每个类都可以包含一个称为伴生对象的对象。关键字“companion
”用于定义伴生对象。伴生对象类似于Java中的静态方法和变量。
伴生对象在类的内部定义,但是它的成员可以直接访问类的私有成员。它们还可以访问其伴生对象的私有成员。
伴生对象的使用如下:
class MyClass {// 外部无法访问,只能在该类的成员内部访问的属性或方法private val myPrivateVar = 10companion object {// 外部可直接访问该属性val myPublicVar = 20// 外部可通过该方法访问该类的私有成员fun accessPrivateVar() = MyClass().myPrivateVar}
}
在上面的示例中,我们定义了一个名为“MyClass
”的类和一个伴生对象。在伴生对象中,我们定义了一个名为“myPublicVar
”的公共属性,它可以直接从类外部访问。我们还定义了一个名为“accessPrivateVar
”的公共方法,它可以从类的外部访问该类的私有成员“myPrivateVar
”。
伴生对象与类相关联,因此它们可以像类一样调用,例如:
val myVar = MyClass.myPublicVar // 直接访问伴生对象的公共属性
val myPrivateVar = MyClass.accessPrivateVar() // 通过伴生对象访问类的私有成员
CheatViewModel
import androidx.lifecycle.ViewModelclass CheatViewModel : ViewModel() {// 是否偷看了答案var isShowAnswer = false
}
string.xml
<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>