挑战练习
在给出答案解析前,我已经默认了你已经跟着本书敲完了本篇章中的必要代码,以及理解了这些代码的逻辑。
GeoQuiz应用有一些大漏洞,我们的任务就是堵住这些漏洞。从易到难,以下为待解决的三个漏洞。
1. 用户作弊后,可以旋转CheatActivity来清除作弊痕迹。
第一个问题是,为什么旋转CheatActivity会导致作弊痕迹丢失;根据这个问题,我们可以引出一个子问题是,程序原先是如何记录是否作弊的信息并返回给QuizActivity的。
先来解决子问题,由源程序代码可知,在CheatActivity中,一旦用户点击“显示答案”按钮,就会调用setAnswerShownResult(true)方法,将已经作弊的信息预存入返回码中。当CheatActivity弹出栈时,返回码返回到QuizActivity中,由此把作弊信息带回。
搞清楚子问题后,我们就可以来看父问题了。在用户查看答案后,此时预返回的返回码中已经带有作弊信息了,而旋转CheatActivity后CheatActivity会销毁并初始化一个新的CheatActivity实例,自然原先的返回码也不带有作弊信息了。
所以我们要做的是,在旋转CheatActivity时,将这个是否已“作弊”的信息保存下来。其实只需要在“作弊”了后保存下来,如果没“作弊”,不保存也不影响结果。所以我的想法是设立一个标志位来表示用户是否已“作弊”,如果用户点击了“查看答案”按钮,则将该标志设置为true。
接下来公布我写的答案。
在CheatActivity中,添加一个私有boolean变量以及一个静态key常量
private boolean setResultFlag = false;
private static final String KEY_IS_CHEATED = "isCheated";
在onCreate方法中,找到mShowAnswer设置监听事件的代码,修改为如下所示
mShowAnswer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//设置一个标志位,表示作弊过
setResultFlag = true;
if (answer == true){
mAnswerTextView.setText(R.string.true_btn);
}else{
mAnswerTextView.setText(R.string.false_btn);
}
setAnswerShownResult(true);
}
});
在旋转CheatActivity时,我们需要把标志位保存,所以在 onSaveInstanceState中添加如下代码
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if(setResultFlag){
//将标志位放入intent
outState.putBoolean(KEY_IS_CHEATED,setResultFlag);
}
}
旋转后,在重建CheatActivity时,让其检查intent中的标志位,我们就知道之前是否作弊过
if(savedInstanceState != null){
setAnswerShownResult(true);
setResultFlag = true;
}
2. 作弊返回后,用户可以旋转QuizActivity来清除作弊痕迹。
经过我的一番思考后,我认为先解决第三个问题,再回来看第二个问题比较合理。所以如果你按顺序看到了这里,请先跳过直接看第三问。
3. 用户可以不断单击NEXT按钮,跳到偷看过答案的问题,从而使作弊记录丢失。
首先我们要清楚,为什么跳过偷看过答案的问题会使得作弊记录丢失,因为按下"pre"或者“next”按钮后会触发点击事件重置mIsCheater为false,那么作弊记录自然消失了。这不是我们想要的效果,mIsCheater的变化应当与当前问题联系起来,如果当前索引的问题作弊过,则mIsCheater为true;否则为false。我们应当创建一个与当前问题数量相同的boolean数组,以此来记录每个问题是否作弊过;并且当用户翻转Activity时,将这个数组保存起来,下次重建时恢复。
下面我公布我写的答案。
第一步,创建一个boolean数组和对应的key常量
private static final String KEY_ISCHEATS = "is_cheats";
private boolean[] mIsCheats = new boolean[]{false,false,false};
第二步,在返回码返回时,用当前索引的boolean变量来接收它
@Override
protected void onActivityResult(int requestCode, int resultCode,Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != RESULT_OK)
return;
if (requestCode == REQUEST_CODE_CHEAT) {
if (data == null)
return;
mIsCheater = CheatActivity.wasAnswerShown(data);
mIsCheats[mCurrentIndex] = mIsCheater;
}
}
第三步,在onSaveInstanceState方法里将boolean数组保存
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(KEY_INDEX,mCurrentIndex);
outState.putBooleanArray(KEY_ISCHEATS,mIsCheats);
}
第四步,重建QuizActivity(调用onCreate)时恢复boolean数组
if(savedInstanceState != null){
mCurrentIndex = savedInstanceState.getInt(KEY_INDEX);
Log.d(TAG,"index:" + mCurrentIndex);
//mIsCheater = savedInstanceState.getBoolean(KEY_ISCHEATED);
mIsCheats = savedInstanceState.getBooleanArray(KEY_ISCHEATS);
if(mIsCheats[mCurrentIndex] == true)
mIsCheater = true;
}
第五步,修改"pre"和"next"按钮触发事件的逻辑(这里只放出next按钮的修改逻辑,pre同理)
class NextQuestionListener implements View.OnClickListener{
@Override
public void onClick(View v) {
//让index递增,然后获取对应Question实例的问题
mCurrentIndex = (mCurrentIndex + 1) % mQuestions.length;
Log.d(TAG,"index:" + mCurrentIndex);
int textId = mQuestions[mCurrentIndex].getTextResId();
mQuestionText.setText(textId);
//mIsCheater = false;
//mIsCheater由当前问题是否已作过弊决定
if(mIsCheats[mCurrentIndex] == true)
mIsCheater = true;
}
}
至此,第三问解决,同时把第二问也解决了,所以大家可以不用回去看了。
回顾与总结
通过从0开始跟着做GeoQuiz应用,我们还是学到了很多东西的。Activity如何创建,如何书写xml来设计UI,如何给按钮添加点击事件,MVC的分层思想(实际上我觉得这不是纯正的MVC。。),如何启动第二个Activity,Activity间如何传递数据,了解了下Activity的生命周期,还有如何调试安卓应用。基本上就是这些了,这个应用还真就是入门级别的,不过里面有很多小知识点是我之前没注意到的,比如如何解决旋转Activity数据丢失的问题,之前看天哥的教学视频似乎没有讲到这个,所以我认为多看看书还是有用的,书上会指出一些常见的往人踩过的坑以及给出对应的解决方法,这对于今后的项目实践会有很大的帮助。
最后,我会把GeoQuiz的源代码开放在Github上,这里是链接,有需要的童鞋可以参考下我对于挑战练习给出的个人答案,那么本期内容就到这里了,我们下期再见!