Skip to content
<

简单控件

本章介绍了 App 开发常见的几类简单控件的用法,主要包括:显示文字的文本视图、容纳视图的常用布局、响应点击的按钮控件、显示图片的图像视图等。然后结合本章所学的知识,演示了一个实战项目“简单计算器”的设计与实现。

文本显示

本节介绍了如何在文本视图 TextView 上显示规定的文本,包括:怎样在 XML 文件和 Java 代码中设置文本内容,尺寸的大小有哪些单位、又该怎样设置文本的大小,颜色的色值是如何表达的、又该怎样设置文本的颜色。

设置文本的内容

在前一章的“使用 Java 代码书写程序逻辑”小节,给出了设置文本内容的两种方式,一种是在 XML 文件中通过属性 android:text 设置文本,比如下面这样: (完整代码见 chapter03\src\main\res\layout\activity_text_view.xml)

xml
<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="你好,世界!" />

另一种是在 Java 代码中调用文本视图对象的 setText 方法设置文本,比如下面这样: (完整代码见chapter03\src\main\java\com\example\chapter03\TextViewActivity.java)

java
// 获取名为tv_hello的文本视图
TextView tv_hello = findViewById(R.id.tv_hello);
tv_hello.setText("你好,世界");  // 设置tv_hello的文字内容

在XML文件中设置文本的话,把鼠标移到“你好,世界”上方时, Android Studio 会弹出如图 3-1 所示的提示框。

看到提示内容为“Hardcoded string "你好,世界", should use @string resouce”,意思说这几个字是硬编码的字符串,建议使用来自 @string 的资源。原来 Android Studio 不推荐在 XML 布局文件里直接写字符串,因为可能有好几个页面都显示“你好,世界”,若想把这句话换成“你吃饭了吗?”,就得一个一个 XML 文件改过去,无疑费时费力。故而 Android Studio 推荐把字符串放到专门的地方管理,这个名为 @string 的地方位于 res/values 目录下的 strings.xml,打开该文件发现它的初始内容如下所示:

xml
<resources>
    <string name="app_name">MyApp</string>
</resources>

看来 strings.xml 定义了一个名为“app_name”的字符串常量,其值为“MyApp”。那么在此添加新的字符串定义,字符串名为“hello”,字符串值为“你好,世界”,添加之后的 strings.xml 内容如下所示:

xml
<resources>
    <string name="app_name">MyApp</string>
    <string name="hello">你好,世界!</string>
</resources>

添加完新的字符串定义,回到 XML 布局文件,将 android:text 属性值改为“@string/字符串名”这般,也就是“@string/hello”,修改之后的 TextView 标签示例如下:

xml
<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/hello" />

然后把鼠标移到“你好,世界”上方,此时 Android Studio 不再弹出任何提示了。 若要在 Java 代码中引用字符串资源,则调用 setText 方法时填写形如“R.string.字符串名”的参数,就本例而言填入“R.string.hello”,修改之后的 Java 代码示例如下:

java
// 获取名为tv_hello的文本视图
TextView tv_hello = findViewById(R.id.tv_hello);
tv_hello.setText(R.string.hello);  // 设置tv_hello的文字内容

至此不管 XML 文件还是 Java 代码都从 strings.xml 引用字符串资源,以后想把“你好,世界”改为其他文字的话,只需改动 strings.xml 一个地方即可。

设置文本的大小

TextView 允许设置文本内容,也允许设置文本大小,在 Java 代码中调用 setTextSize 方法,即可指定文本大小,就像以下代码这样:

(完整代码见 chapter03\src\main\java\com\example\chapter03\TextSizeActivity.java)

java
// 从布局文件中获取名叫tv_sp的文本视图
TextView tv_sp = findViewById(R.id.tv_sp);
tv_sp.setTextSize(30); // 设置tv_sp的文本大小

这里的大小数值越大,则看到的文本也越大;大小数值越小,则看到的文本也越小。在 XML 文件中则通过属性 android:textSize 指定文本大小,可是如果给 TextView 标签添加“android:textSize="30"”,数字马上变成红色如图 3-2 所示,鼠标移过去还会提示错误“Cannot resolve symbol '30'”,意思是无法解析“30”这个符号。

原来文本大小存在不同的字号单位,XML 文件要求在字号数字后面写明单位类型,常见的字号单位主要有 px、dp、sp 3 种,分别介绍如下。

  1. px

px 是手机屏幕的最小显示单位,它与设备的显示屏有关。一般来说,同样尺寸的屏幕(比如 6 英寸手机),如果看起来越清晰,则表示像素密度越高,以 px 计量的分辨率也越大。

  1. dp

dp 有时也写作 dip,指的是与设备无关的显示单位,它只与屏幕的尺寸有关。一般来说,同样尺寸的屏幕以 dp 计量的分辨率是相同的,比如同样是 6 英寸手机,无论它由哪个厂家生产,其分辨率换算成 dp 单位都是一个大小。

  1. sp

sp 的原理跟 dp 差不多,但它专门用来设置字体大小。手机在系统设置里可以调整字体的大小(小、标准、大、超大)。设置普通字体时,同数值 dp 和 sp 的文字看起来一样大;如果设置为大字体,用 dp 设置的文字没有变化,用 sp 设置的文字就变大了。

字体大小采用不同单位的话,显示的文字大小各不相同。例如,30px、30dp、30sp这 3 个字号,在不同手机上的显示大小有所差异。有的手机像素密度较低,一个 dp 相当于两个 px,此时 30px 等同于 15dp;有的手机像素密度较高,一个 dp 相当于 3 个 px,此时 30px 等同于 10dp。假设某个 App 的内部文本使用字号 30px,则该 App 安装到前一部手机的字体大小为 15dp,安装到后一部手机的字体大小为 10dp,显然后一部手机显示的文本会更小。

至于 dp 与 sp 之间的区别,可通过以下实验加以观察。首先创建测试活动页面,该页面的 XML 文件分别声明 30px、30dp、30sp 这 3 个字号的 TextView 控件,布局内容如下所示:

