第 2 章 Android与MVC设计模式

第 2 章 Android与MVC设计模式

本章我们将升级GeoQuiz应用,提供更多的地理知识测试题目,如图2-1所示。

图2-1 更多测试题目

为实现目标,需要为GeoQuiz项目新增一个Question类。该类的一个实例代表一道题目。

然后再创建一个Question数组对象交由QuizActivity管理。

2.1 创建新类

在项目工具窗口中,右键单击com.bignerdranch.android.geoquiz类包,选择New → Java Class菜单项。如图2-2所示,类名处填入Question,然后单击OK按钮。

{%}

图2-2 创建Question

在Question.java中,新增两个成员变量和一个构造方法,如代码清单2-1所示。

代码清单2-1 Question类中的新增代码(Question.java)

public class Question {

    private int mTextResId;
    private boolean mAnswerTrue;

    public Question(int textResId, boolean answerTrue) {
        mTextResId = textResId;
        mAnswerTrue = answerTrue;
    }
}

Question类中封装的数据有两部分:问题文本和问题答案(true或false)。

mTextResId为什么是int类型,而不是String类型呢?变量mTextResId用来保存地理知识问题字符串的资源ID。资源ID总是int类型,所以这里设置它为int而不是String类型。需要用到的问题字符串资源稍后会处理。

新增的两个变量需要获取方法与设置方法。为避免手工输入,可设置由Android Studio自动生成。

生成获取方法与设置方法

首先,配置Android Studio识别成员变量的m前缀。

打开Android Studio首选项对话框(Mac用户选择Android Studio菜单,Windows用户选择File → Settings菜单)。分别展开Editor和Code Style选项,在Java选项下选择Code Generation选项页。

在Naming表单中,选择Fields行,添加m作为fields的前缀,如图2-3所示。然后添加s作为Static Fields的前缀。(GeoQuiz项目不会用到s前缀,但之后的项目会用到。)

{%}

图2-3 设置Java代码风格首选项

单击OK按钮完成。

刚才设置的前缀有何作用?那就是,需要Android StudiomTextResId生成获取方法时,它生成的是getTextResId()而不是getMTextResId()方法;而在为mAnswerTrue生成获取方法时,生成的是isAnswerTrue()而不是isMAnswerTrue()方法。

回到Question.java中,右击构造方法后方区域,选择Generate... → Getter And Setter菜单项。选择mTextResIdmAnswerTrue,为每个变量都生成获取方法与设置方法。

单击OK按钮,Android Studio随即生成了获取方法与设置方法共4个方法的代码,如代码清单2-2所示。

代码清单2-2 生成获取方法与设置方法(Question.java)

public class Question {

    private int mTextResId;
    private boolean mAnswerTrue;

    ...

    public int getTextResId() {
        return mTextResId;
    }

    public void setTextResId(int textResId) {
        mTextResId = textResId;
    }

    public boolean isAnswerTrue() {
        return mAnswerTrue;
    }

    public void setAnswerTrue(boolean answerTrue) {
        mAnswerTrue = answerTrue;
    }
}

这样Question类就完成了。稍后,我们会修改QuizActivity类来配合Question类使用。现在,先整体了解一下GeoQuiz应用,看看各个类是如何一起协同工作的。

我们使用QuizActivity创建Question数组对象。继而通过与TextView以及三个Button的交互,在屏幕上显示地理知识问题,并根据用户的回答作出反馈,如图2-4所示。

{%}

图2-4 GeoQuiz应用对象图解

2.2 Android与MVC设计模式

