小白,请多指教
GeoQuiz应用介绍
该应用是《Android权威编程指南》中的第一个DEMO,大概占了六-七章的篇幅,主要功能是:
- 用户通过点击“True”或“False”按钮回答屏幕上显示的判断题,并根据Toast出的信息检查自己的答案是否正确;
- 通过点击“上一题”或“下一题”切换题目;
- 该应用还提供了作弊功能,当用户点击“Cheat”按钮时,应用会告诉你正确答案,但通过作弊手段得到正确答案的题目将在用户回答该问题时Toast出“你是个作弊者”的信息;
- 该应用还提供了重置功能,当用户点击“RESET”时,所有作弊记录都将被清空。(这个功能书上没有);
本人还修复了该DEMO的若干个bug(这些bug实际上是该书故意留给读者解决的):
- 当用户来到作弊界面以后,可以通过旋转屏幕的方式来清除作弊痕迹;
- 作弊并返回答题界面后,用户可以通过旋转屏幕的方式来清除作弊痕迹;
- 在作弊界面,用户得到题目的结果后,若旋转屏幕,结果丢失;
- 用户可以通过切换题目的方式清除作弊记录;
通过该DEMO能学到的知识点:
- 通过onSaveInstanceState()方法保存Activity界面上的临时数据;
- 横竖屏切换和Activity生命周期的关系;
- UI控件AlertDialog的设计模式;
- layout布局中各控件带“layout”属性和不带“layout”属性的区别
- MVC设计模式;
一、 答题界面的activity和它的布局介绍
1、引用资源
首先,说一下应用中用到的资源:GeoQuiz应用使用了两张图片和一些字符串资源。
图片资源作为切换题目按钮的资源,保存于res/drawable中,如下所示:
字符串资源用来保存题目的内容等,保存于res/values/strings.xml中(在商业应用中,除了需要放在Bundle中的键值对所对应的键和一些静态字符串变量需要在代码中用全大写变量声明外,其他的一些字符串资源应该放到res/values/strings.xml(从服务器的解析数据单说)),如下所示:
<resources>
<string name="app_name">GeoQiuz</string>
<string name="true_button">True</string>
<string name="false_button">False</string>
<string name="cheat_button">Cheat!</string>
<string name="correct_toast">Correct!</string>
<string name="incorrect_toast">Incorrect!</string>
<string name="question_oceans">The Pacific Ocrean is larger than the Atlantic Ocean.</string>
<string name="question_mideast">The Suez Canal connects the Red Sea.</string>
<string name="question_africa">The Source of the Mile River is in Egypt.</string>
<string name="question_americas">The Amazon River is the longest river in the Americas.</string>
<string name="question_asia">Lake Baikal is the world\‘s oldest and deepest freshwater lake.</string>
<string name="desc_prev">click this button to turn to the previous question</string>
<string name="desc_next">click this button to turn to the next question</string>
<string name="warning_text">Are you sure u want to do this</string>
<string name="show_answer_button">Show answer</string>
<string name="cheater_judgement_toast">U are a cheater!</string>
<string name="cheat_reset_button">RESET</string>
</resources>
2、答题界面的布局
接着,我们将为答题界面(由主activity控制)布局做一简单解析(我们先通过所见即所得的Graphic Layout看看布局长啥样):
图2对应的XML被放在res/layout文件夹中,图3对应的XML被放在res/layout-land文件夹中,需要注意的是,这两个XML的名称相同,它们只是不同方向布局的不同呈现,在商业应用中,应该对不同方向的布局分别定制,而不能仅仅是让横竖布局的XML内容完全一样。
以下是这两个XML的代码:
<!--纵向布置的答题界面(对书中的代码做了一些优化)-->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:id="@+id/cheat_reset_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cheat_reset_button" />
<TextView
android:id="@+id/question_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:padding="24dp"
android:text="题目的位置"/>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center" >
<Button
android:id="@+id/true_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#69696969"
android:text="@string/true_button" />
<Button
android:id="@+id/false_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_toRightOf="@id/true_button"
android:background="#69696969"
android:text="@string/false_button" >
</Button>
</RelativeLayout>
<Button
android:id="@+id/cheat_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/cheat_button" />
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="10dp"
android:gravity="center" >
<ImageButton
android:id="@+id/prev_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#69696969"
android:contentDescription="@string/desc_prev"
android:padding="3dp"
android:src="@drawable/image_view_button_share_prev" />
<ImageButton
android:id="@+id/next_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_toRightOf="@id/prev_button"
android:background="#69696969"
android:contentDescription="@string/desc_next"
android:padding="3dp"
android:src="@drawable/image_view_button_share" />
</RelativeLayout>
</LinearLayout>
<!--横向布置的答题界面-->
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/cheat_reset_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cheat_reset_button" />
<TextView
android:id="@+id/question_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:padding="24dp"
android:text="题目的位置" />
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" >
<Button
android:id="@+id/true_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#69696969"
android:text="@string/true_button" />
<Button
android:id="@+id/false_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_toRightOf="@id/true_button"
android:background="#69696969"
android:text="@string/false_button" >
</Button>
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_margin="5dp" >
<ImageButton
android:id="@+id/prev_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:background="#69696969"
android:contentDescription="@string/desc_prev"
android:padding="3dp"
android:src="@drawable/image_view_button_share_prev" />
<Button
android:id="@+id/cheat_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="@string/cheat_button" />
<ImageButton
android:id="@+id/next_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:background="#69696969"
android:contentDescription="@string/desc_next"
android:padding="3dp"
android:src="@drawable/image_view_button_share" />
</RelativeLayout>
</FrameLayout>
在UI控件中,有的属性带“layout”前缀,有的不带,这区别可大了:所有带layout前缀的属性,它都表示该控件相对于它的父控件的位置,而不带layout的属性则表示该控件自身的内容相对于该控件的位置。比方说,layout_gravity这个属性,如果在一个Button中声明了这个属性,并设置为center,则表示该Button位于它的父容器的中心位置;如果为该Button声明了gravity这个属性,则表示它的text中的内容在这个Button控件的中心位置。
顺带再说一句Button这个控件(包含类似的附带图片展示的控件),android提供了一个很好的属性:contentDescription,当背景图片因为某种原因未正常显示时,该图片位置将显示contextDescription属性定义的内容。
3、控制答题界面的activity(主activity)
1、保存题目的TrueFalse类
首先,我们需要一个能保存每一道题目信息的类,该类就是一个简单的DTO对象,包含三个成员变量,分别用于存储题目、答案、用户是否做过弊,代码如下:
public class TrueFalse {
//题目内容,题目保存于strings.xml中,需用R.string.....引用,
//所以是int类型
private int mQuestion;
//题目的答案
private boolean mTrueQuestion;
//用户是否在该题上作弊
private boolean mCheated;
public boolean isCheated() {
return mCheated;
}
public void setCheated(boolean cheated) {
mCheated = cheated;
}
public TrueFalse(int question,boolean trueQuestion,boolean cheated)
{
mQuestion = question;
mTrueQuestion = trueQuestion;
mCheated = cheated;
}
public int getQuestion() {
return mQuestion;
}
public void setQuestion(int question) {
mQuestion = question;
}
public boolean isTrueQuestion() {
return mTrueQuestion;
}
public void setTrueQuestion(boolean trueQuestion) {
mTrueQuestion = trueQuestion;
}
}
2、QuizActivity类(主activity)
public class MainActivity extends Activity {
private Button mTrueButton;
private Button mFalseButton;
// private boolean mTrueQuestion;
private ImageButton mNextButton;
private ImageButton mPrevButton;
private TextView mQuestionTextView;
private Button mCheatButton;
private Button mResetButton;
// private AlertDialog.Builder mBuilder = new AlertDialog.Builder(this);
private int mCurrentIndex = 0;
// 当屏幕旋转时,保存数据
private static final String KEY_INDEX = "Index";
// 将该键值打包进Bundle后放入intent传递
public static final String EXTRA_ANSWER_IS_TRUE = "com.text.geoquiz.answer_is_true";
// private boolean mIsCheater;
// 通过该键可确定user是否查看了答案
private String CHEARTER = "USERISACHEATER";
private TrueFalse[] mQuestionBank = new TrueFalse[] {
new TrueFalse(R.string.question_oceans, true, false),
new TrueFalse(R.string.question_mideast, false, false),
new TrueFalse(R.string.question_americas, true, false),
new TrueFalse(R.string.question_africa, false, false),
new TrueFalse(R.string.question_asia, true, false) };
//更新题目的内容
private void updateQuestion() {
int _question = mQuestionBank[mCurrentIndex].getQuestion();
mQuestionTextView.setText(_question);
}
//判断用户的答案是否正确
private void checkAnswer(boolean userPressedTrue) {
boolean answerTrue = mQuestionBank[mCurrentIndex].isTrueQuestion();
int messageResId = 0;
//如果用户偷窥了答案,那么在作答时,将Toast出“你是个作弊者”的信息
if (mQuestionBank[mCurrentIndex].isCheated()) {
messageResId = R.string.cheater_judgement_toast;
}
//如果用户没有作弊,,那么在作答时,将Toast出作答的正确性
else {
if (answerTrue == userPressedTrue) {
messageResId = R.string.correct_toast;
} else {
messageResId = R.string.incorrect_toast;
}
}
Toast.makeText(this, messageResId, Toast.LENGTH_SHORT).show();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(com.text.geoqiuz.R.layout.activity_main);
//用于接收在activity被销毁之前,保存的临时数据,包含当前答题的题号和用户是否作弊的信息
if (savedInstanceState != null) {
mCurrentIndex = savedInstanceState.getInt(KEY_INDEX, 0);
// mIsCheater = savedInstanceState.getBoolean(CHEARTER);
mQuestionBank[mCurrentIndex].setCheated(savedInstanceState
.getBoolean(CHEARTER));
}
mQuestionTextView = (TextView) findViewById(R.id.question_text_view);
updateQuestion();
mTrueButton = (Button) findViewById(R.id.true_button);
mFalseButton = (Button) findViewById(R.id.false_button);
mNextButton = (ImageButton) findViewById(R.id.next_button);
mPrevButton = (ImageButton) findViewById(R.id.prev_button);
mCheatButton = (Button) findViewById(R.id.cheat_button);
mResetButton = (Button) findViewById(R.id.cheat_reset_button);
mTrueButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
checkAnswer(true);
}
});
mFalseButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
checkAnswer(false);
}
});
mPrevButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
if (mCurrentIndex == 0) {
mCurrentIndex = mQuestionBank.length - 1;
// updateQuestion();
} else {
mCurrentIndex -= 1;
// updateQuestion();
}
// mIsCheater = false;
updateQuestion();
}
});
mNextButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
mCurrentIndex = (mCurrentIndex + 1) % mQuestionBank.length;
// 重置参数
// mIsCheater = false;
updateQuestion();
}
});
//点击Cheat按钮,将以显式intent的方式创建CheatActivity对象,
//同时intent还携带了一个该题得正确答案
mCheatButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
Intent _intent = new Intent(MainActivity.this,
SecondActivity.class);
boolean _answerIsTrue = mQuestionBank[mCurrentIndex]
.isTrueQuestion();
_intent.putExtra(EXTRA_ANSWER_IS_TRUE, _answerIsTrue);
startActivityForResult(_intent, 0);
}
});
//新增一个RESET按钮,该按钮用于清除所有题目的作弊记录,
//弹出一个AlertDialog防止用户操作失误
mResetButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
new AlertDialog.Builder(MainActivity.this)
.setTitle("RESET")
.setIcon(R.drawable.ic_launcher)
.setMessage("Are U Sure To Clean All Cheating Marks?")
.setPositiveButton("Clean",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
// TODO Auto-generated method stub
for (int i = 0; i < mQuestionBank.length; i++) {
mQuestionBank[i].setCheated(false);
}
Toast.makeText(MainActivity.this,
"all cheated marks cleaned!",
Toast.LENGTH_SHORT).show();
}
}).setNegativeButton("Cancel", null).show();
}
});
//通过点击题目也能切换至下一题
mQuestionTextView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
mCurrentIndex = (mCurrentIndex + 1) % mQuestionBank.length;
updateQuestion();
}
});
}
//接收作弊activity传过来的bundle,该bundle携带了用户是否作弊的信息
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// TODO Auto-generated method stub
super.onActivityResult(requestCode, resultCode, data);
if (data == null) {
return;
}
mQuestionBank[mCurrentIndex].setCheated(data.getBooleanExtra(
SecondActivity.EXTRA_ANSWER_IS_SHOWN, false));
}
//旋转屏幕时,系统会销毁该activity对象,在销毁之前保存一些有用的数据
@Override
protected void onSaveInstanceState(Bundle outState) {
// TODO Auto-generated method stub
super.onSaveInstanceState(outState);
outState.putInt(KEY_INDEX, mCurrentIndex);
outState.putBoolean(CHEARTER, mQuestionBank[mCurrentIndex].isCheated());
}
}
先说一下UI控件中的AlertDialog:从它的创建模式跟一般的对象创建模式不太一样——AlertDialog用到了所谓的建造者(Builder)模式。众所周知,对话框是一个可以高度定制的UI控件,我们可以设置它的抬头,背景,标题,子标题,内容,确定和取消的按钮等,若用常规的初始化方法将dialog初始化,那构造函数的参数就得写上好几行,而且有些内容可设可不设,那么就要重载N多个构造方法,所以不妨对dialog的每一部分都设置一个方法,这样就可以有选择的构造每一部分,构造方法也不必是好几行了。
再简单说一下onSaveInstanceState(),这个方法实际上和activity的生命周期有关:众做周知,在一个activity实例被销毁之前,都要回调onPause()、onStop()、onDestory()方法,因为系统一般不会销毁正在onResume的activity,而可能会回收处于暂停或停止状态的activity对象,所以,onSaveInstanceState()方法被回调的时刻有可能是在onPause()被调用之后(也就是onStop()被调用之前),或者onStop()被调用之后;但是还有一个问题,当系统销毁activity后,用onSaveInstanceState()将数据保存在系统中就安全了吗?有时候内存不够用了,或是用户通过back键退出应用一段时间了,这时候系统不仅会销毁activity,还会销毁应用所在进程,这时候数据可能就真的不在了。
至于内存还剩多少不够用,或是说系统如何按照进程的优先级杀死应用,以及退出应用多长时间该进程被销毁,这就是系统的事了。
二、 作弊界面的activity和它的布局介绍
1、布局介绍
至于作弊界面的布局,就简单多了,如下所示:
以下是布局的xml文件:
<!--作弊界面的xml文件-->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="24dp"
android:text="@string/warning_text" />
<TextView
android:id="@+id/answer_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="24dp"
android:text="答案显示位置" />
<Button
android:id="@+id/show_answer_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/show_answer_button" />
</LinearLayout>
2、作弊界面的activity
该activity接收主activity传过来的答案信息,同时通过setResult()的bundle携带“用户是否触发了作弊按钮”信息回传给主activity,以下是代码:
public class SecondActivity extends Activity {
private boolean mAnswerIsTrue;
private Button mShowAnswerButton;
private TextView mAnswerTextView;
private String CHEATER = "CHEATER IS CHEAT";
//用于获得用户是否作弊的信息
private boolean mCheater = false;
private String ANSWERISTRUE = "ANSWERISTRUE";
public static final String EXTRA_ANSWER_IS_SHOWN = "com.text.geoquiz.answer_is_shown";
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
mAnswerIsTrue = getIntent().getBooleanExtra(
MainActivity.EXTRA_ANSWER_IS_TRUE, false);
mShowAnswerButton = (Button) findViewById(R.id.show_answer_button);
mAnswerTextView = (TextView) findViewById(R.id.answer_text_view);
if (savedInstanceState != null) {
mCheater = savedInstanceState.getBoolean(CHEATER);
mAnswerTextView.setText(savedInstanceState
.getCharSequence(ANSWERISTRUE));
}
setAnswerShownResult(mCheater);
mShowAnswerButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
if (mAnswerIsTrue) {
mAnswerTextView.setText(R.string.true_button);
} else {
mAnswerTextView.setText(R.string.false_button);
}
mCheater = true;
setAnswerShownResult(mCheater);
}
});
}
//将用户作弊的情况回传给主activity
private void setAnswerShownResult(boolean isAnswerShown) {
Intent data = new Intent();
data.putExtra(EXTRA_ANSWER_IS_SHOWN, isAnswerShown);
setResult(RESULT_OK, data);
}
//1、保存用户的作弊信息,防止用户通过旋转屏幕清除作弊痕迹
//2、保存用户得到答案的信息,防止用户旋转屏幕而造成信息丢失
@Override
protected void onSaveInstanceState(Bundle outState) {
// TODO Auto-generated method stub
super.onSaveInstanceState(outState);
outState.putBoolean(CHEATER, mCheater);
outState.putCharSequence(ANSWERISTRUE, mAnswerTextView.getText());
}
}