(完整代码见 chapter03\src\main\res\layout\activity_text_size.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">

    <TextView
        android:id="@+id/tv_px"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="你好,世界(px 大小)"
        android:textSize="30px" />

    <TextView
        android:id="@+id/tv_dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="你好,世界(dp 大小)"
        android:textSize="30dp" />

    <TextView
        android:id="@+id/tv_sp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="你好,世界(sp 大小)"
        android:textSize="30sp" />

</LinearLayout>

接着打开手机的设置菜单,依次选择“显示”→“字体与显示大小”,确认当前的字体为标准大小,如图 3-3 所示。然后在手机上运行测试 App,进入测试页面看到的文字效果如图 3-4 所示。

回到设置菜单的字体页面,将字体大小调整为大号,如图 3-5 所示。再次进入测试页面看到的文字效果如图 3-6 所示。

对照图 3-4 和图 3-6,发现字号单位 30px 和 30dp 的文字大小不变,而 30sp 的文字随着系统字体一起变大了。

既然 XML 文件要求 android:textSize 必须指定字号单位,为什么 Java 代码调用 setTextSize 只填数字不填单位呢?其实查看 SDK 源码,找到 setTextSize 方法的实现代码如下所示:

java
@android.view.RemotableViewMethod
public void setTextSize(float size) {
    setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
}

原来纯数字的 setTextSize 方法,内部默认字号单位为 sp(COMPLEX_UNIT_SP),这也从侧面印证了之前的说法:sp 才是 Android 推荐的字号单位。

补充

名称解释
px(Pixel 像素)也称为图像元素,是作为图像构成的基本单元,单个像素的大小并不固定,跟随屏幕大小和像素数量的关系变化,一个像素点为 1px。
Resolution(分辨率)是指屏幕的垂直和水平方向的像素数量,如果分辨率是 1920*1080 ,那就是垂直方向有 1920 个像素,水平方向有 1080 个像素。
Dpi(像素密度)是指屏幕上每英寸(1 英寸 = 2.54 厘米)距离中有多少个像素点。
Density(密度)是指屏幕上每平方英寸(2.54 ^ 2 平方厘米)中含有的像素点数量。
Dip/dp(设备独立像素)也可以叫做 dp,长度单位,同一个单位在不同的设备上有不同的显示效果,具体效果根据设备的密度有关,详细的公式请看下面 。

计算规则

我们以一个 4.95 英寸 1920 * 1080 的 nexus5 手机设备为例:

Dpi

  1. 计算直角边像素数量: 1920^2+1080^2=2202^2(勾股定理)。
  2. 计算 DPI:2202 / 4.95 = 445。
  3. 得到这个设备的 DPI 为 445(每英寸的距离中有 445 个像素)。

Density

上面得到每英寸中有 445 像素,那么 density 为每平方英寸中的像素数量,应该为: 445^2=198025。

Dip

所有显示到屏幕上的图像都是以 px 为单位,Dip 是我们开发中使用的长度单位,最后他也需要转换成 px,计算这个设备上 1dip 等于多少 px:

px = dip x dpi /160

根据换算关系:

320 x 480 分辨率,3.6 寸的手机:dpi 为 160,1dp=1px

实验一

相同分辨率,不同大小的手机 AB:

代号分辨率尺寸dpidp
手机 A320x4803.6 寸1601dp=1px
手机 B320x4807.2 寸801dp=0.5px

假如 AB 都设置一个宽度为 100dp 的 TextView:

代号TextView 宽度手机宽度比例关系
手机 A100px320px10/32
手机 B50px320px5/32

得出结论:

对于相同分辨率的手机,屏幕越大,同DP的组件占用屏幕比例越小

如图所示:

实验二

相同大小,不同分辨率的手机 AB:

代号分辨率尺寸dpidp
手机 A320x4803.6 寸1601dp=1px
手机 B640x9603.6 寸3201dp=2px

假如 AB 都设置了一个宽度为 100dp 的 TextView:

|代号|TextView 宽度|手机宽度|比例关系| |手机 A|100px|320px|10/32| |手机 B|200px|640px|10/32|

得出结论:

对于相同尺寸的手机,即使分辨率不同,同 DP 的组件占用屏幕比例也相同。

如图:

综上:

dp 的 UI 效果只在相同尺寸的屏幕上相同,如果屏幕尺寸差异过大,则需要重做 dp 适配

这也是平板需要单独做适配的原因,可见dp 不是比例

设置文本的颜色

除了设置文字大小,文字颜色也经常需要修改,毕竟 Android 默认的灰色文字不够醒目。在 Java 代码中调用 setTextColor 方法即可设置文本颜色,具体在 Color 类中定义了 12 种颜色,详细的取值说明见表 3-1。

Color 类中的颜色类型说明Color 类中的颜色类型说明
BLACK黑色GREEN绿色
DKGRAY深灰BLUE蓝色
GRAY灰色YELLOW黄色
LTGRAY浅灰CYAN青色
WHITE白色MAGENTA玫红
RED红色TRANSPARENT透明

比如以下代码便将文本视图的文字颜色改成了绿色:

(完整代码见 chapter03\src\main\java\com\example\chapter03\TextColorActivity.java)

java
// 从布局文件中获取名为tv_code_system的文本视图
TextView tv_code_system = findViewById(R.id.tv_code_system);
 // 将tv_code_system的文字颜色设置系统自带的绿色
tv_code_system.setTextColor(Color.GREEN);

可是 XML 文件无法引用 Color 类的颜色常量,为此 Android 制定了一套规范的编码标准,将色值交由透明度alpha 和 RGB 三原色(红色 red、绿色 green、蓝色 blue)联合定义。该标准又有八位十六进制数与六位十六进制数两种表达方式,例如八位编码 FFEEDDCC 中,FF 表示透明度,EE 表示红色的浓度,DD 表示绿色的浓度,CC 表示蓝色的浓度。透明度为 FF 表示完全不透明,为 00 表示完全透明。RGB 三色的数值越大,表示颜色越浓,也就越暗;数值越小,表示颜色越淡,也就越亮。RGB 亮到极致就是白色,暗到极致就是黑色。

至于六位十六进制编码,则有两种情况,它在 XML 文件中默认不透明(等价于透明度为 FF),而在代码中默认透明(等价于透明度为 00)。以下代码给两个文本视图分别设置六位色值与八位色值,注意添加 0x 前缀表示十六进制数:

java
// 从布局文件中获取名为tv_code_six的文本视图
TextView tv_code_six = findViewById(R.id.tv_code_six);
 // 将tv_code_six的文字颜色设置为透明的绿色,透明就是看不到
tv_code_six.setTextColor(0x00ff00);
 // 从布局文件中获取名为tv_code_eight的文本视图
TextView tv_code_eight = findViewById(R.id.tv_code_eight);
 // 将tv_code_eight的文字颜色设置为不透明的绿色,即正常的绿色
tv_code_eight.setTextColor(0xff00ff00);

运行测试 App,发现 tv_code_six 控件的文本不见了(其实是变透明了),而 tv_code_eight 控件的文本显示正常的绿色。

在 XML 文件中可通过属性 android:textColor 设置文字颜色,但要给色值添加井号前缀“#”,设定好文本颜色的 TextView 标签示例如下: (完整代码见 chapter03\src\main\res\layout\activity_text_color.xml)

xml
<TextView
    android:id="@+id/tv_xml"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="布局文件设置六位文字颜色"
    android:textColor="#00ff00"
    android:textSize="17sp" />

就像字符串资源那样,Android 把颜色也当作一种资源,打开 res/values 目录下的 colors.xml,发现里面已经定义了 3 种颜色:

xml
<resources>
    <color name="colorPrimary">#008577</color>
    <color name="colorPrimaryDark">#00574B</color>
    <color name="colorAccent">#D81B60</color>
</resources>

那么先在 resources 节点内部补充如下的绿色常量定义:

xml
<color name="green">#00ff00</color>

然后回到 XML 布局文件,把 android:textColor 的属性值改为“@color/颜色名称”,也就是 android:textColor="@color/green",修改之后的标签 TextView 如下所示:

java
<TextView
    android:id="@+id/tv_values"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="资源文件引用六位文字颜色"
    android:textColor="@color/green"
    android:textSize="17sp" />

不仅文字颜色,还有背景颜色也会用到上述的色值定义,在 XML 文件中通过属性 android:background 设置控件的背景颜色。Java 代码则有两种方式设置背景颜色,倘若色值来源于 Color 类或十六进制数,则调用 setBackgroundColor 方法设置背景;倘若色值来源于 colors.xml 中的颜色资源,则调用 setBackgroundResource 方法,以“R.color.颜色名称”的格式设置背景。下面是两种方式的背景设定代码例子:

java
// 从布局文件中获取名叫tv_code_background的文本视图
TextView tv_code_background = findViewById(R.id.tv_code_background);
 // 将tv_code_background的背景颜色设置为绿色
tv_code_background.setBackgroundColor(Color.GREEN); // 在代码中定义的色值
tv_code_background.setBackgroundResource(R.color.green); // 颜色来源于资源文件

注意属性 android:background 和 setBackgroundResource 方法,它俩用来设置控件的背景,不单单是背景颜色,还包括背景图片。在设置背景图片之前,先将图片文件放到 res/drawable* 目录(以 drawable 开头的目录,不仅仅是 drawable 目录),然后把 android:background 的属性值改为“@drawable/不含扩展名的图片名称”,或者调用 setBackgroundResource 方法填入“R.drawable.不含扩展名的图片名称”。

视图基础

本节介绍视图的几种基本概念及其用法,包括如何设置视图的宽度和高度,如何设置视图的外部间距和内部间距,如何设置视图的外部对齐方式和内部对齐方式,等等。

设置视图的宽高

手机屏幕是块长方形区域,较短的那条边叫做宽,较长的那条边叫做高。App 控件通常也是长方形状,控件宽度通过属性 android:layout_width 表达,控件高度通过属性 android:layout_height 表达,宽高的取值主要有下列 3 种:

  1. match_parent:表达与上级视图保持一致。上级视图的尺寸有多大,当前视图的尺寸就有多大。
  2. wrap_parent:表示与内容自适应。对于文本视图来说,内部文字需要多大的显示空间,当前视图就要占据多大的尺寸。但最宽不能超过上级视图的宽度,一旦超过就要换行;最高不能超过上级视图的高度,一旦超过就会隐藏。
  3. 以 dp 为单位的具体尺寸,比如 300dp,表示宽度或者高度就是这么大。

在 XML 文件中采用以上任一方式均可设置视图的宽高,但在 Java 代码中设置宽高就有点复杂了,首先确保 XML 中的宽高属性值为 wrap_content,这样才允许在代码中修改宽高。接着打开该页面对应的 Java 代码,依序执行以下 3 个步骤:

步骤一,调用控件对象的 getLayoutParams 方法,获取该控件的布局参数,参数类型为 ViewGroup.LayoutParams。

步骤二,布局参数的 width 属性表示宽度,height 属性表示高度,修改这两个属性值,即可调整控件的宽高。

步骤三,调用控件对象的 setLayoutParams 方法,填入修改后的布局参数使之生效。

不过布局参数的 width 和 height 两个数值默认是 px 单位,需要将 dp 单位的数值转换为 px 单位的数值,然后才能赋值给 width 属性和 height 属性。下面是把 dp 大小转为 px 大小的方法代码:

(完整代码见 chapter03\src\main\java\com\example\chapter03\util\Utils.java)

java
// 根据手机的分辨率从 dp 的单位 转成为 px(像素)
public static int dip2px(Context context, float dpValue) {
    // 获取当前手机的像素密度(1 个 dp 对应几个 px)
    float scale = context.getResources().getDisplayMetrics().density;
    return (int) (dpValue * scale + 0.5f); // 四舍五入取整
}

有了上面定义的公共方法 dip2px,就能将某个 dp 数值转换成 px 数值,比如准备把文本视图的宽度改为 300dp,那么调整宽度的 Java 代码示例如下:

java
// 获取名为 tv_code 的文本视图
TextView tv_code = findViewById(R.id.tv_code);
// 获取 tv_code 的布局参数(含宽度和高度)
ViewGroup.LayoutParams params = tv_code.getLayoutParams();
// 修改布局参数中的宽度数值,注意默认 px 单位,需要把 dp 数值转成 px 数值
params.width = Utils.dip2px(this, 300);
tv_code.setLayoutParams(params); // 设置 tv_code 的布局参数

接下来通过演示页面并观察几种尺寸设置方式的界面效果,主要通过背景色区分当前视图的宽高范围,详细的 XML 文件内容如下所示:

(完整代码见 chapter03\src\main\res\layout\activity_view_border.xml)

java
<?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">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:background="#00ffff"
        android:text="视图宽度采用wrap_content定义"
        android:textColor="#000000"
        android:textSize="17sp" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:background="#00ffff"
        android:text="视图宽度采用match_parent定义"
        android:textColor="#000000"
        android:textSize="17sp" />

    <TextView
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:background="#00ffff"
        android:text="视图宽度采用固定大小"
        android:textColor="#000000"
        android:textSize="17sp" />

    <TextView
        android:id="@+id/tv_code"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:background="#00ffff"
        android:text="通过代码指定视图宽度"
        android:textColor="#000000"
        android:textSize="17sp" />

</LinearLayout>

运行测试 App,打开演示界面如图 3-7 所示,一句背景色判断文本视图的边界,可见 wrap_content 方式刚好包住了文本内容,match_parent 方式扩展到了与屏幕等宽,而 300dp 的宽度介于前两者之间(安卓手机的屏幕宽度基本为 360dp)。

设置视图的间距

在上一小节末尾的 XML 文件中,每个 TextView 标签都携带新的属性 android:layout_marginTop="5dp",该属性的作用是让当前视图与上方间隔一段距离。同理,android:layout_marginLeft 让当前视图与左边间隔一段距离,android:layout_marginRight 让当前视图与右边间隔一段距离,android:layout_marginBottom 让当前视图与下方间隔一段距离。如果上下左右都间隔同样的距离,还能使用 android:layout_margin 一次性设置四周的间距。

layout_margin 不单单用于文本视图,还可用于所有视图,包括各类布局和各类控件。因为不管布局还是控件,它们统统由视图基类 View 派生而来,而 layout_margin 正是 View 的一个通用属性,所以 View 的子子孙孙都能使用 layout_margin。在 View 的大家族中,视图组 ViewGroup 尤为特殊,它既是 View 的子类,又是各类布局的基类。布局下面能容纳其他视图,而控件却不行,这正源自 ViewGroup 的组装特性。View、ViewGroup、控件、布局四者的继承关系如图 3-8 所示。

除了 layout_margin 关系之外,padding 也是 View 的一个通用属性,它用来设置视图的内部间距,并且 padding 也提供了 paddingTop、paddingLeft、paddingRight 四个方向的距离属性。同样是设置间距,layout_margin 指的是当前视图与外部视图(包括上级视图和平级视图)之间的距离,而 padding 指的是当前视图与内部视图(包括下级视图和内部文本)之间的距离。为了观察外部间距和内部间距的差异,接下来做个实验,看看 layout_margin 与 padding 究竟有什么区别。

首先创建新的活动页面,并给该页面的 XML 文件填入以下的布局内容:

(完整代码见 chapter03\src\main\res\layout\activity_view_margin.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="300dp"
    android:background="#00AAFF"
    android:orientation="vertical">

    <!-- 中间层的布局背景为黄色 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="20dp"
        android:background="#FFFF99"
        android:padding="60dp">

        <!-- 最内层的视图背景为红色 -->
        <View
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#FF0000"/>

    </LinearLayout>


</LinearLayout>

上面的 XML 文件有两层视图嵌套,第一层是蓝色背景布局里面放黄色背景布局,第二层是黄色背景布局里面放红色背景视图。中间层的黄色背景布局,同时设置了 20dp 的 layout_margin,以及 60dp 的 padding,其中 padding 是 layout_margin 的三倍宽(60/20=3)。接着运行测试 App,看到的演示界面如图 3-9 所示。

设置视图的对齐方式

App 界面上的视图排列,默认靠左朝上对齐,这也符合日常的书写格式。然而页面的排版不是一成不变的,有时出于美观或者其他原因,要将视图排列改为朝下或靠右对齐,为此需要另外指定视图的对齐方式。在 XML 文件中通过属性 android:layout_gravity 可以指定当前视图的对齐方向,当属性值为 top 时表示视图朝上对齐,为 bottom 时表示视图朝下对齐,为 left 时表示视图靠左对齐,为 right 时表示视图靠右对齐。如果希望视图既朝上又靠左,则用竖线连接 top 与 left,此时属性标记为 android:layout_gravity="top|left";如果希望视图既朝下又靠右,则用竖线连接 bottom 与 right,此时属性标记为 android:layout_gravity="bottom|right"。

注意 layout_gravity 规定的对齐方式,指的是当前视图往上级视图的哪个方向对齐,并非当前视图的内部对齐。若想设置内部视图的对齐方向,则需由当前视图的 android:gravity 指定,该属性一样拥有 top、bottom、left、right 4 种取值及其组合。它与 layout_gravity 的不同之处在于:layout_gravity 设定了当前视图相对于上级视图的对齐方式,而 gravity 设定了下级视图相对于当前视图的对齐方式;前者决定了当前视图的位置,而后者决定了下级视图的位置。

为了进一步分辨 layout_gravity 与 gravity 的区别,接下来做个实验,对某个布局视图同时设置 android:layout_gravity 和 android:gravity 属性,再观察内外视图的对齐情况。下面便是实验用的 XML 文件例子:

(完整代码见 chapter03\src\main\res\layout\activity_view_gravity.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="300dp"
    android:background="#ffff99"
    android:orientation="horizontal">

    <!-- 第一个子布局背景为红色,它在上级视图中朝下对齐,它的下级视图则靠左对齐 -->
    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="200dp"
        android:layout_gravity="bottom"
        android:layout_margin="10dp"
        android:layout_weight="1"
        android:background="#ff0000"
        android:padding="10dp"
        android:gravity="left">

        <!-- 内部视图的宽度和高度都是100dp,且背景色为青色 -->
        <View
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="#00ffff"/>

    </LinearLayout>

    <!-- 第二个子布局背景为红色,它在上级视图中朝上对齐,它的下级视图则靠右对齐 -->
    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="200dp"
        android:layout_gravity="top"
        android:layout_margin="10dp"
        android:layout_weight="1"
        android:background="#ff0000"
        android:padding="10dp"
        android:gravity="right">

        <View
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="#00ffff"/>

    </LinearLayout>
</LinearLayout>

由效果图可见,第一个子布局朝下,并且它的内部视图靠左;而第二个子布局朝上,并且它的内部视图靠右。对比 XML 文件中的 layout_gravity 和 gravity 取值,证明了二者的对齐情况正如之前所言:layout_gravity 决定当前视图位于上级视图的哪个方位,而 gravity 决定了下级视图位于当前视图的哪个方位。

常用布局

本节介绍常见的几种布局用法,包括在某个方向上顺序排列的线性布局,参照其他视图的位置相对排列的相对布局,像表格那样分列显示的网格布局,以及支持通过华东操作拉出更多内容的滚动视图。

线性布局 LinearLayout

前几个小节的例程中,XML 文件用到了 LinearLayout 布局,它的学名为线性布局。顾名思义,线性布局像是用一根线把它的内部视图串起来,故而内部视图之间的排列顺序是固定的,要么从左到右排列,要么从上到下排列。在 XML 文件中,LinearLayout 通过属性 android:orientation 区分两种方向,属性值为 vertical。如果 LinearLayout 标签不指定具体方向,则系统默认该布局为水平方向排列,也就是默认 android:orientation="horizontal"。

下面做个实验,让 XML 的根节点挂着两个线性布局,第一个线性布局采取 horizontal 水平方向,第二个线性布局采取 vertical 垂直方向。然后每个线性布局内部各有两个文本视图,通过观察这些文本视图的排列情况,从而检验线性布局的显示效果。详细的 XML 文件内容如下所示:

(完整代码见 chapter03\src\main\res\layout\activity_linear_layout.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">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="横排第一个"
            android:textSize="17sp"
            android:textColor="#000000"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="横排第二个"
            android:textSize="17sp"
            android:textColor="#000000"/>

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="竖排第一个"
            android:textSize="17sp"
            android:textColor="#000000"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="竖排第二个"
            android:textSize="17sp"
            android:textColor="#000000"/>

    </LinearLayout>

</LinearLayout>

运行测试 App,进入如图 3-11 所示的演示页面,可见 horizontal 为横向排列。vertical 为纵向排列,说明 android:orientation 的方向属性确实奏效了。

除了方向之外,线性布局还有一个权重概念,所谓权重,指的是线性布局的下级视图各自拥有多大比例的宽高。比如一块蛋糕分给两个人吃,可能两人平均分,也可能甲分三分之一,乙分三分之二。两个人平均分的话,先把蛋糕切两半,然后甲分到一半,乙分到另一半,此时甲乙的权重比为 1:1。甲分三分之一、乙分三分之二的话,先把蛋糕平均切成三块,然后甲分到一块,乙分到两块,此时甲乙的权重比为 1:2。就线性布局而言,它自身的尺寸相当于一整块蛋糕,它的下级视图们一起来分这个尺寸蛋糕,有的视图分得多,有的视图分得少。分多少全凭每个视图分到了多大的权重,这个权重在 XML 文件中通过属性 android:layout_weight 来表达。

把线性布局看作蛋糕的话,分蛋糕的甲乙两人就相当于线性布局的下级视图。假设线性布局平均分为左右两块,则甲视图和乙视图的权重比为 1:1,意味着两个下级视图的 layout_weight 属性都是 1。不过视图有宽高两个方向,系统怎知 layout_weight 表示哪个方向的权重呢?所以这里有个规定,一旦设置了 layout_weight 属性值,便要求layout_width 填 0dp 或者 layout_height 填 0dp。如果 layout_width 填 0dp,则 layout_weight 表示水平方向的权重,下级视图会从左往右分割线性布局;如果 layout_height 填 0dp,则 layout_weight 表示垂直方向的权重,下级视图会从上往下分割线性布局。

按照左右均分的话,线性布局设置水平方向 horizontal,且甲乙两视图的 layout_width 都填 0dp,layout_weight 都填 1,此时横排的 XML 片段示例如下:

(完整代码见 chapter03\src\main\res\layout\activity_linear_layout.xml)

xml
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="横排第一个"
        android:textSize="17sp"
        android:textColor="#000000"/>

    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="横排第二个"
        android:textSize="17sp"
        android:textColor="#000000"/>

</LinearLayout>

按照上下均分的话,线性布局设置垂直方向 vertical,且甲乙两视图的 layout_height 都填 0dp,layout_weight 都填 1,此时竖排的 XML 片段示例如下:

xml
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:text="竖排第一个"
        android:textSize="17sp"
        android:textColor="#000000"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:text="竖排第二个"
        android:textSize="17sp"
        android:textColor="#000000"/>

</LinearLayout>

把上面两个片段放到新页面的 XML 文件,其中第一个是横排区域采用红色背景(色值为 ff0000),第二个是竖排区域采用青色背景(色值为 00ffff)。重新运行测试 App,打开演示界面如图 3-12 所示,可见横排区域平均分为左右两块,竖排区域平均分为上下两块。

相对布局 RelativeLayout

线性布局的下级视图是顺序排列着的,另一种相对布局的下级视图位置则由其他视图决定。相对布局名为 RelativeLayout,因为下级视图的位置是相对位置,所以得有具体的参照物才能确定最终位置。如果不设定下级视图的参照物,那么下级视图默认显示在 RelativeLayout 内部的左上角。

用于确定下级视图位置的参照物分两种,一种是与该视图自身平级的视图;另一种是该视图的上级视图(也就是它归属的 RelativeLayout)。综合两种参照物,相对位置在 XML 文件中的属性名称说明见表 3-2。

相对位置的属性取值相对位置说明
layout_toLeftOf当前视图在指定视图的左边
layout_toRightOf当前视图在指定视图的右边
layout_above当前视图在指定视图的上方
layout_below当前视图在指定视图的下方
layout_alignLeft当前视图与指定视图的左侧对齐
layout_alignRight当前视图与指定视图的右侧对齐
layout_alignTop当前视图与指定视图的顶部对齐
layout_alignBottom当前视图与指定视图的底部对齐
layout_centerInParent当前视图在上级视图的中间
layout_centerHorizontal当前视图在上级视图的水平方向居中
layout_centerVertical当前视图在上级视图的垂直方向居中
layout_alignParentLeft当前视图与上级视图的左侧对齐
layout_alignParentRight当前视图与上级视图的右侧对齐
layout_alignParentTop当前视图与上级视图的顶部对齐
layout_alignParentBottom当前视图与上级视图的底部对齐

为了更好地理解上述相对属性的含义,接下来使用 RelativeLayout 及其下级视图进行布局来看看实际效果图。下面是演示相对布局的 XML 文件例子:

(完整代码见 chapter03\src\main\res\layout\activity_relative_layout.xml)

xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="150dp">

    <TextView
        android:id="@+id/tv_center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:background="#ffffff"
        android:text="我在中间"
        android:textColor="#000000"
        android:textSize="11sp" />

    <TextView
        android:id="@+id/tv_center_horizontal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:background="#ffffff"
        android:text="我在水平中间"
        android:textColor="#000000"
        android:textSize="11sp" />

    <TextView
        android:id="@+id/tv_center_vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:background="#ffffff"
        android:text="我在垂直中间"
        android:textColor="#000000"
        android:textSize="11sp" />

    <TextView
        android:id="@+id/tv_parent_left"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:background="#ffffff"
        android:text="我跟上级左边对齐"
        android:textColor="#000000"
        android:textSize="11sp" />

    <TextView
        android:id="@+id/tv_parent_right"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:background="#ffffff"
        android:text="我跟上级右边对齐"
        android:textColor="#000000"
        android:textSize="11sp" />

    <TextView
        android:id="@+id/tv_parent_top"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:background="#ffffff"
        android:text="我跟上级顶部对齐"
        android:textColor="#000000"
        android:textSize="11sp" />

    <TextView
        android:id="@+id/tv_parent_bottom"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:background="#ffffff"
        android:text="我跟上级底部对齐"
        android:textColor="#000000"
        android:textSize="11sp" />

    <TextView
        android:id="@+id/tv_left_center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toLeftOf="@id/tv_center"
        android:layout_alignTop="@id/tv_center"
        android:background="#ffffff"
        android:text="我在中间左边"
        android:textColor="#000000"
        android:textSize="11sp" />

    <TextView
        android:id="@+id/tv_right_center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/tv_center"
        android:layout_alignBottom="@id/tv_center"
        android:background="#ffffff"
        android:text="我在中间右边"
        android:textColor="#000000"
        android:textSize="11sp" />

    <TextView
        android:id="@+id/tv_above_center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@id/tv_center"
        android:layout_alignLeft="@id/tv_center"
        android:background="#ffffff"
        android:text="我在中间上面"
        android:textColor="#000000"
        android:textSize="11sp" />

    <TextView
        android:id="@+id/tv_below_center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/tv_center"
        android:layout_alignRight="@id/tv_center"
        android:background="#ffffff"
        android:text="我在中间下面"
        android:textColor="#000000"
        android:textSize="11sp" />


</RelativeLayout>

上述 XML 文件的布局效果如图 3-13 所示,RelativeLayout 的下级视图都是文本视图,控件上的文字说明了所处的相对位置,具体的控件显示方位正如 XML 属性中描述的那样。

网格布局 GridLayout

虽然线性布局既能在水平方向排列,也能在垂直方向排列,但它不支持多行多列的布局方式,只支持单行(水平排列)或单列(垂直排列)的布局方式。若要实现类似表格那样的多行多列形式,可采用网格布局 GridLayout。

网格布局默认从左往右、从上到下排列,它先从第一行从左往右放置下级视图,塞满之后另起一行防止其余的下级视图,如此循环往复直至所有下级视图都放置完毕。为了判断能够容纳几行几列,网格布局新增了 android:columnCount 与 android:rowCount 两个属性,其中 columnCount 指定了网格的列数,即每行能放多少个视图;rowCount 指定了网格的行数,即每列能放多少个视图。

下面是运用网格布局的 XML 布局样例,它规定了一个两行两列的网格布局,且内部容纳四个文本视图。XML 文件内容如下所示:

(完整代码见 chapter03\src\main\res\layout\activity_grid_layout.xml)

xml
<?xml version="1.0" encoding="utf-8"?>
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:columnCount="2"
    android:rowCount="2">

    <TextView
        android:layout_width="0dp"
        android:layout_height="60dp"
        android:layout_columnWeight="1"
        android:background="#ffcccc"
        android:gravity="center"
        android:text="浅红色"
        android:textColor="#000000"
        android:textSize="17sp" />

    <TextView
        android:layout_width="0dp"
        android:layout_height="60dp"
        android:layout_columnWeight="1"
        android:background="#ffaa00"
        android:gravity="center"
        android:text="橙色"
        android:textColor="#000000"
        android:textSize="17sp" />

    <TextView
        android:layout_width="0dp"
        android:layout_height="60dp"
        android:layout_columnWeight="1"
        android:background="#00ff00"
        android:gravity="center"
        android:text="绿色"
        android:textColor="#000000"
        android:textSize="17sp" />

    <TextView
        android:layout_width="0dp"
        android:layout_height="60dp"
        android:layout_columnWeight="1"
        android:background="#660066"
        android:gravity="center"
        android:text="深紫色"
        android:textColor="#000000"
        android:textSize="17sp" />

</GridLayout>

在一个新建的活动页面加载上述布局,运行 App 观察到的界面如图 3-14 所示。

由图 3-14 可见,App界面的第一行分布着浅红色背景与橙色背景的文本视图,第二行分布着绿色背景与深紫色背景的文本视图,说明利用网格布局实现了多行多列的效果。

滚动视图 ScrollView

手机屏幕的显示控件有限,常常需要上下滑动或左右滑动才能拉出其余页面内容,可惜一般的布局节点都不支持自行滚动,这时就要借助滚动视图了。与线性布局类似,滚动视图也分为垂直方向和水平方向两类,其中垂直滚动视图名为 ScrollView,水平滚动视图名为 horizontalScrollView。这两个滚动视图的使用并不复杂,主要注意以下 3 点:

  1. 垂直方向滚动时,layout_width 属性值设置为 match_parent,layout_height 属性值设置为 wrap_content。
  2. 水平方向滚动时,layout_width 属性值设置为 wrap_content,layout_height 属性值设置为 match_parent。
  3. 滚动视图节点下面必须且只能挂着一个子布局节点,否则会在运行时报错 Caused by:java.lang.IllegalStateException:ScrollView can host only one direct child。

下面是垂直滚动视图 ScrollView 和水平滚动视图 HorizontalScrollView 的 XML 例子:

(完整代码见 chapter03\src\main\res\layout\activity_scroll_view.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">

    <HorizontalScrollView
        android:layout_width="wrap_content"
        android:layout_height="200dp">

        <!-- 水平方向的线性布局,两个子视图的颜色分别为青色和黄色 -->
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:orientation="horizontal">

            <View
                android:layout_width="300dp"
                android:layout_height="match_parent"
                android:background="#aaffff"/>

            <View
                android:layout_width="300dp"
                android:layout_height="match_parent"
                android:background="#ffff00"/>

        </LinearLayout>

    </HorizontalScrollView>

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <!-- 垂直方向的线性布局,两个子视图的颜色分别为绿色和橙色 -->
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <View
                android:layout_width="match_parent"
                android:layout_height="400dp"
                android:background="#00ff00"/>

            <View
                android:layout_width="match_parent"
                android:layout_height="400dp"
                android:background="#ffffaa"/>

        </LinearLayout>

    </ScrollView>

</LinearLayout>

运行测试 App,可知 ScrollView 在纵向滚动,而 HorizontalScrollView 在横向滚动。有时 ScrollView 的实际内容不够,又想让它充满屏幕,怎么办呢?如果把 layout_height 属性赋值为 match_parent,接过还是不会充满,正确的做法是再增加一行属性 android:fillViewport(该属性为 true 表示允许填满视图窗口),属性片段举例如下:

android:layout_height="match_parent"
android:fillViewport="true"

按钮触控

本文介绍了按钮控件的常见用法,包括:如何设置大小写属性与点击属性,如何响应按钮的点击事件和长按事件,如何禁用按钮又该如何启用按钮,等等。

按钮控件 Button

除了文本视图之外,按钮 Button 也是一种基础控件。因为 Button 是由 TextView 派生而来,所以文本视图拥有的属性和方法,包括文本内容、文本大小、文本颜色等,按钮控件均能使用。不同的是,Button 拥有默认的按钮背景,而 TextView 默认无背景;Button 的内部文本默认居中对齐,而 TextView 的内部文本默认靠左对齐。此外,按钮还要额外注意 textAllCaps 与 onClick 两个属性,分别介绍如下:

textAllCaps 属性

对于 TextView 来说,text 属性设置了什么文本,文本视图就显示什么文本。但对于 Button 来说,不管 text 属性设置的是大写字母还是小写字母,按钮控件都默认转成大写字母显示。比如在 XML 文件中加入下面的 Button 标签:

xml
<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:text="下面的按钮英文默认大写"
    android:textColor="@color/black"
    android:textSize="17sp" />

编译运行后的 App 界面,按钮上显示全大写的“HELLO WORLD”,而非原来大小写混合的“Hello World”。显然这个效果不符合预期,为此需要给 Button 标签补充 textAllCaps 属性,该属性默认为 true 表示全部转为大写,如果设置为 false 则表示不转为大写。于是在布局文件添加新的 Button 标签,该标签补充了 android:textAllCaps="false",具体内容如下所示:

(完整代码见 chapter03\src\main\res\layout\activity_button_style.xml)

再次运行 App,此时包含新旧按钮的界面如图 3-15 所示,可见 textAllCaps 果然能够控制大小写转换。

onClick 属性

按钮之所以成为按钮,是因为它会响应按下动作,就手机而言,按下动作等同于点击操作,即手指轻触屏幕然后马上松开。每当点击按钮之时,就表示用户确认了某个事项,接下来轮到 App 接着处理了。onClick 属性便用来接管用户的点击动作,该属性的值是个方法名,也就是当前页面的 Java 代码存在这么一个方法:当用户点击按钮时,就自动调用该方法。

譬如下面的 Buttom 标签指定了 onClick 属性值为 doClick,表示点击该按钮会触发 Java 代码中的 doClick 方法:

(完整代码见 chapter03\src\main\res\layout\activity_button_style.xml)

xml
<Button
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="直接指定点击方法"
    android:textAllCaps="false"
    android:textColor="@color/black"
    android:textSize="17sp"
    android:onClick="doClick"/>

<TextView
    android:id="@+id/tv_result"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="这里查看按钮的点击结果"
    android:textColor="@color/black"
    android:textSize="17sp" />

与之相对应,页面所在的 Java 代码需要增加 doClick 方法,方法代码示例如下:

(完整代码见 chapter03\src\main\java\com\example\chapter03\ButtonStyleActivity.java)

java
public void doClick(View view) {
    String desc = String.format("%s 您点击了按钮: %s", DateUtil.getNowTime(), ((Button) view).getText());
    tv_result.setText(desc);
}

然后编译运行,并在 App 界面上点击新加的按钮,点击前后的界面如图 3-16 和图 3-17 所示,其中图 3-16 为点击之前的界面,图 3-17 为点击之后的界面。

比较图 3-16 和图 3-17 的文字差异,可见点击按钮之后确实调用了 doClick 方法。

点击事件和长按事件

虽然按钮控件能够在 XML 文件中通过 onClick 属性指定点击方法,但是方法的名称可以随便叫,既能叫 doClick 也能叫 doTouch,甚至叫它 doA 或 doB 都没问题,这样很不利于规范化代码,倘若以后换了别人接手,就不晓得 doA 和 doB 是干什么用的。因此在实际开发中,不推荐使用 Button 标签的 onClick 属性,而是在代码中给按钮对象注册点击监听器。

所谓监听器,意思是专门监听控件的动作行为,它平时无所事事,只有控件发生了指定的动作,监听器才会触发开关去执行对应的代码逻辑。点击监听器需要实现接口 View.OnClickListener,并重写 onClick 方法补充点击事件的处理代码,再由按钮调用 setOnClickListener 方法设置监听对象。比如下面的代码给按钮控件 btn_click_single 设置了一个点击监听器:

(完整代码见 chapter03\src\main\java\com\example\chapter03\ButtonClickActivity.java)

java
// 从布局文件中获取名为 btn_click_single 的按钮控件
Button btn_click_single = findViewById(R.id.btn_click_single);
// 给 btn_click_single 设置监听器,一旦用户点击按钮,就触发监听器的 onClick 方法
btn_click_single.setOnClickListener(new MyOnClickListener());

上面的点击监听器名为 MyOnClickListener,它的定义代码示例如下:

java
// 定义一个监听器,它实现了接口 View.OnClickListener
class MyOnClickListener implements View.OnClickListener {
    @Override
    public void onClick(View v) { //点击事件的处理方法
        String desc = String.format("%s 您点击了按钮:%s", DateUtil.getNowTime(), ((Button) v).getText());
        tv_result.setText(desc);
    }
}

接着运行 App,点击按钮之后的界面如图 3-18 所示,可见点击动作的确触发了监听器的 onClick 方法。

如果一个页面只有一个按钮,单独定义新的监听器倒也无妨,可是如果存在许多按钮,每个按钮都定义自己的监听器,那就劳民伤财了。对于同时监听多个按钮的情况,更好的办法是注册统一的监听器,也就是让当前页面实现接口 View.OnClickListener,如此一来,onClick 方法便写在了页面代码之内。因为是统一的监听器,所以 onClick 内部需要判断是哪个按钮被点击了,也就是利用视图对象的 getId 方法检查控件编号,完整的 onClick 代码举例如下:

(完整代码见 chapter03\src\main\java\com\example\chapter03\ButtonClickActivity.java)

java
@Override
public void onClick(View v) { // 点击事件的处理方法
    if (v.getId() == R.id.btn_click_public) { // 来自于按钮 btn_click_public
        String desc = String.format("%s 您点击了按钮:%s", DateUtil.getNowTime(), ((Button) v).getText());
        tv_result.setText(desc); // 设置文本视图的文本内容
    }
}

当然该页面的 onCreate 内部别忘了调用按钮对象的 setOnClickListener 方法,把按钮的点击监听器设置成当前页面,设置代码如下所示:

java
// 从布局文件中获取名为 btn_click_public 的按钮控件
Button btn_click_public = findViewById(R.id.btn_click_public);
// 设置点击监听器,一旦用户点击按钮,就触发监听器的 onClick 方法
btn_click_public.setOnClickListener(this);

重新运行 App,点击第二个按钮之后的界面如图 3-19 所示,可见当前页面的 onClick 方法也正确执行了。

除了点击事件,Android 还设计了另外一种长按事件,每当控件被按住超过 500 毫秒之后,就会触发该控件的长按事件。若要捕捉按钮的长按事件,可调用按钮对象的 setOnLongClickListener 方法设置长按监听器。具体的设置代码示例如下:

(完整代码见 chapter03\src\main\java\com\example\chapter03\ButtonLongclickActivity.java)

java
// 从布局文件中获取名为 btn_click_public 的按钮控件
Button btn_longclick_public = findViewById(R.id.btn_longclick_public);
// 设置长按监听器,一旦用户长按按钮,就触发监听器的 onLongClick 方法
btn_longclick_public.setLongClickListener(this);

以上代码把长按监听器设置到当前页面,意味着该页面需要实现对应的长按接口 View.OnLongClickListener,并重写长按方法 onLongClick,下面便是重写后的 onLongClick 代码例子:

java
@Override
public boolean onLongClick(View v) { // 长按事件的处理方法
    if (v.getId() == R.id.btn_longclick_public) { // 来自于按钮
        String desc = String.format("%s 您长按了按钮:%s", DateUtil.getNowTime(), ((Button) v).getText());
        tv_result.setText(desc); // 设置文本视图的文本内容
        return true;
    }
}

再次运行 App,长按按钮之后的界面如图 3-20 所示,说明长按事件果然触发了 onLongClick 方法。

值得注意的是,点击监听器和长按监听器不局限于按钮控件,其实它们都来源于视图基类 View,凡是 View 派生而来的各类控件,均可注册点击监听器和长按监听器。譬如文本视图 TextView,其对象也能调用 setOnClickListener 方法与 setOnLongClickListener 方法,此时 TextView 控件就会响应点击动作和长按动作。因为按钮存在按下和松开两种背景,便于提示用户该控件允许点击,但文本视图默认没有按压背景,不方便判断是否被点击,所以一般不会让文本视图处理点击事件和长按事件。

禁用与恢复按钮

尽管按钮控件生来就是给人点击的,可是某些情况希望暂时禁止点击操作,譬如用户在注册的时候,有的网站要求用户必须统一指定条款,而且至少浏览 10 秒之后才能点击注册按钮。那么在 10 秒之前,注册按钮应该置灰且不能点击,等过了 10 秒之后,注册按钮才恢复正常。在这样的业务场景中,按钮先后拥有两种状态,即不可用状态与可用状态,它们在外观和功能上的区别如下:

  1. 不可用按钮:按钮不允许点击,既是点击也没反应,同时按钮文字为灰色。
  2. 可用按钮:按钮允许点击,点击按钮会触发点击事件,同时按钮文字为正常的黑色。

从上述的区别说明可知,不可用与可用状态主要有两点差异:其一,是否允许点击;其二,按钮文字的颜色。就文字颜色而言,可在布局文件中使用 textColor 属性设置颜色,也可在 Java 代码中调用 setTextColor 方法设置颜色。至于是否允许点击,则需引入新属性 android:enabled,该属性值为 true 时表示启用按钮,即允许点击按钮;该属性值为 false 时表示禁用按钮,即不允许点击按钮。在 Java 代码中,则可通过 setEnabled 方法设置按钮的可用状态(true 表示启用,false 表示禁用)。

接下来通过一个例子演示按钮的启用和禁用操作。为了改变测试按钮的可用状态,需要额外添加两个控制按钮,分别是“启用测试按钮”和“禁用测试按钮”,加起来一共 3 个按钮控件,注意“测试按钮”默认是灰色文本。测试界面的布局效果如图 3-21 所示。

与图 3-21 对应的布局文件内容如下所示:

(完整代码见 chapter03\src\main\res\layout\activity_button_enable.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">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btn_enable"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="启用测试按钮"
            android:textColor="#000000"
            android:textSize="17sp" />

        <Button
            android:id="@+id/btn_disable"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="禁用测试按钮"
            android:textColor="#000000"
            android:textSize="17sp" />

    </LinearLayout>

    <Button
        android:id="@+id/btn_test"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:enabled="false"
        android:text="测试按钮"
        android:textColor="#888888"
        android:textSize="17sp" />

    <TextView
        android:id="@+id/tv_result"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="这里查看测试按钮的点击结果"
        android:textColor="#000000"
        android:textSize="17sp" />

</LinearLayout>

然后在 Java 代码中给 3 个按钮分别注册点击监听器,注册代码如下所示:

(完整代码见 chapter03\src\main\java\com\example\chapter03\ButtonEnableActivity.java)

java
// 因为按钮控件的 setOnClickListener 方法来源于 View 基类,所以也可对 findViewById 得到的视图直接设置点击监听器
Button btn_enable = findViewById(R.id.btn_enable);
Button btn_disable = findViewById(R.id.btn_disable);
btn_test = findViewById(R.id.btn_test);
tv_result = findViewById(R.id.tv_result);

btn_enable.setOnClickListener(this);
btn_disable.setOnClickListener(this);
btn_test.setOnClickListener(this);

同时重写页面的 onClick 方法,分别处理 3 个按钮的点击事件,修改之后的 onClick 代码示例如下:

java
@Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.btn_enable:
            // 启用当前控件
            btn_test.setEnabled(true);
            // 设置按钮的文字颜色
            btn_test.setTextColor(Color.BLACK);
            break;
        case R.id.btn_disable:
            // 禁用当前控件
            btn_test.setEnabled(false);
            btn_test.setTextColor(Color.GRAY);
            break;
        case R.id.btn_test:
            String desc = String.format("%s 您点击了按钮: %s", DateUtil.getNowTime(), ((Button) v).getText());
            tv_result.setText(desc);
            break;
    }
}

最后编译运行 App,点击了“启用测试按钮”之后,原本置灰的测试按钮 btn_test 恢复正常的黑色文本,点击该按钮发现界面有了反应,具体效果如图 3-22 所示。

对比图 3-21 和图 3-22,观察按钮启用前后的文官及其是否响应点击动作,即可知晓禁用按钮和启用按钮两种模式的差别。

图像显示

本节介绍了与图像显示有关的几种控件用法,包括:专门用于显示图片的图像视图以及若干缩放类型效果,支持显示图片的按钮控件——图像按钮,如何在按钮控件上同时显示文本和图标等。

图像视图 ImageView

显示文本用到了文本视图 TextView,显示图像则用到图像视图 ImageView。由于图像通常保存为单独的图片文件,因此需要先把图片放到 res/drawable 目录,然后再去引用该图片的资源名称。比如现在有张苹果图片名为 apple.png,那么 XML 文件通过属性 android:src 设置图片资源,属性值格式如“@drawable/不含扩展名的图片名称”。添加了 src 属性的 ImageView 标签示例如下:

(完整代码见 chapter03\src\main\res\layout\activity_image_scale.xml)

xml
<ImageView
    android:id="@+id/iv_scale"
    android:layout_width="match_parent"
    android:layout_height="220dp"
    android:layout_marginTop="5dp"
    android:src="@drawable/apple"/>

若想在 Java 代码中设置图像视图的图片资源,可调用 ImageView 控件的 setImageResource 方法,方法参数格式形如“R.drawable.不含扩展名的图片名称”。仍以上述的苹果图片为例,给图像视图设置图片资源的代码例子如下所示:

(完整代码见 chapter03\src\main\java\com\example\chapter03\ImageScaleActivity.java)

java
// 从布局文件中获取名为 iv_scale 的图像视图
ImageView iv_scale = findViewById(R.id.iv_scale);
iv_scale.setImageResource(R.drawable.apple); // 设置图像视图的图片资源

运行测试 App,展示图片的界面效果如图 3-23 所示。

观察效果图发现苹果图片居中显示,而非文本视图里的文字那样默认靠左显示,这是怎么回事?原来 ImageView 本身默认图片居中显示,不管图片有多大亦或有多小,图像视图都会自动缩放图片,使之刚好够着 ImageView 的边界,并且缩放后的图片保持原始的宽高比例,看起来图片很完美地占据视图中央。这种缩放类型在 XML 文件中通过属性 android:scaleType 定义,即使图像视图未明确指定该属性,系统也会默认其值为 fitCenter,表示让图像缩放后居中显示。添加了缩放属性的 ImageView 标签如下所示:

xml
<ImageView
    android:id="@+id/iv_scale"
    android:layout_width="match_parent"
    android:layout_height="220dp"
    android:src="@drawable/apple"
    android:scaleType="fitCenter"/>

在 Java 代码中可调用 setScaleType 方法设置图像视图的缩放类型,其中 fitCenter 对应的类型为 ScaleType.FIT_CENTER,设置代码示例如下:

java
// 将缩放类型设置为“保持宽高比例,缩放图片使其位于视图中间”
iv_scale.setScaleType(ImageView.ScaleType.FIT_CENTER);

除了居中显示,图像视图还提供了其他缩放类型,详细的缩放类型取值说明见表 3-3。

XML 中的缩放类型ScaleType 类中的缩放类型说明
fitCenterFIT_CENTER保持宽高比例,缩放图片使其位于视图中间
centerCropCENTER_CROP缩放图片使其充满视图(超出部分会被裁剪),缩小不放大
centerCENTER保持图片原尺寸,并使其位于视图中间
fitXYFIT_XY缩放图片使其正好填满视图(图片可能被拉伸变形)
fitStartFIT_START保持宽高比例,缩放图片使其位于视图上方或左侧
fitEndFIT_END保持宽高比例,缩放图片使其位于视图下方或右侧

注意居中显示 fitCenter 是默认的缩放类型,它的图像效果如之前的图 3-23 所示。其余缩放类型的图像显示效果分别如图 3-24 到图 3-29 所示,其中图 3-24 为 centerCrop 的效果图,图 3-25 为 centerInside 的效果图,图 3-26 为 center 的效果图,图 3-27 为 fitXY 的效果图,图 3-28 为 fitStart 的效果图,图 3-29 为 fitEnd 的效果图。

注意到 centerInside 和 center 的小时效果居然都一模一样,这缘于它们的缩放规则设定。表面上 fitCenter、centerInside、center 三个类型都是居中显示,且均不越过图像视图的边界。它们之间的区别在于:fitCenter 既允许缩小图片、也允许放大图片,centerInside 只允许缩小图片、不允许放大图片,而 center 自始至终保持原始尺寸(既不允许缩小图片、也不允许放大图片)。因此,当图片尺寸大于视图宽高,centerInside 与 fitCenter 都会缩小图片,此时它俩的显示效果相同;当图片尺寸小于视图宽高,centerInside 与 center 都保持图片大小不变,此时它俩的显示效果相同。

图像按钮 ImageButton

常见的按钮控件 Button 其实是文本按钮,因为按钮上面只能显示文字,不能显示图片,ImageButton 才是显示图片的图像按钮。虽然 ImageButton 号称图像按钮,但它并非继承 Button,而是继承了 ImageView,所以凡是 ImageView 拥有的属性和方法,ImageButton 统统拿了过来,区别在于 ImageButton 有个按钮背景。

尽管 ImageButton 源自 ImageView,但它毕竟是个按钮呀,按钮家族常用的点击事件和长按事件,ImageButton全都没落下。不过 ImageButton 和 Button 之间除了名称不同,还有下列差异:

  • Button 既可显示文本也可显示图片(通过 setBackgroundResource 方法设置背景图片),而 ImageButton 只能显示图片不能显示文本。
  • ImageButton 上的图像可按比例缩放,而 Button 通过背景设置的图像会拉伸变形,因为背景图采取 fitXY 方式,无法按比例缩放。
  • Button 只能靠背景显示一张图片,而 ImageButton 可分别在前景和背景显示图片,从而实现两张图片叠加的效果。

从上面可以看出,Button 与 ImageButton 各有千秋,通常情况使用 Button 就够用了。但在某些场合,比如输入法打不出来的字符,以及特殊字体显示的字符串,就适合先切图再放到 ImageButton。举个例子,数学常见的开平方运算,由输入法打出来的运算符号为“√”,但该符号缺少右上角的一横,正确的开平方符号是带横线的,此时便需要通过 ImageButton 显示这个开方图片。

不过使用 ImageButton 得注意,图像按钮默认的缩放类型为 center(保持原始尺寸不缩放图片),而非图像的视图默认的 fitCenter,倘若图片尺寸较大,那么图像按钮将无法显示整个图片。为避免显示不完整的情况,XML 文件中的 ImageButton 标签必须指定 fitCenter 的缩放类型,详细的标签内容示例如下:

(完整代码见 chapter03\src\main\res\layout\activity_image_button.xml)

xml
<ImageButton
    android:layout_width="match_parent"
    android:layout_height="80dp"
    android:src="@drawable/sqrt"
    android:scaleType="fitCenter" />

运行测试 App,打开演示界面如图 3-30 所示,可见图像按钮正确展示了开平方符号。

同时展示文本与图像

现在有了 Button 可在按钮上显示文字,又有 ImageButton 可在按钮上显示图像,照理说绝大多数场合都够用了。然而现实项目中的需求往往捉摸不定,例如客户要求在按钮文字的左边加一个图标,这样按钮内部既有文字又有图片,乍看之下 Button 和 ImageButton 都没法直接使用。若用 LinearLayout 对 ImageView 和 TextView 组合布局,虽然可行,XML 文件却变得冗长许多。

其实有个既简单又灵活的办法,要想在文字周围防止图片,使用按钮控件 Button 就能实现。原来 Button 悄悄提供了几个与图标有关的属性,通过这些属性即可指定文字旁边的图标,以下是有关的图标属性说明。

  • drawableTop:指定文字上方的图片。
  • drawableBottom:指定文字下方的图片。
  • drawableLeft:指定文字左边的图片。
  • drawableRight:指定文字右边的图片。
  • drawablePadding:指定图片与文字的间距。

譬如下面是个既有文字又有图标的 Button 标签例子:

(完整代码见 chapter03\src\main\res\layout\activity_image_text.xml)

xml
<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:drawableLeft="@drawable/ic_about"
    android:drawablePadding="5dp"
    android:text="图标在左"
    android:textSize="17sp" />

以上的 Button 标签通过属性 android:drawableTop 设置了文字上边的图标,若想变更图标所处的位置,只要把 drawableTop 换成对应方向的属性即可。各方向的图文混排按钮效果分别如图 3-31 到图 3-34 所示,其中图 3-31 为指定了 drawableTop 的按钮界面,图 3-32 为指定了 drawableBottom 的按钮界面,图 3-33 为指定了 drawableLeft 的按钮界面,图 3-34 为指定了drawableRight 的按钮界面。

实战项目:计算器

本章虽然只学了一些 Android 的简单控件,但是只要活学善用这些布局和控件,也能够做出实用的 App。接下来让我们尝试设计并实现一个简单计算器。

需求描述

计算器是人们日常生活中最常用的工具之一,无论在电脑上还是手机上,都少不了计算器的身影。以 Windows 系统自带的计算器为例,它的界面简洁且十分实用,如图 3-35 所示。

计算器的界面分为两大部分,第一部分是上方的计算表达式,既包括用户的按键输入,也包括计算结果数字;第二部分是下方的各个按键,例如:从 0 到 9 的数字按钮、加减乘除与等号、正负号按钮、小数点按钮、求倒数按钮、平方按钮,以及退格、清空、取消等控制按钮。通过这些按键操作,能够实现整数和小数的四则运算,以及求倒数、求平方、求开方等简单运算。

界面设计

上一小节介绍的 Windows 计算器,它主要由上半部分的计算结果与下半部分的计算按钮两块区域组成,据此可创建一个界面相似的计算器 App,同样由计算结果和计算按钮两部分组成,如图 3-36 所示。

按照计算器 App 的效果图,大致分布着下列 Android 控件:

  • 线性布局 LinearLayout:因为计算器界面整体从上往下布局,所以需要垂直方向的 LinearLayout。
  • 网格布局 GridLayout:计算器下半部分的几排按钮,正好成五行四列表格分布,适合采用 GridLayout。
  • 滚动视图 ScrollView:虽然计算器界面不宽也不高,但是以防万一,最好还是加个垂直方向的 ScrollView。
  • 文本视图 TextView:很明显顶部标题“简单计算器”就是 TextView,且文字居中显示;标题下面的计算结果也需要使用 TextView,且文字靠右靠下显示。
  • 按钮 Button:几乎所有的数字与运算符按钮都采用了 Button 控件。
  • 图像按钮 ImageButton:开根号的运算符“√”虽然能够打出来,但是右上角少了数学课本上的一横,所以该按钮要显示一张标准的开根号图片,这用到了 ImageButton。

关键代码

App 同用户交互的过程中,市场要向用户反馈一些信息,例如:点错了按钮、输入了非法字符等等,注入此类。对于这些一句话的提示,Android 设计了 Toast 控件,用于展示短暂的提示文字。Toast 的用法很简单,只需以下一行代码即可弹出提示小窗:

java
Toast.makeText(MainActivity.this, "提示文字", Toast.LENGTH_SHORT).show();

上面代码用到了两个方法,分别是 makeText 和 show,其中 show 方法用来展示提示窗,而 makeText 方法用来构建提示文字的模板。makeText 的第一个参数为当前页面的实例,倘若当前页面名为 MainActivity 的话,这里就填 MainActivity.this,当然如果不引发歧义的话,直接填 this 也可以;第二个参数为准备显示的提示文本;第三个参数规定了提示窗的驻留时长,为 Toast.LENGTH_SHORT 标识停留 2 秒后消失,为 Toast.LENGTH_LONG 表示停留 3.5 秒后消失。

对于计算器来说,有好几种情况需要提示用户,比如“被除数不能为零”、“开根号的数值不能小于零”、“不能对零求倒数”等等,这时就能通过 Toast 控件弹窗提醒用户。Toast 弹窗的展示效果如图 3-37 所示,此时 App 发现了被除数为零的情况。

对于简单计算来说,每次运算至少需要两个操作数,比如加减乘除四则运算就要求有两个操作数,求倒数、求平方、求开方只要求一个操作数;并且每次运算过程有且仅有一个运算符(等号不计在内),故而计算器 App 得事先声明下列几个字符串变量:

java
// 第一个操作数
private String firstNum = "";
// 运算符
private String operator = "";
// 第二个操作数
private String secondNum = "";
// 当前的计算结果
private String result = "";
// 显示的文本内容
private String showText = "";

用户在计算器界面每输入一个按键,App 都要进行下列两项操作:

  1. 输入按键的合法性校验

在开展计算之前,务必检查用户的按键是否合法,因为非法按键将导致不能正常运算。非法的按键输入包括但不限于下列情况:

  • 被除数不能为零。
  • 开根号的数值不能小于零。
  • 不能对零求倒数。
  • 一个数字不能有两个小数点。
  • 如果没输入运算符,就不能点击等号按钮。
  • 如果没输入操作数,也不能点击等号按钮。

比如点击等号按钮之时,App 的逻辑校验代码示例如下:

(完整代码见 chapter03\src\main\java\com\example\chapter03\CalculatorActivity.java)

java
if (v.getId() == R.id.btn_equal) { // 点击了等号按钮
    if ("".equals(operator)) { // 无运算符
        Toast.makeText(this, "请输入运算符", Toast.LENGTH_SHORT).show();
        return false;
    }
    if ("".equals(firestNum) || "".equals(secondNum)) { // 无操作数
        Toast.makeText(this, "请输入数字", Toast.LENGTH_SHORT).show();
        return false;
    }
    if ("÷".equals(operator) && Double.parseDouble(secondNum) == 0) { // 除数为零
        Toast.makeText(this, "除数不能为零", Toast.LENGTH_SHORT).show();
        return false;
    }
}
  1. 执行运算并显示计算结果

合法性校验通过,方能继续接下来的业务逻辑,倘若用户本次未输入与计算有关的按钮(例如等号、求倒数、求平方、求开方),则计算器只需拼接操作数或者运算符;倘若用户本次输入了与计算有关的按钮(例如等号、求倒数、求平方、求开方),则计算器立即执行运算操作并显示计算结果。以加减乘除四则运算为例,它们的计算代码例子如下所示:

java
// 加减乘除四则运算,返回计算结果
private double calculateFour() {
    switch (operator) {
        case "+":
            return Double.parseDouble(firstNum) + Double.parseDouble(secondNum);
        case "-":
            return Double.parseDouble(firstNum) - Double.parseDouble(secondNum);
        case "×":
            return Double.parseDouble(firstNum) * Double.parseDouble(secondNum);
        default: {
            return Double.parseDouble(firstNum) / Double.parseDouble(secondNum);
        }
    }
}

小结

本章主要介绍了 App 开发中常见简单控件的用法,包括:在文本视图上显示文本(设置文本的内容、大小和颜色)、修改视图的基本属性(设置视图的宽高、间距和对齐方式)、运用各种布局排列控件(线性布局、相对布局、网格布局、滚动视图)、处理按钮的触控事件(按钮控件的点击、长按、禁用与恢复)、在图像控件上显示图片(图像视图、图像按钮、同时展示文本与图像)。最后设计了一个实战项目“简单计算器”,在该项目的 App 编码中用到了前面介绍的大部分控件和布局,从而加深了对所学知识的理解。

通过本章的学习,读者应该能掌握以下 4 种开发技能:

  1. 学会在文本控件上正确展示文字。
  2. 学会在图像控件上正确展示图片。
  3. 学会正确处理按钮的点。
  4. 学会在常见布局上排列组合多个控件。