如图2-4所示,应用对象按模型、控制器和视图的类别分为三部分。Android应用基于模型-控制器-视图(Model-View-Controller,MVC)的架构模式进行设计。MVC设计模式表明,应用的任何对象,归根结底都属于模型对象视图对象以及控制对象中的一种。

  • 模型对象存储着应用的数据和业务逻辑。模型类通常用来映射与应用相关的一些事物,如用户、商店里的商品、服务器上的图片或者一段电视节目;又或是GeoQuiz应用里的地理知识问题。模型对象不关心用户界面,它存在的唯一目的就是存储和管理应用数据。

    Android应用里的模型类通常就是我们创建的定制类。应用的全部模型对象组成了模型层

    GeoQuiz的模型层由Question类组成。

  • 视图对象知道如何在屏幕上绘制自己以及如何响应用户的输入,如用户的触摸等。一个简单的经验法则是,凡是能够在屏幕上看见的对象,就是视图对象。

    Android默认自带了很多可配置的视图类。当然,也可以定制开发自己的视图类。应用的全部视图对象组成了视图层

    GeoQuiz应用的视图层是由activity_quiz.xml文件中定义的各类组件构成的。

  • 控制对象含有应用的逻辑单元,是视图与模型对象的联系纽带。控制对象响应视图对象触发的各类事件,此外还管理着模型对象与视图间的数据流动。

    在Android的世界里,控制器通常是ActivityFragmentService的一个子类(第7章和第26章将分别介绍fragment和service的概念)。

    GeoQuiz应用的控制层仅由QuizActivity类组成。

图2-5展示了在响应用户单击按钮等事件时,对象间的交互控制数据流。注意,模型对象与视图对象不直接交互。控制器作为它们之间的联系纽带,接收对象发送的消息,然后向其他对象发送操作指令。

{%}

图2-5 MVC数据控制流与用户交互

使用MVC设计模式的好处

随着应用功能的持续扩展,应用往往会变得过于复杂而让人难以理解。以Java类组织代码有助于从整体视角设计和理解应用。这样,我们就可以按类而不是按变量和方法去思考设计开发问题。

同样,把Java类以模型、视图和控制层进行分类组织,也有助于我们设计和理解应用。这样,我们就可以按层而非一个个类来考虑设计开发了。

尽管GeoQuiz应用不复杂,但以MVC分层模式设计它的好处还是显而易见的。接下来,我们来升级GeoQuiz应用的视图层,为它添加一个NEXT按钮。我们会发现,在添加NEXT按钮的过程中,可以完全不用考虑刚才创建的Question类。

使用MVC模式还可以让类的复用更加容易。相比功能多而全的类,功能单一的专用类更加有利于代码复用。

举例来说,模型类Question与用作显示问题的组件毫无代码逻辑关联。这样,就很容易在应用里按需使用Question类。假设现在想显示所有地理知识问题列表,很简单,直接复用Question对象逐条显示就可以了。

2.3 更新视图层

了解了MVC设计模式后,现在来更新GeoQuiz应用的视图层,为其添加一个NEXT按钮。

在Android编程中,视图层对象通常生成自XML布局文件。GeoQuiz应用唯一的布局定义在activity_quiz.xml文件中。布局定义文件需要更新的地方如图2-6所示。(注意,为节约版面,不作改动的组件属性这里就不再列出了。)

{%}

图2-6 新增的按钮

应用视图层所需的变动操作如下。

  • 删除TextViewandroid:text属性定义。这里不再需要硬编码地理知识问题。

  • TextView新增android:id属性。TextView组件需要资源ID,以便在QuizActivity代码中为它设置要显示的文字。

  • 以根LinearLayout为父组件,新增一个Button组件。

回到activity_quiz.xml文件中,参照代码清单2-3完成XML文件的相应修改。

代码清单2-3 新增按钮以及文本视图的调整(activity_quiz.xml)

<LinearLayout
  ... >

  <TextView
    android:id="@+id/question_text_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="24dp"
    android:text="@string/question_text"
    />

  <LinearLayout
    ... >

    ...

  </LinearLayout>

  <Button
    android:id="@+id/next_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/next_button" />

</LinearLayout>

保存activity_quiz.xml文件。这时,会看到一个错误弹框提示,提醒我们缺少字符串资源。

返回到res/values/strings.xml文件中。删除硬编码的地理知识问题字符串,添加新按钮所需的字符串资源定义,如代码清单2-4所示。

代码清单2-4 更新字符串资源定义(strings.xml)

