《Android编程权威指南》之第二个activity源码及挑战

文章目录

  • 前言
  • 效果图
  • 依赖
  • Mainactivity
    • Kotlin的“ ?”
      • kotlin的符号
  • QuizViewModel
  • Question类
  • CheatActivity
    • onBackPressed()
    • companion
  • CheatViewModel
  • string.xml

前言

实现禁止一题多答,按题记录作弊状态、偷看次数限制、横竖屏切换依旧保存状态数据
个人思维的项目分析
《Android编程权威指南》之第二个activity源码及挑战_第1张图片

效果图

《Android编程权威指南》之第二个activity源码及挑战_第2张图片
《Android编程权威指南》之第二个activity源码及挑战_第3张图片

依赖

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的“ ?”

在 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.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类

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

data 是一个关键字,用于在类中自动生成一些特殊的方法。当你在类声明中使用 data 关键字时,Kotlin 会自动为这个类生成以下方法:

  1. equals(): 这个方法用于比较两个对象是否相等。在 data 类中,Kotlin 会自动将所有的属性用于 equals() 方法的比较。
  2. hashCode(): 这个方法返回对象的哈希码。在 data 类中,Kotlin 会自动为每个属性计算哈希码,并将其组合以产生最终的哈希码。
  3. toString(): 这个方法返回对象的字符串表示形式。在 data 类中,Kotlin 会自动将所有属性用于 toString() 方法的生成。
  4. 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.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()方法是Android中的一个方法,用于处理用户按下设备上的“返回”按钮时的操作。当用户按下“返回”按钮时,系统会自动调用此方法。onBackPressed()方法通常被覆盖,以提供自定义的返回行为。例如,您可以使用onBackPressed()方法来关闭一个活动或片段,或在退出应用程序之前显示确认对话框。

companion

在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()     // 通过伴生对象访问类的私有成员

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>

你可能感兴趣的:(android)