1、简介
注:本文代码参考
我们将开发本系列第一个应用,并借此学习一些Android基本概念以及构成应用的UI组件。
初学习,如果没能全部理解,也不必担心,后续还会涉及这些内容并有更加详细的讲解。
马上要开发的应用名叫GeoQuiz,它能给出一道道地理知识问题。用户点击TRUE或FALSE
按钮来回答屏幕上的问题,GeoQuiz即时作出反馈。
下图显示了用户点击TRUE按钮的结果:
用户点击TRUE按钮图,图1
1.1、Activity和布局文件
GeoQuiz应用由一个activity和一个布局(layout)组成。
- activity是Android SDK中 Activity类的一个实例,负责管理用户与应用界面的交互。
- 应用的功能是通过编写 Activity 子类来实现的。对于简单的应用来说,一个 Activity 子
类可能就够了,而复杂的应用则会有多个。 GeoQuiz是个简单应用,因此它只有一个名叫 QuizActivity 的 Activity
子类。 QuizActivity它所对应的 用户界面。 - 布局定义了一系列用户界面对象以及它们显示在屏幕上的位置。组成布局的定义保存在
XML文件中。每个定义用来创建屏幕上的一个对象,如按钮或文本信息。
GeoQuiz应用包含一个名叫activity_quiz.xml的布局文件。该布局文件中的XML标签定义了
具体的用户界面。
QuizActivity 与activity_quiz.xml文件的关系如下图2所示:
Activity java文件与布局xml文件对应关系图:
1.2、用户界面设计
Android Studio如何创建项目以及Android Studio的界面介绍这里不赘述,我们通过Android Studio(以后简称“AS”)创建好GeoQuiz应用后,首先打开app/res/layout/activity_quiz.xml文件。如果看到的是布局预览界面,请点击底部的
Text页切换显示XML代码。
当前,activity_quiz.xml文件定义了默认的activity布局。
应用activity的默认布局定义了两个组件(widget): RelativeLayout 和 TextView 。
组件是用户界面的构造模块。组件可以显示文字或图像,与用户交互,甚至布置屏幕上的其
他组件。按钮、文本输入控件和选择框等都是组件。
Android SDK内置了多种组件,通过配置各种组件可获得所需的用户界面及行为。每一个组
件都是 View 类或其子类(如 TextView 或 Button )的一个具体实例。
RelativeLayout 和 TextView 是在屏幕上的显示图 图3:
QuizActivity 的用户界面需要下列组件:
- 一个垂直 LinearLayout 组件;
- 一个 TextView 组件;
- 一个水平 LinearLayout 组件;
- 两个 Button组件。
组件是如何构成 QuizActivity 用户界面图 图4:
下面我们在activity_quiz.xml文件中定义这些组件。
<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/question_text" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/true_button" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/false_button" />
</LinearLayout>
</LinearLayout>
上述代码暂时不理解也没关系,后续学习中逐渐弄明白的。
注意,开发工具无法校验布局XML内容,拼写错误早晚会出问题,应尽量避免。
可以看到,有三行以 android:text 开头的代码出现了错误信息。暂时忽略它们,稍后会
处理。
对照上图用户界面查看XML文件,可以看出组件与XML元素一一对应。元素的名
称就是组件的类型。
各元素均有一组XML属性。属性可以看作如何配置组件的指令。
为方便理解元素与属性的工作原理,接下来我们将以层级视角来研究布局。
1.2.1、视图层级结构
组件包含在视图(View)对象的层级结构中,这种结构又称作视图层级结构(view hierarchy)。
上述代码所示的XML布局对应的视图层级结构图 图5:
从布局的视图层级结构可以看到,其根元素是一个 LinearLayout 组件。作为根元素,
LinearLayout 组件必须指定Android XML资源文件的命名空间属性,这里是http://schemas.
android.com/apk/res/android。
LinearLayout 组件继承自 ViewGroup 组件(也是个 View 子类)。 ViewGroup 组件是包含并配
置其他组件的特殊组件。想要以一列或一排的样式布置组件,就可以使用 LinearLayout 组件。
其他 ViewGroup 子类还有 FrameLayout 、 TableLayout 和 RelativeLayout 。
若某个组件包含在一个 ViewGroup 中,该组件与 ViewGroup 即构成父子关系。根 Linear-
Layout 有两个子组件: TextView 和另一个 LinearLayout 。作为子组件的 LinearLayout 自己还
有两个 Button 子组件。
1.2.2、组件属性
看看配置组件时常用的一些属性。
- android:layout_width 和 android:layout_height 属性
几乎每类组件都需要 android:layout_width 和 android:layout_height 属性。以下是它
们的两个常见属性值(二选一)。
match_parent :视图与其父视图大小相同。
wrap_content :视图将根据其显示内容自动调整大小。
(以前还有个 fill_parent 属性值,等同于 match_parent ,现已废弃不用。)
根 LinearLayout 组件的高度与宽度属性值均为 match_parent 。 LinearLayout 虽然是根元
素,但它也有父视图——Android提供该父视图来容纳应用的整个视图层级结构。
其他包含在界面布局中的组件,其高度与宽度属性值均被设置为 wrap_content 。
TextView 组件比其包含的文字内容区域稍大一些,这主要是 android:padding="24dp" 属性
的作用。该属性告诉组件在决定大小时,除内容本身外,还需增加额外指定量的空间。这样屏幕
上显示的问题与按钮之间便会留有一定的空间,使整体显得更为美观。(不理解dp的意思?dp即
density-independent pixel,指与密度无关的像素。)
2. android:orientation 属性
android:orientation 属性是两个 LinearLayout 组件都具有的属性,它决定两者的子组件
是水平放置还是垂直放置。根 LinearLayout 是垂直的,子 LinearLayout 是水平的。
子组件的定义顺序决定其在屏幕上显示的顺序。在垂直的 LinearLayout 中,第一个定义的
子组件出现在屏幕的最上端;而在水平的 LinearLayout 中,第一个定义的子组件出现在屏幕的
最左端。(如果设备文字从右至左显示,如阿拉伯语或者希伯来语,第一个定义的子组件则出现
在屏幕的最右端。)
3. android:text 属性
TextView 与 Button 组件具有 android:text 属性。该属性指定组件要显示的文字内容。
请注意, android:text 属性值不是字符串值,而是对字符串资源(string resource)的引用。
字符串资源包含在一个独立的名叫strings的XML文件中(strings.xml),虽然可以硬编码设置
组件的文本属性值,如 android:text="True" ,但这通常不是个好主意。
比较好的做法是:将文
字内容放置在独立的字符串资源XML文件中,然后引用它们。这样会方便应用的本地化(支持
多国语言)。
需要在activity_quiz.xml文件中引用的字符串资源还没添加。现在就来处理。
1.2.3、创建字符串资源
每个项目都包含一个默认字符串资源文件strings.xml。
在项目工具窗口中,找到app/res/values目录,展开目录,打开strings.xml文件。
可以看到,项目模板已经添加了一些字符串资源。如下代码所示,添加应用布局需要
的三个新的字符串。
<resources>
<string name="app_name">GeoQuiz</string>
<string name="question_text">Canberra is the capital of Australia.</string>
<string name="true_button">True</string>
<string name="false_button">False</string>
</resources>
(某些版本的Android Studio的strings.xml默认带有其他字符串,请勿删除它们,否则会引发与
其他文件的联动错误。)
现在,在GeoQuiz项目的任何XML文件中,只要引用到 @string/false_button ,应用运行
时,就会得到文本“False”。
保存strings.xml文件。这时,activity_quiz.xml布局缺少字符串资源的提示信息应该就消失了。
(如仍有错误提示,请检查一下这两个文件,确认没有拼写错误。)
默认的字符串文件虽然已命名为strings.xml,仍可以按个人喜好重命名。一个项目也可以
有多个字符串文件。只要这些文件都放在res/values/目录下,含有一个 resources 根元素,以及多
个 string 子元素,应用就能找到并正确使用它们。
1.3、从布局 XML 到视图对象
知道activity_quiz.xml中的XML元素是如何转换为视图对象的吗?答案就在于 QuizActivity
类。
在创建GeoQuiz项目的同时,向导也创建了一个名叫 QuizActivity 的 Activity 子类。
QuizActivity 类文件存放在项目的app/java目录下。java目录是项目全部Java源代码的存放处。
在项目工具窗口中,依次展开app/java目录与com.bignerdranch.android.geoquiz包。找到并打
开QuizActivity.java文件,查看其中的代码,如下所示:
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class QuizActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_quiz);
}
}
是不是不明白 AppCompatActivity 的作用?它实际就是一个 Activity 子类,能为Android
旧版本系统提供兼容支持。
如果无法看到全部类包导入语句,请单击第一行导入语句左边的⊕符号来显示它们。
该Java类文件有一个 Activity 方法: onCreate(Bundle) 。
(如果你的文件还包含 onCreateOptionsMenu(Menu) 和 onOptionsItemSelected(MenuItem)
方法,暂时不用理会)
activity子类的实例创建后, onCreate(Bundle) 方法会被调用。activity创建后,它需要获取
并管理用户界面。要获取activity的用户界面,可调用以下 Activity 方法:
public void setContentView(int layoutResID)
根据传入的布局资源ID参数,该方法生成指定布局的视图并将其放置在屏幕上。布局视图生
成后,布局文件包含的组件也随之以各自的属性定义完成实例化。
1.3.1、资源与资源 ID
布局是一种资源。资源是应用非代码形式的内容,如图像文件、音频文件以及XML文件等。
项目的所有资源文件都存放在目录app/res的子目录下。在项目工具窗口中可以看到,
activity_quiz.xml布局资源文件存放在res/layout/目录下。strings.xml字符串资源文件存放在
res/values/目录下。
可以使用资源ID在代码中获取相应的资源。activity_quiz.xml布局的资源ID为R.layout.
activity_quiz。
查看GeoQuiz应用的资源ID需要切换项目视图。Android Studio默认使用Android项目视图,
图6所示。为让开发者专注于最常用的文件和目录,默认视图隐藏了Android项目的真实文件
目录结构。
图6:
在项目工具窗口的最上部找到下拉菜单,从Android项目视图切换至Project视图。Project视图
会显示出当前项目的所有文件和目录。
展开目录app/build/generated/source/r/debug,找到项目包名称并打开其中的R.java文件,即可
看到GeoQuiz应用当前所有的资源。R.java文件在Android项目编译过程中自动生成,如该文件头
部的警示所述,请不要修改该文件的内容。
修改布局或字符串等资源后,R.java文件不会实时刷新。Android Studio另外还存有一份代码
编译用的R.java隐藏文件。当前代码编辑区打开的R.java文件仅在应用安装至设备或模拟器前产
生,因此只有在Android Studio中点击运行应用时,它才会得到更新。
R.java文件通常比较大,如下所示:
/* AUTO-GENERATED FILE. DO NOT MODIFY.
*
* This class was automatically generated by the
* aapt tool from the resource data it found. It
* should not be modified by hand.
*/
public final class R {
public static final class anim {
...
}
...
public static final class id {
...
}
public static final class layout {
...
public static final int activity_quiz=0x7f030017;
}
public static final class mipmap {
public static final int ic_launcher=0x7f030000;
}
public static final class string {
...
public static final int app_name=0x7f0a0010;
public static final int false_button=0x7f0a0012;
public static final int question_text=0x7f0a0014;
public static final int true_button=0x7f0a0015;
}
}
可以看到R.layout.activity_quiz即来自该文件。 activity_quiz 是 R 的内部类 layout 里的一个
整型常量名。
GeoQuiz应用需要的字符串同样具有资源ID。目前为止,我们还未在代码中引用过字符串,
如果需要,可以使用以下方法:
setTitle(R.string.app_name);
Android为整个布局文件以及各个字符串生成资源ID,但activity_quiz.xml布局文件中的组件
除外,因为不是所有组件都需要资源ID。在本章中,我们要在代码里与两个按钮交互,因此只需
为它们生成资源ID即可。
这里主要使用Android项目视图,生成资源ID之前,记得切回。当然,如果你就喜欢使用Project
视图,也没啥问题。
要为组件生成资源ID,请在定义组件时为其添加 android:id 属性。在activity_quiz.xml文件
中,分别为两个按钮添加上 android:id 属性,如下代码所示:
<LinearLayout ... >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="24dp"
android:text="@string/question_text" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/true_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/true_button" />
<Button
android:id="@+id/false_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/false_button" />
</LinearLayout>
</LinearLayout>
注意, android:id 属性值前面有一个+标志,而 android:text 属性值则没有。这是因为我
们在创建资源ID,而对字符串资源只是做引用。
1.4、组件的实际应用
按钮有了资源ID,就可以在 QuizActivity 中直接获取它们。首先,在QuizActivity.java文件
中增加两个成员变量。
在QuizActivity.java文件中输入代码:
public class QuizActivity extends AppCompatActivity {
private Button mTrueButton;
private Button mFalseButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_quiz);
}
}
文件保存后,会看到两个错误提示。没关系,稍后会处理。请注意新增的两个成员(实例)
变量名称的 m 前缀。该前缀是Android编程应遵循的命名约定。
现在,将鼠标移至代码左边的错误提示处时,会看到两条同样的错误信息:Cannot resolve
symbol 'Button'。
这告诉我们,需要在QuizActivity.java文件中导入 android.widget.Button 类包。可在文件
头部手动输入以下代码:
import android.widget.Button;
或者使用Option+Return(Alt+Enter)组合键,让Android Studio自动为你导入。代码有误时,
也可以使用该组合键来修正。记得要常用。
类包导入后,刚才的错误提示应该就消失了。(如果仍然存在,请检查Java代码以及XML文
件,确认是否存在输入或拼写错误。)
接下来,我们来编码使用按钮组件,这需要以下两个步骤:
- 引用生成的视图对象;
- 为对象设置监听器,以响应用户操作。
1.4.1 引用组件
在activity中,可调用以下 Activity 方法引用已生成的组件:
public View findViewById(int id)
该方法以组件的资源ID作为参数,返回一个视图对象。
在QuizActivity.java文件中,使用按钮的资源ID获取视图对象,赋值给对应的成员变量。注意,赋值前,必须先将返回的 View 类型转换为 Button 。
public class QuizActivity extends AppCompatActivity {
private Button mTrueButton;
private Button mFalseButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_quiz);
mTrueButton = (Button) findViewById(R.id.true_button);
mFalseButton = (Button) findViewById(R.id.false_button);
}
}
1.4.2 设置监听器
Android应用属于典型的事件驱动类型。不像命令行或脚本程序,事件驱动型应用启动后,
即开始等待行为事件的发生,如用户点击某个按钮。(事件也可以由操作系统或其他应用触发,
但用户触发的事件更直观,如点击按钮。)
应用等待某个特定事件的发生,也可以说应用正在“监听”特定事件。为响应某个事件而创
建的对象叫作监听器(listener)。监听器会实现特定事件的监听器接口(listener interface)。
无需自己动手,Android SDK已经为各种事件内置了很多监听器接口。当前应用需要监听用
户的按钮“点击”事件,因此监听器需实现 View.OnClickListener 接口。
首先处理TRUE按钮。在QuizActivity.java文件中,在 onCreate(Bundle) 方法的变量赋值语
句后输入下列代码
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_quiz);
mTrueButton = (Button) findViewById(R.id.true_button);
mTrueButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Does nothing yet, but soon!
}
});
mFalseButton = (Button) findViewById(R.id.false_button);
}
(如果遇到View cannot be resolved to a type的错误提示,请使用Option+Return(Alt+Enter)快
捷键导入 View 类。)
在上面代码中,我们设置了一个监听器。按钮 mTrueButton 被点击后,监听器会立即通
知我们。传入 setOnClickListener(OnClickListener) 方法的参数是一个监听器。它是一个实
现了 OnClickListener 接口的对象。
使用匿名内部类
这里,一个匿名内部类(anonymous inner class)实现了 OnClickListener 接口。语法看上去
稍显复杂,不过有个助记小技巧:最外层一对括号内的全部代码就是传入 setOnClickListener
(OnClickListener) 方法的参数。
mTrueButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Does nothing yet, but soon!
}
});
后续所有的监听器都以匿名内部类来实现。这样做有两大好处:
- 第一,使用匿名内部类,可 以相对集中地实现监听器方法,一眼可见;
- 第二,事件监听器一般只在一个地方使用,使用匿名
内部类,就不用去创建繁琐的命名类了。
匿名内部类实现了 OnClickListener 接口,因此它也必须实现该接口唯一的 onClick(View)
方法。 onClick(View) 现在是个空方法。虽然必须实现 onClick(View) 方法,但具体如何实现取
决于使用者,因此即使是个空方法,编译器也可以编译通过。
(如果匿名内部类、监听器、接口等概念已忘得差不多了,现在就该去复习一下,或找本参
考手册备查。)
1.5、创建提示消息
接下来要实现的是,分别点击两个按钮,弹出我们称之为toast的提示消息。Android的toast
是用来通知用户的简短弹出消息,用户无需输入什么,也不用做任何干预操作。这里,我们要用
toast来反馈答案,如图7所示:
首先回到strings.xml文件,如下代码所示,为toast添加消息显示用的字符串资源。
<resources>
<string name="app_name">GeoQuiz</string>
<string name="question_text">Canberra is the capital of Australia.</string>
<string name="true_button">True</string>
<string name="false_button">False</string>
<string name="correct_toast">Correct!</string>
<string name="incorrect_toast">Incorrect!</string>
</resources>
调用 Toast 类的以下方法,可创建toast:
public static Toast makeText(Context context, int resId, int duration)
该方法的 Context 参数通常是 Activity 的一个实例( Activity 本身就是 Context 的子类)。
第二个参数是toast要显示字符串消息的资源ID。 Toast 类必须借助 Context 才能找到并使用字符
串资源ID。第三个参数通常是两个 Toast 常量中的一个,用来指定toast消息的停留时间。
创建toast后,可调用 Toast.show() 方法在屏幕上显示toast消息。
在 QuizActivity 代码里,分别调用 makeText(...) 方法。在添加
makeText(...) 时,可利用Android Studio的代码自动补全功能,让代码输入更轻松。
使用代码自动补全
代码自动补全功能可以节约大量开发时间,越早掌握受益越多。
依次输入下面代码。当输入到 Toast 类后的点号时,Android Studio会弹出一
个窗口,窗口内显示了建议使用的 Toast 类的常量与方法。
要选择需要的建议方法,使用上下键。(如果不想使用代码自动补全功能,请不要按Tab键、
Return/Enter键,或使用鼠标点击弹出窗口,只管继续输入代码直至完成。)
在建议列表里,选择 makeText(Context context, int resID, int duration) 方法,代
码自动补全功能会自动添加完整的方法调用。
完成 makeText 方法的全部参数设置,完成后的代码如下代码所示:
mTrueButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(QuizActivity.this,
R.string.correct_toast,
Toast.LENGTH_SHORT).show();
// Does nothing yet, but soon!
}
});
mFalseButton = (Button) find ViewById(R.id.false_button);
mFalseButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(QuizActivity.this,
R.string.incorrect_toast,Toast.LENGTH_SHORT).show();
}
});
在 makeText(...) 里,传入 QuizActivity 实例作为 Context 的参数值。注意此处应输入的
参数是 QuizActivity.this ,不要想当然地直接输入 this 。因为匿名类的使用,这里的 this 指
的是监听器 View.OnClickListener 。
使用代码自动补全功能,自己也就不用导入 Toast 类了,因为Android Studio会自动导入相
关类。
好了,现在可以运行应用了。
1.6、 使用模拟器运行应用
运行Android应用需使用硬件设备或虚拟设备(virtual device)。包含在开发工具中的Android
设备模拟器可提供多种虚拟设备。
要创建Android虚拟设备(AVD),在Android Studio中,选择Tools → Android → AVD Manager
菜单项。AVD管理器窗口弹出时,点击窗口左下角的+Create Virtual Device…按钮。一路next,详细的创建不赘述了。
创建完成后即可启动运行。
AVD创建成功后,我们用它运行GeoQuiz应用。点击Android Studio工具栏上的Run按钮,或
者使用Control+R快捷键。Android Studio会自动找到新建的虚拟设备,安装应用包(APK),然后
启动并运行应用。
模拟器的启动过程比较耗时,请耐心等待。等设备启动完成,应用运行后,就可以在应用界
面点击按钮,让toast告诉你答案了。
假如启动时或在点击按钮时,GeoQuiz应用崩溃,可以在Android DDMS工具窗口的LogCat
视图中看到有用的诊断信息。(如果LogCat没有自动打开,可点击Android Studio窗口底部的
Android Monitor按钮打开它。),查看日志。
最好不要关掉模拟器,这样就不必在反复运行调试应用时,浪费时间等待AVD启动了。
单击AVD模拟器上的后退按钮可以停止应用。这个后退按钮的形状像一个指向左侧的三角形
(在较早版本的Android中,它像一个U型箭头)。需要调试变更时,再通过Android Studio重新运
行应用。
模拟器虽然好用,但在实体设备上测试应用能获得更准确的结果。
1.7、深入学习:Android 编译过程
学习到这里,你可能迫切想知道Android是如何编译的。
你已经知道在项目文件发生变化时,
Android Studio无需指示便会自动进行编译。
在整个编译过程中,Android开发工具将资源文件、
代码以及AndroidManifest.xml文件(包含应用的元数据)编译生成.apk文件。.apk文件要在模拟器上
运行,还需以debug key签名。(分发.apk应用给用户时,应用必须以release key签名。更多有关编译
过程的信息,可参考Android开发文档网页developer.android.com/tools/publishing/preparing.html。)
那么,应用的activity_quiz.xml布局文件的内容该如何转变为 View 对象呢?作为编译过程的
一部分,aapt(Android Asset Packaging Tool)将布局文件资源编译压缩紧凑后,打包到.apk文件
中。
然后,在 QuizActivity 类的 onCreate(Bundle) 方法调用 setContentView(...) 方法时,
QuizActivity 使用 LayoutInflater 类实例化布局文件中定义的每一个 View 对象,如图8所示:
1.7.1、Android 编译工具
当前,我们看到的项目编译都是在Android Studio里执行的。编译功能已整合到IDE中,IDE
负责调用aapt等Android标准编译工具,但编译过程本身仍由Android Studio管理。
有时,出于某种原因,可能需要脱离Android Studio编译代码。最简单的方法是使用命令行
编译工具。现代Android编译系统使用Gradle编译工具
要从命令行使用Gradle,请切换到项目目录并执行以下命令:
$ ./gradlew tasks
如果是Windows系统,执行以下命令:
gradlew.bat tasks
执行以上命令会显示一系列可用任务。你需要的任务是installDebug,因此,再执行以下命令:
$ ./gradlew installDebug
如果是Windows系统,执行以下命令:
gradlew.bat installDebug
以上命令将把应用安装到当前连接的设备上,但不会运行它。要运行应用,需要在设备上手
动启动。