...

  <string name="app_name">GeoQuiz</string>
  <string name="question_text">Constantinople is the largest city in Turkey.</string>
  <string name="true_button">TRUE</string>
  <string name="false_button">FALSE</string>
  <string name="next_button">NEXT</string>
  <string name="correct_toast">Correct!</string>

  ...

保持strings.xml文件的打开状态,继续添加向用户显示的一系列地理知识问题的字符串,如代码清单2-5所示。

代码清单2-5 新增问题字符串(strings.xml)

...

  <string name="incorrect_toast">Incorrect!</string>
  <string name="question_oceans">The Pacific Ocean is larger than
    the Atlantic Ocean.</string>
  <string name="question_mideast">The Suez Canal connects the Red Sea
    and the Indian Ocean.</string>
  <string name="question_africa">The source of the Nile 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>
  ...

注意最后一行字符串定义中的\'。这里,我们使用了转义字符对符号'进行了处理。在字符串资源定义中,也可使用其他常见的转义字符,比如\n新行符。

保存修改过的文件。然后回到activity_quiz.xml文件中,在图形布局工具里预览确认修改后的布局文件。

至此,GeoQuiz应用视图层的操作就全部完成了。接下来,我们对控制层的QuizActivity类进行代码编写与资源引用,从而最终完成GeoQuiz应用。

2.4 更新控制层

在上一章,应用控制层的QuizActivity类的处理逻辑很简单:显示定义在activity_quiz.xml文件中的布局对象,通过在两个按钮上设置监听器,响应用户点击事件并创建提示消息。

既然现在有了更多的地理知识问题可以检索与展示,QuizActivity类就需要更多的处理逻辑来关联GeoQuiz应用的模型层与视图层。

打开QuizActivity.java文件,添加TextView和新Button变量。另外,再创建一个Question对象数组以及一个该数组的索引变量,如代码清单2-6所示。

代码清单2-6 增加按钮变量及Question对象数组(QuizActivity.java)

public class QuizActivity extends AppCompatActivity {

    private Button mTrueButton;
    private Button mFalseButton;
    private Button mNextButton;
    private TextView mQuestionTextView;

    private Question[] mQuestionBank = new Question[] {
        new Question(R.string.question_oceans, true),
        new Question(R.string.question_mideast, false),
        new Question(R.string.question_africa, false),
        new Question(R.string.question_americas, true),
        new Question(R.string.question_asia, true)
    };

    private int mCurrentIndex = 0;
    ...

这里,我们通过多次调用Question类的构造方法,创建了Question对象数组。

(在更为复杂的项目里,这类数组的创建和存储我们会单独处理。在本书后续应用开发中,会介绍更好的模型数据存储管理方式。现在,简单起见,我们选择在控制层代码中创建数组。)

要把一系列地理知识问题显示在屏幕上,可以使用mQuestionBank数组、mCurrentIndex变量以及Question对象的存取方法。

首先,引用TextView,并将其文本内容设置为当前数组索引所指向的地理知识问题,如代码清单2-7所示。

代码清单2-7 使用TextView(QuizActivity.java)

public class QuizActivity extends AppCompatActivity {

    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_quiz);

        mQuestionTextView = (TextView) findViewById(R.id.question_text_view);
        int question = mQuestionBank[mCurrentIndex].getTextResId();
        mQuestionTextView.setText(question);

        mTrueButton = (Button) findViewById(R.id.true_button);
        ...
    }
}

保存所有文件,确保没有错误发生。然后运行GeoQuiz应用。可看到数组存储的第一个问题显示在TextView上了。

现在我们来处理NEXT按钮。首先引用NEXT按钮,然后为其设置监听器View.OnClickListener。该监听器的作用是:递增数组索引并相应更新显示TextView的文本内容。如代码清单2-8所示。

代码清单2-8 使用新增按钮(QuizActivity.java)

public class QuizActivity extends AppCompatActivity {

    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_quiz);

        mQuestionTextView = (TextView) findViewById(R.id.question_text_view);
        int question = mQuestionBank[mCurrentIndex].getTextResId();
        mQuestionTextView.setText(question);

        ...

        mFalseButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(QuizActivity.this,
                                  R.string.correct_toast,
                                    Toast.LENGTH_SHORT).show();
            }
        });

        mNextButton = (Button) findViewById(R.id.next_button);
        mNextButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mCurrentIndex = (mCurrentIndex + 1) % mQuestionBank.length;
                int question = mQuestionBank[mCurrentIndex].getTextResId();
                mQuestionTextView.setText(question);
            }
        });
    ...
    }
}

