The second activity source code and challenges of "The Definitive Guide to Android Programming"

Preface

It prohibits multiple answers to a question, records cheating status by question, limits the number of peeks, and saves status data when switching between horizontal and vertical screens.
Project analysis of personal thinking
Insert image description here

renderings

Please add image description
Please add image description

rely

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.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's "?"

In Kotlin, ?it is a nullable type marker used to indicate that a variable can be null.

You can use ?the tag to declare a variable of a nullable type, for example:

var nullableString: String? = null

In the above example, we declared a String?variable of type nullableStringwhich can store a string or null value.

When we use nullable types, we need to pay attention to using safe call operators ?.to avoid null pointer exceptions.

For example, the following code will only nullableStringprint a string if is not null:

nullableString?.let {
    
     println(it) }

You can also use the non-null assertion operator !!to operate variables of nullable type, but you need to note that if the variable is null, a null pointer exception will be triggered. Therefore, its use should be avoided as much as possible !!.

In short, in Kotlin, using ?tags can ensure the robustness of the code and avoid null pointer exceptions.

kotlin symbols

In Kotlin, the symbol .is used to refer to members of a class, for example:

val list = listOf(1, 2, 3)
list.size // 使用 . 符号访问 list 对象的 size 属性
list.get(0) // 等价于 list[0],使用 [] 符号访问 list 对象的成员函数或下标运算符

In addition, there are some other symbols in Kotlin:

  • =: Used for assignment or copy operations, for example: val b = a. ==Note that it is different from the equality operator in Java .
  • ?:: Elvis operator, used to provide a default value when a variable is null, for example:val result = nullableVariable ?: defaultValue
  • !!: Not-null Assertion operator, used to assert that a variable is not null and force type conversion, for example:val strLength = str!!.length
  • ?: Nullable type marker, used to indicate that a variable can be empty (null), for example:var nullableString: String? = null
  • ::: A reference to a class or function, for example: val functionRef = :: functionNameindicates functionRefthat is a functionNamereference to a function.

The above are some common symbols in Kotlin. Of course, there are other symbols, such as lambda expressions ->, interval operators, ..etc., which are basic syntax symbols in Kotlin and require in-depth understanding.

QuizViewModel

Using ViewModel, you can gather all the data to be displayed on the user interface in one place and format it uniformly for other objects to obtain.

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")
    }
}

Question class

data class Question(@StringRes val textResId: Int, val answer: Boolean)

datais a keyword used to automatically generate some special methods in a class. When you use the keyword in a class declaration data, Kotlin automatically generates the following methods for this class:

  1. equals(): This method is used to compare whether two objects are equal. In the data class, Kotlin automatically uses all properties for comparison by the equals() method.
  2. hashCode(): This method returns the hash code of the object. In the data class, Kotlin automatically calculates a hash code for each attribute and combines them to produce the final hash code.
  3. toString(): This method returns the string representation of the object. In the data class, Kotlin automatically uses all attributes toString()for method generation.
  4. Kotlin also generates a method for datathe class that creates a new object with the same property values ​​as the original objectcopy()

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.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()

onBackPressed()Method is a method in Android, 用于处理用户按下设备上的“返回”按钮时的操作. This method is automatically called when the user presses the Back button. The onBackPressed() method is often overridden to provide custom return behavior. For example, you can use the onBackPressed() method to close an activity or fragment, or to display a confirmation dialog before exiting the application.

companion

In Kotlin, every class can contain an object called a companion object. The keyword " companion" is used to define companion objects. Companion objects are similar to static methods and variables in Java.
The companion object is defined inside the class, but its members can directly access the private members of the class. They can also access private members of their companion objects.
Companion objects are used as follows:

class MyClass {
    
    
    // 外部无法访问,只能在该类的成员内部访问的属性或方法
    private val myPrivateVar = 10

    companion object {
    
    
        // 外部可直接访问该属性
        val myPublicVar = 20

        // 外部可通过该方法访问该类的私有成员
        fun accessPrivateVar() = MyClass().myPrivateVar
    }
}

In the above example, we have defined a MyClassclass named " " and a companion object. In the companion object, we define a myPublicVarpublic property called " ", which can be accessed directly from outside the class. We also define a accessPrivateVarpublic method named " ", which can access the private members of the class " " from outside the class myPrivateVar.

Companion objects are associated with classes, so they can be called like classes, for example:

val myVar = MyClass.myPublicVar       // 直接访问伴生对象的公共属性
val myPrivateVar = MyClass.accessPrivateVar()     // 通过伴生对象访问类的私有成员

CheatViewModel

import androidx.lifecycle.ViewModel

class 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>

Guess you like

Origin blog.csdn.net/weixin_74239923/article/details/134885069