可以看到,用来更新mQuestionTextView变量的相同代码分布在两个不同的地方。参照代码清单2-9,花点时间把公共代码放在单独的私有方法里。然后在mNextButton监听器里以及onCreate(Bundle)方法的末尾分别调用该方法,以初步设置activity视图中的文本。

代码清单2-9 使用updateQuestion()封装公共代码(QuizActivity.java)

public class QuizActivity extends AppCompatActivity {

    ...

    private void updateQuestion() {
        int question = mQuestionBank[mCurrentIndex].getTextResId();
        mQuestionTextView.setText(question);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        mQuestionTextView = (TextView) findViewById(R.id.question_text_view);
        int question = mQuestionBank[mCurrentIndex].getTextResId();
        mQuestionTextView.setText(question);
    ...

        mNextButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mCurrentIndex = (mCurrentIndex + 1) % mQuestionBank.length;
                int question = mQuestionBank[mCurrentIndex].getTextResId();
                mQuestionTextView.setText(question);
                updateQuestion();
           }
        });

        updateQuestion();
    ...
    }
}

现在,运行GeoQuiz应用验证新增的NEXT按钮。

一切正常的话,问题应该已经完美显示出来了。当前,GeoQuiz应用认为所有问题的答案都是false,下面着手修正这个逻辑错误。同样,为避免代码重复,我们将解决方案封装在一个私有方法里。

要添加到QuizActivity类的方法如下:

private void checkAnswer(boolean userPressedTrue)

该方法接受boolean类型的变量参数,判别用户单击了TRUE还是FALSE按钮。然后,将用户的答案同当前Question对象中的答案作比较。最后,判断答案正确与否,生成一个Toast消息反馈给用户。

在QuizActivity.java文件中,添加checkAnswer(boolean)方法的实现代码,如代码清单2-10所示。

代码清单2-10 增加方法checkAnswer(boolean)(QuizActivity.java)

public class QuizActivity extends AppCompatActivity {

    ...

    private void updateQuestion() {
        int question = mQuestionBank[mCurrentIndex].getTextResId();
        mQuestionTextView.setText(question);
    }

    private void checkAnswer(boolean userPressedTrue) {
        boolean answerIsTrue = mQuestionBank[mCurrentIndex].isAnswerTrue();

        int messageResId = 0;

        if (userPressedTrue == answerIsTrue) {
            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) {
         ...
    }
}

在按钮的监听器里,调用checkAnswer(boolean)方法,如代码清单2-11所示。

代码清单2-11 调用方法checkAnswer(boolean)(QuizActivity.java)

public class QuizActivity extends AppCompatActivity {

    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        mTrueButton = (Button) findViewById(R.id.true_button);
        mTrueButton.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                Toast.makeText(QuizActivity.this,
                               R.string.incorrect_toast,
                               Toast.LENGTH_SHORT).show();
                checkAnswer(true);
            }
        });

        mFalseButton = (Button) findViewById(R.id.false_button);
        mFalseButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(QuizActivity.this,
                               R.string.correct_toast,
                               Toast.LENGTH_SHORT).show();
                checkAnswer(false);
            }
        });

        mNextButton = (Button) findViewById(R.id.next_button);
        ...
    }
}

GeoQuiz应用已准备就绪,可以在设备上运行了。

2.5 在设备上运行应用

本节,我们学习如何设置系统、设备和应用,实现在硬件设备上运行GeoQuiz应用。

2.5.1 连接设备

首先,将设备连接到系统上。如果是在Mac系统上开发,系统应该会立即识别出所用设备。如果是Windows系统,则可能需要安装adb(Android Debug Bridger)驱动。如果Windows系统自身无法找到adb驱动,请到设备生产商的网站下载。

2.5.2 配置设备用于应用开发

要在设备上测试应用,首先应打开设备的USB调试模式。

  • Android 4.2或之后版本的设备,开发选项默认不可见。先选择“设定 → 关于平板/手机”项,通过点击版本号(Build Number)7次启用它,然后回到“设定”项,选择“开发”项,找到并勾选“USB调试”选项。

  • Android 4.0或4.1版本的设备,选择“设定 → 开发”项,找到并勾选“USB调试”选项。

  • Android 4.0版本以前的设备,选择“设定 → 应用项→ 开发”项,找到并勾选“USB调试”选项。

从以上操作可以看出,不同版本设备的设置有较大差异。如在设置过程中遇到问题,请访问http://developer.android.com/tools/device.html寻求帮助。

可打开Devices视图来确认设备是否得到识别。选择Android Studio底部的Android工具窗口,可快速打开Devices视图。如图2-7所示,你会看到已连接设备的下拉列表。AVD以及硬件设备应该已经列在其中了。

{%}

图2-7 查看已连接设备

如果遇到设备无法识别的问题,请首先确认是否已打开设备和开发者项。如果仍然无法解决,请访问Android开发网站http://developer.android.com/tools/device.html,或访问本书论坛http://forums.bignerdranch.com寻求帮助。

再次运行GeoQuiz应用,Android Studio会询问是在虚拟设备上还是在物理设备上运行应用。选择物理设备并继续。稍等片刻,GeoQuiz应用应该已经在设备上运行了。

如果Android Studio没有提供选择,应用依然在虚拟设备上运行,请按以上步骤重新检查设备设置,并确保设备与系统已正确连接。然后,再检查运行配置是否有问题。要修改运行配置,请选择Android Studio窗口靠近顶部的app下拉列表,如图2-8所示。

{%}

图2-8 打开运行配置

选择Edit Configurations打开运行配置编辑窗口,如图2-9所示。选择窗口左边区域的app,确认已打开Target Device区域的Show chooser dialog选项。点击OK按钮并重新运行该应用,现在你会看到可以运行应用的设备选项。

{%}

图2-9 运行配置界面

2.6 添加图标资源

GeoQuiz应用现在已经可用了。如果NEXT按钮上能够显示向右的图标,用户界面看起来应该会更美。

本书随书文件中提供了这样的箭头图标(https://www.bignerdranch.com/solutions/AndroidProgramming2e.zip)。随书文件集合了Android Studio项目文件,每章对应一个项目文件。

通过以上链接下载文件后,找到并打开02_MVC/GeoQuiz/app/src/main/res目录。在该目录下,可以找到drawable-hdpi、drawable-mdpi、drawable-xhdpi和drawable-xxhdpi四个目录。

四个目录各自的后缀名代表设备的像素密度。

  • mdpi:中等像素密度屏幕(约160dpi)。

  • hdpi:高像素密度屏幕(约240dpi)。

  • xhdpi:超高像素密度屏幕(约320dpi)。

  • xxhdpi:超超高像素密度屏幕(约480dpi)。

(另外还有ldpi和xxxhdpi这两个本书用不到的类别,因此,代码文件省略了他们。)

每个目录下,可看到两个图片文件:arrow_right.png和arrow_left.png。这些图片文件都是按照目录名对应的dpi进行定制的。

GeoQuiz项目中的所有图片资源都会随应用安装在设备里,Android操作系统知道如何为不同设备提供最佳匹配。注意,在为不同设备准备适配图片的同时,应用安装包需要的容量也随之增大。当然,对于GeoQuiz这样的小项目,问题并不明显。

如果应用不包含设备对应的屏幕像素密度文件,在运行时,Android系统会自动找到可用的图片资源,针对该设备进行适配。有了这种特性,就不必准备各种屏幕像素密度文件了。因此,为控制应用包的大小,我们可以只为主流设备准备分辨率较高的定制图片资源。至于那些不常见的低分辨率设备,让Android系统自动适配就好。

(在第21章,我们会介绍为屏幕像素密度定制图片的替代方案。另外,还会解释mipmap目录的用途。)

2.6.1 向项目中添加资源

接下来,需将图片文件添加到GeoQuiz项目资源中去。

首先,确认已准备好了需要的drawable目录。再确认打开了Android Studio的Project视图。展开GeoQuiz/app/src/main/res目录,找到匹配各类像素密度的子目录,如图2-10所示。(你还有可能看到其他的子目录,目前暂时忽略。)

{%}

图2-10 确认准备好了各类drawable目录

如果要用到的drawable目录不存在,首先需要创建它们。右键单击res目录,选择New → Directory,会弹出如图2-11所示的对话框。输入需要的目录名,如drawable-mdpi,然后点击OK按钮确认。

{%}

图2-11 创建drawable目录

成功创建drawable-mdpi目录后,就应该能在Project视图中看到它。如果看不到,很可能你当前打开的还是Android视图,请自行切换至Project视图。

如有需要,重复上述步骤,完成创建drawable-hdpi、drawable-xhdpi和drawable-xxhdpi目录。

然后将已下载文件目录中对应的图片文件(arrow_left.png和arrow_right.png)复制到项目的对应drawable目录中。

完成复制后,在Android Studio的项目工具窗口就可以看到新的arrow_left.png和arrow_right.png文件了,如图2-12所示。

{%}

图2-12 drawable目录中的箭头图标

如果将项目工具窗口切换回Android视图,新增加的drawable图片资源会以图2-13所示的形式展示。

{%}

图2-13 drawable目录中的箭头图标汇总

向应用里添加图片就这么简单。任何添加到res/drawable目录中,后缀名为.png、.jpg或者.gif的文件都会自动获得资源ID。(注意,文件名必须是小写字母且不能有任何空格符号。)

这些资源ID并不按照屏幕密度匹配。因此不需要在运行的时候确定设备的屏幕像素密度,只需在代码中引用这些资源ID就可以了。应用运行时,操作系统知道如何在特定的设备上显示匹配的图片。

Android资源系统是如何运作的?从第3章起,我们会深入学习这方面的相关知识。而现在,NEXT按钮上能够显示右箭头图标就可以了。

2.6.2 在XML文件中引用资源

在代码中,可以使用资源ID引用资源。但如果想在布局定义中配置NEXT按钮显示箭头图标的话,又要如何在布局XML文件中引用资源呢?

很简单,只是语法稍有不同而已。打开activity_quiz.xml文件,为Button组件新增两个属性,如代码清单2-12所示。

代码清单2-12 为NEXT按钮增加图标(activity_quiz.xml)

<LinearLayout
  ... >

  ...

  <LinearLayout
    ... >
    ...

  </LinearLayout>

  <Button
    android:id="@+id/next_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/next_button"
    android:drawableRight="@drawable/arrow_right"
    android:drawablePadding="4dp"
    />

</LinearLayout>

在XML资源文件中,通过资源类型和资源名称,可引用其他资源。以@string/开头的定义是引用字符串资源。以@drawable/开头的定义是引用drawable资源。

从第3章起,我们会学到更多资源命名以及res目录结构中其他资源的使用等相关知识。

运行GeoQuiz应用。新按钮很漂亮吧?测试一下,确认它仍然工作正常。

别高兴太早,GeoQuiz应用有个bug。应用运行时,单击NEXT按钮显示下一道题,然后旋转设备。(如果是在模拟器上运行的应用,请按组合键Fn+Control+F12/Ctrl+F12实现旋转。)

我们发现,设备旋转后应用又显示了第一道测试题。怎么回事?如何修正?

要解决此类问题,需了解activity生命周期的概念。第3章会进行专题介绍。

2.7 关于挑战练习

本书大部分章末尾都安排有挑战练习,需要你独立完成。有些很简单,就是练习所学知识。有些难度较大,需要较强的问题解决能力。

希望你一定完成这些练习。攻克它们不仅可以巩固所学,树立信心,还可以让自己从被动学习快速成长为自主开发的Android程序员。

在解答挑战练习的过程中,若一时陷入困境,可休息休息,理理头绪,重新再来。如果仍然无法解决,可访问本书论坛http://forums.bignerdranch.com,参考其他读者发布的解决方案。当然你也可以自己发布问题和答案与其他读者一起交流学习。

为保持当前工作项目的完整性,建议你在Android Studio中先复制当前项目,然后在复制的项目上进行练习。

在你的电脑上,通过文件浏览器找到项目文件的根目录,复制一份GeoQuiz文件并重命名为GeoQuiz Challenge。回到Android Studio中,选择File → Import Project....菜单项,通过导入功能找到GeoQuiz Challenge并导入。这样,复制项目就在新窗口中打开了。开始挑战吧!

2.8 挑战练习:为TextView添加监听器

NEXT按钮不错,但如果用户单击应用的TextView文字区域(地理知识问题),也可以跳转到下一道题,用户体验应该会更好。你来试一试。

提示 TextView也是View的子类,因此和Button一样,可为TextView设置View.OnClickListener监听器。

2.9 挑战练习:添加后退按钮

为GeoQuiz应用新增后退按钮,用户单击时,可以显示上一道测试题目。完成后的用户界面应如图2-14所示。

图2-14 添加了后退按钮的用户界面

这是个很棒的练习,需回顾前两章的内容才能完成。

2.10 挑战练习:从按钮到图标按钮

如果能实现前进与后退按钮上只显示指示图标,用户界面看起来可能更加简洁美观。只显示图标按钮的用户界面如图2-15所示。

图2-15 只显示图标的按钮

完成此练习,需将普通Button组件替换成ImageButton组件。

ImageButton组件继承ImageViewButton组件则继承TextviewImageButtonButtonView间的继承关系如图2-16所示。

图2-16 ImageButtonButton与View间的继承关系

如以下代码所示,将Button组件替换成ImageButton组件,删除NEXT按钮的text以及drawable属性定义,并添加ImageView属性:

<Button ImageButton
  android:id="@+id/next_button"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="@string/next_button"
  android:drawableRight="@drawable/arrow_right"
  android:drawablePadding="4dp"
  android:src="@drawable/arrow_right"
  />

当然,为了使用ImageButton,记得调整QuizActivity类代码。

将按钮组件替换成ImageButton后,Android Studio会警告说找不到android:content Description属性定义。该属性为视力障碍用户提供方便。在为其设置文字属性值后,如果用户设备的可访问性选项作了相应设置,那么在用户点击图形按钮时,设备便会读出属性值的内容。

最后,为每个ImageButton都添加上android:contentDescription属性定义。

目录

  • 版权声明
  • 献词
  • 致谢
  • 如何学习Android开发
  • 开发必备工具
  • 第 1 章 Android开发初体验
  • 第 2 章 Android与MVC设计模式
  • 第 3 章 Activity的生命周期
  • 第 4 章 Android应用的调试
  • 第 5 章 第二个activity
  • 第 6 章 Android SDK版本与兼容
  • 第 7 章 UI fragment与fragment管理器
  • 第 8 章 使用布局与组件创建用户界面
  • 第 9 章 使用RecyclerView显示列表
  • 第 10 章 使用fragment argument
  • 第 11 章 使用ViewPager
  • 第 12 章 对话框
  • 第 13 章 工具栏
  • 第 14 章 SQLite数据库
  • 第 15 章 隐式intent
  • 第 16 章 使用intent拍照
  • 第 17 章 Master-Detail用户界面
  • 第 18 章 Assets
  • 第 19 章 使用SoundPool播放音频
  • 第 20 章 样式与主题
  • 第 21 章 XML drawable
  • 第 22 章 深入学习intent和任务
  • 第 23 章 HTTP与后台任务
  • 第 24 章 Looper、Handler和HandlerThread
  • 第 25 章 搜索
  • 第 26 章 后台服务
  • 第 27 章 broadcast intent
  • 第 28 章 网页浏览
  • 第 29 章 定制视图与触摸事件
  • 第 30 章 属性动画
  • 第 31 章 地理位置和Play服务
  • 第 32 章 使用地图
  • 第 33 章 material design
  • 第 34 章 编后语