Skip to content
<

中级控件 ​

本章介绍 App 开发常见的几类中级控件的用法,主要包括:如何定制几种简单的图形、如何使用几种选择按钮、如何高效地输入文本、如何利用对话框获取交互信息等,然后结合本章所学的知识,演示了几个实战项目“找回密码”的设计与实现。

图形定制 ​

本节介绍 Android 图形的基本概念和几种常见图形的使用办法,包括:形状图形的组成结构及其具体用法、九宫格图片(点九图片)的制作过程及其适用场景、状态列表图形的产生背景及其具体用法。

图形 Drawable ​

Android 把所有能够显示的图形都抽象为 Drawable 类(可绘制的)。这里的图形不只是图片,还包括色块、画板、背景等。

包含图片在内的图形文件放在 res 目录的各个 drawable 目录下,其中 drawable 目录一般保存描述性的 XML 文件,而图片文件一般放在具体分辨率的 drawable 目录下。例如:

  • drawable-ldpi 里面存放低分辨率的图片(如 240x320),现在基本没有这样的智能手机了。
  • drawable-mdpi 里面存放中等分辨率的图片(如 320x480),这样的智能手机已经很少了。
  • drawable-hdpi 里面存放高分辨率的图片(如 480x800),一般对应 4 英寸~4.5 英寸的手机(但不绝对),同尺寸的手机有可能分辨率不同,手机分辨率就高不就低,因为分辨率低了屏幕会有模糊的感觉。
  • drawable-xhdpi 里面存放加高分辨率的图片(如 720x1280),一般对应 5 英寸~5.5 英寸的手机。
  • drawable-xxhdpi 里面存放超高分辨率的图片(如 1080x1920),一般对应 6 英寸~6.5 英寸的手机。
  • drawable-xxxhdpi 里面存放超超高分辨率的图片(如 1440x2560),一般对应 7 英寸以上的平板电脑。

基本上,分辨率每加大一级,宽度和高度就要增加二分之一或三分之一像素。如果各目录存在同名图片,Android 就会根据手机的分辨率分别适配对应文件夹里的图片。在开发 App 时,为了兼容不同的手机屏幕,在各目录存放不同分辨率的图片,才能达到最合适的显示效果。例如,在 drawable-hdpi 放了一张背景图片 bg.png(分辨率为 480x800),其他目录没放,使用分辨率为 480x8000 的手机查看该 App 界面没有问题,但是使用分辨率为 720x1280 的手机查看该 App 会发现背景图片有点模糊,原因是 Android 为了让 bg.png 适配高分辨率的屏幕,强行把 bg.png 拉伸到了 720x1280,拉伸的后果是图片变模糊了。

在 XML 布局文件中引用图形文件可使用“@drawable/不含扩展名的文件名称”这种形式,如各视图的 background 属性、ImageView 和 ImageButton 的 src 属性、TextView 和 Button 四个方向的 drawable*** 系列属性都可以引用图形文件。

形状图形 ​

Shape 图形又称形状图形,它用来描述常见的几何形状,包括矩形、圆角矩形、圆形、椭圆等。用好形状图形可以让 App 页面不再呆板,还可以节省美工不少工作量。

形状图形的定义文件放在 drawable 目录下,它是以 shape 标签为根节点的 XML 描述文件。根节点下定义了 6 个节点,分别是: size(尺寸)、stroke(描边)、corners(圆角)、solid(填充)、padding(间隔)、gradient(渐变),各节点的属性值主要是长宽、半径、角度以及颜色等。下面是形状图形各个节点及其属性的简要说明。

shape(形状) ​

shape 是形状图形文件的根节点,它描述了当前是哪种几何图形。下面是 shape 节点的常用属性说明。

  • shape:字符串类型,表示图形的形状。形状类型的取值说明见表 5-1。
形状类型说明
rectangle矩形。默认值
oval椭圆。此时 corners 节点会失效
line直线。此时必须设置 stroke 节点,不然会报错
ring圆环

size(尺寸) ​

size 是 shape 的下级节点,它描述了形状图形的宽高尺寸。若无 size 节点,则表示宽高与宿主视图一样大小。下面是 size 节点的常用属性说明。

  • height:像素类型,图形高度。
  • width:像素类型,图形宽度。

stroke(描边) ​

stroke 是 shape 的下级节点,它描述了形状图形的描边规格。若无 stroke 节点,则表示不存在描边。下面是 stroke 节点的常用属性说明。

  • color:颜色类型,描边的颜色。
  • dashGap:像素类型,每段虚线之间的间隔。
  • dashWidth:像素类型,每段虚线的宽度。若 dashGap 和 dashWidht 有一个值为 0,则描边为实线。
  • width:像素类型,描边的厚度。

corners(圆角) ​

corners 是 shape 的下级节点,它描述了形状图形的圆角大小。若无 corners 节点,则表示没有圆角。下面是 corners 节点的常用属性说明。

  • bottomLeftRadius:像素类型,左下圆角的半径。
  • bottomRightRadius:像素类型,右下圆角的半径。
  • topLeftRadius:像素类型,左上圆角的半径。
  • topRightRadius:像素类型,右上圆角的半径。
  • radius:像素类型,4 个圆角的半径(若有上面 4 个圆角半径的定义,则不需要 radius 定义)。

solid(填充) ​

solid 是 shape 的下级节点,它描述了形状图形的填充色彩。若无 solid 节点,则表示无填充颜色。下面是 solid 节点的常用属性说明。

  • color:颜色类型,内部填充的颜色。

padding(间隔) ​

padding 是 shape 的下级节点,它描述了形状图形与周围边界的间隔。若无 padding 节点,则表示四周不设间隔。下面是 padding 节点的常用属性说明。

  • top:像素类型,与上方的间隔。
  • bottom:像素类型,与下方的间隔。
  • left:像素类型,与左边的间隔。
  • right:像素类型,与右边的间隔。

gradient(渐变) ​

gradient 是 shape 的下级节点,它描述了形状图形的颜色渐变。若无 gradient 节点,则表示没有渐变效果。下面是 gridient 节点的常用属性说明。

  • angle:整型,渐变的起始角度。为 0 时表示时钟的 9 点位置,值增大表示往逆时针方向旋转。例如,值为 90 表示 6 点位置,值为 180 表示 3 点位置,值为 270 表示 0 点 /12 点位置。
  • type:字符串类型,渐变类型。渐变类型的取值说明见表 5-2。

|渐变类型|说明| |linear|线性渐变,默认值| |radial|放射渐变,起始颜色就是圆心颜色| |sweep|滚动渐变,即一个线段以某个断电为圆心做 360 度旋转|

  • centerX:浮点型,圆心的 X 坐标。当 android:type="linear" 时不可用。
  • centerY:浮点型,圆心的 Y 坐标。当 android:type="linear" 时不可用。
  • gradientRadius:整型,渐变的半径。当 android:type="radial" 时需要设置该属性。
  • centerColor:颜色类型,渐变的中间颜色。
  • startColor:颜色类型,渐变的起始颜色。
  • endColor:颜色类型,渐变的终止颜色。
  • useLevel:布尔类型,设置 true 为无渐变色、false 为有渐变色。

在实际开发中,形状图形主要使用 3 个节点:stroke(描边)、corners(圆角)和 solid(填充)。至于 shape 根节点的属性一般不用设置(默认矩形即可)。

接下来演示一下形状图形的界面效果,首先右击 drawable 目录,并依次选择右键菜单的 New->Drawable resource file,在弹窗中输入文件名称再单击 OK 按钮,即可自动生成一个 XML 描述文件。往该文件填入下面的圆角矩形内容定义:

(完整代码见 chapter05\src\main\res\drawable\shape_rect_gold.xml)

xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- 指定了形状内部的填充颜色 -->
    <solid android:color="#ffdd66" />
    <!-- 指定了形状轮廓的粗细与颜色 -->
    <stroke
        android:width="1dp"
        android:color="#aaaaaa" />
    <!-- 指定了形状四个圆角的半径 -->
    <corners android:radius="10dp" />

</shape>

接着创建一个测试页面,并在页面的 XML 文件中添加名为 v_content 的 View 标签,再给 Java 代码补充以下的视图背景设置代码:

(完整代码见 chapter05\src\main\java\com\example\chapter05\DrawableShapeActivity.java)

java
// 从布局文件中获取名为 v_content 的视图
View v_content = findViewById(R.id.v_content);
// v_content 的背景设置为圆角矩形
v_content.setBackgroundResource(R.drawable.shape_rect_gold);

然后运行测试 App,观察到对应的形状图形如图 5-1 所示。该形状为一个圆角矩形,内部填充色为黄土色,边缘线为灰色。

再来一个椭圆的 XML 描述文件示例如下:

(完整代码见 chapter05\src\main\res\drawable\shape_oval_rose.xml)

xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">

    <!-- 指定了形状内部的填充颜色 -->
    <solid android:color="#ff66aa" />

    <!-- 指定了形状轮廓的粗细与颜色 -->
    <stroke
        android:width="1dp"
        android:color="#aaaaaa" />

</shape>

把前述的视图对象 v_content 背景改为 R.drawable.shape_oval_rose,运行 App 观察到对应的形状图形如图 5-2 所示。该形状为一个椭圆,内部填充色为玫红色,边缘线为灰色。

九宫格图片 ​

将某张图片设置成视图背景时,如果图片尺寸太小,则系统会自动拉伸图片使之填满背景。可是一旦图片拉得过大,其画面容易变得模糊,如图 5-3 所示,上面按钮的背景图片被拉得很宽,此时左右两边的边缘线既变宽又变模糊了。

为了解决这个问题,Android 专门设计了点九图片。点九图片的扩展名是 png,文件名后面常带有“.9”字样。因为该图片划分了 3x3 的九宫格区域,所以得名点九图片,也叫九宫格图片。如果背景是一个形状图形,其 stroke 节点的 width 属性已经设置了固定数值(如 1dp),那么无论该图形被拉到多大,描边宽度始终是 1dp。点九图片的实现原理与之类似,即拉伸图形时,只拉伸内部区域,不拉伸边缘线条。

为了掩饰九宫格图片的展示效果,要利用 Android Studio 制作一张点九图片。首先在 drawable 目录下找到代加工的原始图片 button_pressed_orig.png,右击它弹出右键菜单如图 5-4 所示。

选择右键菜单下面的“Create 9-Patch files...”,并在随后弹出的对话框中单击 OK 按钮。接着 drawable 目录自动生成一个名为“button_pressed_orig.9.png”的图片,双击该文件,主界面右侧弹出如图 5-5 所示的点九图片的加工窗口。

注意图 5-5 的左侧窗口是图片加工区域,右侧窗口是图片预览区域,从上到下依次是纵向拉伸预览、横向拉伸预览、两方向同时拉伸预览。在左侧窗口图片四周的马赛克处单击会出现一个黑点,把黑点左右或上下拖动会拖出一段黑线,不同方向上的黑线表示不同的效果。

如图 5-6 所示,界面上边的黑线指的是水平方向的拉伸区域。水平方向拉伸图片时,只有黑线区域内的图像会拉伸,黑线以外的图像保持原状,从而保证左右两侧的边框厚度不变。

如图 5-7 所示,界面左边的黑线指的是垂直方向的拉伸区域。垂直方向拉伸图片时,只有黑线区域内的图像会拉伸,黑线以外的图像保持原状,从而保证上下两侧的边框厚度不变。

如图 5-8 所示,界面下边的黑线指的是该图片作为控件背景时,控件内部的文字左右边界只能放在黑线区域内。这里 Horizontal Padding 的效果就相当于 android:paddingLeft 与 android:paddingRight。

如图 5-9 所示,界面右边的黑线指的是该图片作为控件背景时,空间内部的文字上下边界只能放在黑线区域内。这里 Vertical Padding 的效果就相当于 android:paddingTop 与 android:paddingBottom。

尤其注意,如果点九图片被设置为视图背景,且该图片指定了 Horizontal Padding 和 Vertical Padding,那么视图内部将一直与视图边缘保持固定间距,无论怎么调整 XML 文件和 Java 代码都无法缩小间隔,缘由是点九图片早已在水平和垂直方向都设置了 padding。

状态列表图形 ​

常见的图形文件一般为静态图形,但有时会用到动态图形,比如按钮控件的背景在正常情况下是凸起的,在按下时是凹陷的,从按下到弹起的过程,用户便晓得点击了该按钮。根据不同的触摸情况变更图形状态,这种情况用到了 Drawable 的一个子类 StateListDrawable(状态列表图形),它在 XML 文件中规定了不同状态时候所呈现的图形列表。

接下来演示一下状态列表图形的界面效果,右击 drawable 目录,并依次选择右键菜单的 New->Drawable resource file,在弹窗中输入文件名称再单击 OK 按钮,即可自动生成一个 XML 描述文件。往该文件填入下面的状态列表图形定义:

(完整代码见 chapter05\src\main\res\drawable\btn_nine_selector.xml)

xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:drawable="@drawable/button_pressed" android:state_pressed="true" />
    <item android:drawable="@drawable/button_normal" />

</selector>

上述 XML 文件的关键点是 state_pressed 属性,该属性表示按下状态,值为 true 表示按下时显示 button_pressed 图像,其余情况显示 button_normal 图像。

为方便理解,接下来做个实验,首先将按钮控件的 background 属性设置为 @drawable/btn_nine_selector,然后在屏幕上点击该按钮,观察发现按下时候的界面如图 5-10 所示,而松开时候的界面如图 5-11 所示,可见按下与松开果然显示不同的图片。

状态列表图形不仅用于按钮控件,还可用于其他拥有多种状态的控件,这取决于开发者在 XML 文件中指定了哪种状态类型。各种状态类型的取值说明详见表 5-3。

状态类型的属性名称说明适用的控件
state_pressed是否按下按钮 Button
state_checked是否勾选复选框 CheckBox、单选按钮 RadioButton
state_focused是否获取焦点文本编辑框 EditText
state_selected是否选中各控件通用

选择按钮 ​

本节介绍几个常用的特殊控制按钮,包括:如何使用复选框 CheckBox 及其勾选监听器、如何使用开关按钮 Switch、如何借助状态列表图形实现仿 iOS 的开关按钮、如何使用单选按钮 RadioButton 和单选组 RadioGroup 及其选中监听器。

复选框 CheckBox ​

在学习复选框之前,先了解一下 CompoundButton。在 Android 体系中,CompoundButton 类是抽象的复合按钮,因为是抽象类,所以它不能直接使用。实际开发中用的是 CompoundButton 的几个派生类,主要有复选框 CheckBox、单选按钮 RadioButton 以及开关按钮 Swtich,这些派生类均可使用 CompoundButton 的属性和方法。加之 CompoundButton 本身继承了 Button 类,故以上几种按钮同时具备 Button 的属性和方法,它们之间的继承关系如图 5-12 所示。

CompoundButton 在 XML 文件中主要使用下面两个属性。

  • checked:指定按钮的勾选状态,true 表示勾选,false 则表示未勾选。默认为未勾选。
  • button:指定左侧勾选图标的图形资源,如果不指定就使用系统的默认图标。

CompoundButton 在 Java 代码中主要使用下列 4 种方法。

  • setChecked:设置按钮的勾选状态。
  • setButtonDrawable:设置左侧勾选图标的图形资源。
  • setOnCheckedChangeListener:设置勾选状态变化的监听器。
  • isChecked:判断按钮是否勾选。

复选框 CheckBox 是 CompoundButton 一个最简单的实现控件,点击复选框将它勾选,再次点击取消勾选。复选框对象调用 setOnCheckedChangeListener 方法设置勾选监听器,这样在勾选和取消勾选时就会触发监听器的勾选事件。

接下来演示复选框的操作过程,首先编写活动页面的 XML 文件如下所示:

(完整代码见 chapter05\src\main\res\layout\activity_check_box.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"
    android:padding="5dp">

    <CheckBox
        android:id="@+id/ck_system"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="5dp"
        android:checked="false"
        android:text="这是系统的CheckBox"
        android:textSize="17sp" />

    <CheckBox
        android:id="@+id/ck_custom"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:button="@drawable/checkbox_selector"
        android:checked="true"
        android:padding="5dp"
        android:text="这个CheckBox换了图标" />

</LinearLayout>

接着便血对应的 Java 代码,主要是如何处理勾选监听器,具体代码如下所示:

(完整代码见 chapter05\src\main\java\com\example\chapter05\CheckBoxActivity.java)

java
package com.dongnaoedu.chapter05;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.CheckBox;
import android.widget.CompoundButton;

public class CheckBoxActivity extends AppCompatActivity implements CompoundButton.OnCheckedChangeListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_check_box);
        CheckBox ck_system = findViewById(R.id.ck_system);
        CheckBox ck_custom = findViewById(R.id.ck_custom);

        ck_system.setOnCheckedChangeListener(this);
        ck_custom.setOnCheckedChangeListener(this);
    }

    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        String desc = String.format("您%s了这个CheckBox", isChecked ? "勾选" : "取消勾选");
        buttonView.setText(desc);
    }
}

然后运行测试 App,一开始的演示界面如图 5-13 所示,此时复选框默认未勾选。首次点击复选框,此时复选框的图标及文字均发生变化,如图 5-14 所示;再次点击复选框,此时复选框的图标及文字又发生变化,如图 5-15 所示;可见先后出发了勾选与取消勾选事件。

开关按钮 Switch ​

Switch 是开关按钮,它像一个高级版本的 CheckBox,在选中与取消选中时可展现的界面元素比复选框丰富。Switch 控件新添加的 XML 属性说明如下:

  • textOn:设置右侧开启时的文本。
  • textOff:设置左侧关闭时的文本。
  • track:设置开关轨道的背景。
  • thumb:设置开关标识的图标。

虽然开关按钮是升级版的复选框,但它在实际开发中用得不多。原因之一是大家觉得 Switch 的默认界面不够大气,如图 5-16 和图 5-17 所示,小巧的开关图标显得有些拘谨;原因之二是大家觉得 iPhone 的界面很漂亮,无论用户还是客户,都希望 App 实现 iOS 那样的控件风格,于是 iOS 的开关 UISwitch 就成了安卓开发者仿照的对象。

现在要让 Android 实现类似 iOS 的开关按钮,主要思路是借助状态列表图形,首先创建一个图形专用的 XML 文件,给状态列表指定选中与未选中时候的开关图标,如下所示:

(完整代码见 chapter05\src\main\res\drawable\switch_selector.xml)

xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:drawable="@drawable/switch_on" android:state_checked="true" />
    <item android:drawable="@drawable/switch_off" />

</selector>

然后把 CheckBox 标签的 background 属性设置为@drawable/switch_selector,同时将 button 属性设置为@null。完整的 CheckBox 标签内容示例如下:

(完整代码见 chapter05\src\main\res\layout\activity_switch_ios.xml)

xml
<CheckBox
    android:id="@+id/ck_status"
    android:layout_width="60dp"
    android:layout_height="30dp"
    android:layout_gravity="end"
    android:background="@drawable/switch_selector"
    android:button="@null" />

为什么这里修改 background 属性,而不直接修改 button 属性呢?因为 button 属性有局限,无论多大的图片,都只显示一个小小的图标,可是小小的图标一点都不大气,所以这里必须使用 background 属性,要它有多大就能有多大,这才够炫够酷。

最后看看这个仿 iOS 开关按钮的效果,分别如图 5-18 和图 5-19 所示。这下开关按钮脱胎换骨,又圆又鲜艳,比原来的 Switch 好看了很多。

单选按钮 RadioButton ​

所谓单选按钮,指的是在一组按钮中选择其中一项,并且不能多选,这要求有个容器确定这组按钮的范围,这个容器便是单选组 RadioGroup。单选组实质上是个布局,同一组 RadioGroup 都要放在同一个 RadioGroup 节点下。RadioGroup 提供了 orientation 属性指定下级控件的排列方向,该属性为 horizontal 时,单选按钮在水平方向排列;该属性为 vertical 时,单选按钮在垂直方向排列。RadioGroup 下面除了 RadioButton,还可以挂载其他子控件(如 TextView、ImageView 等)。如此看来,单选组相当于特殊的线性布局,它们主要有以下两个区别:

  1. 单选组多了管理单选按钮的功能,而线性布局不具备该功能。
  2. 如果不指定 orientation 属性,那么单选组默认垂直排列,而线性布局默认水平排列。

下面是 RadioGroup 在 Java 代码中的 3 个常用方法。

  • check:选中指定资源编号的单选按钮。
  • getCheckedRadioButtonId:获取已选中单选按钮的资源编号。
  • setOnCheckedChangeListener:设置单选按钮勾选变化的监听器。

与 CheckBox 不同的是,RadioButton 默认未选中,点击后显示选中,但是再次点击不会取消选中。只有点击同组的其他单选按钮时,原来选中的单选按钮才会取消选中。另需注意,单选按钮的选中事件不是由 RadioButton 处理,而是由 RadioGroup 处理。

接下来演示单选按钮的操作过程,首先编写活动页面的 XML 文件如下所示:

(完整代码见 chapter05\src\main\res\layout\ activity_radio_horizontal.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"
    android:padding="5dp">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="请选择您的性别" />

    <RadioGroup
        android:id="@+id/rg_gender"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <RadioButton
            android:id="@+id/rb_male"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="男" />

        <RadioButton
            android:id="@+id/rb_female"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="女" />

    </RadioGroup>

    <TextView
        android:id="@+id/tv_result"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp" />
</LinearLayout>

接着编写对应的 Java 代码,主要是如何处理选中监听器,具体代码如下所示:

java
public class RadioHorizontalActivity extends AppCompatActivity implements RadioGroup.OnCheckedChangeListener {

    private TextView tv_result;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_radio_horizontal);
        RadioGroup rb_gender = findViewById(R.id.rg_gender);
        tv_result = findViewById(R.id.tv_result);
        rb_gender.setOnCheckedChangeListener(this);
    }

    @Override
    public void onCheckedChanged(RadioGroup group, int checkedId) {
        switch (checkedId){
            case R.id.rb_male:
                tv_result.setText("哇哦,你是个帅气的男孩");
                break;
            case R.id.rb_female:
                tv_result.setText("哇哦,你是个漂亮的女孩");
                break;
        }
    }
}

然后运行测试 App,一开始的演示界面如图 5-20 所示,此时两个单选按钮均未选中。先点击左边的单选按钮,此时左边按钮显示选中状态,如图 5-21 所示;再点击右边的单选按钮,此时右边按钮显示选中状态,同时左边按钮取消选中,如图 5-22 所示;可见果然实现了组内只能选中唯一按钮的单选功能。

文本输入 ​

本节介绍如何再编辑框 EditText 上高效地输入文本,包括:如何改变编辑框的控件外观,如何利用焦点变更监听器提前校验输入位数,如何利用文本变化监听器自动关闭软键盘。

编辑框 EditText ​

编辑框 EditText 用于接收软键盘输入的文字,例如用户名、密码、评价内容等,它由文本视图派生而来,除了 TextView 已有的各种属性和方法,EditText 还支持下列 XML 属性。

  • inputType:指定输入的文本类型。输入类型的取值说明见表 5-4,若同时使用多种文本类型,则可使用竖线“|”把多种文本类型拼接起来。
  • maxLength:指定文本允许输入的最大长度。
  • hint:指定提示文本的内容。
  • textColorHint:指定提示文本的颜色。
输入类型说明
text文本
textPassword文本密码。显示时用圆点“·”代替
number整型数
numberSigned带符号的数字。允许在开头带负号“-”
numberDecimal数字密码。显示时用圆点“·”代替
datetime时间日期格式。除了数字外,还允许输入横线、斜杠、空格、冒号
date日期格式。除了数字外,还允许输入横线“-”和斜杠“/”
time时间格式。除了数字外,还允许输入冒号“:”

接下来通过 XML 布局观看编辑框界面效果,演示用的 XML 文件内容如下:

(完整代码见 chapter05\src\main\res\layout\activity_edit_simple.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"
    android:padding="5dp">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="下面是登录信息" />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入用户名"
        android:inputType="text" />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入密码"
        android:inputType="textPassword" />
</LinearLayout>

运行测试 App,进入初始的编辑框页面如图 5-23 所示。然后往用户名编辑框输入文字,输满 10 个字后发现不能再输入,于是切换到密码框继续输,知道输满 8 位密码,此时编辑框页面如图 5-24 所示。

根据以上图示可知编辑框的各属性正常工作,不过编辑框有根下划线,未输入时显示灰色,正在输入时显示红色,这种效果是怎么实现的呢?其实下划线没用到新属性,而用了已有的背景属性 background;至于未输入与正在输入两种情况的颜色差异,乃是因为使用了状态列表图形,编辑框获得焦点时(正在输入)显示红色的下划线,其余时候显示灰色下划线。当然 EditText 默认的下划线背景不甚好看,下面将利用状态列表图形将编辑框背景改为更加美观的圆角矩形。

首先编写圆角矩形的形状图形文件,它的 XML 定义文件示例如下:

(完整代码见 chapter05\src\main\res\drawable\shape_edit_normal.xml)

xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- 指定了形状内部的填充颜色 -->
    <solid android:color="#ffffff" />

    <!-- 指定了形状轮廓的粗细与颜色 -->
    <stroke
        android:width="1dp"
        android:color="#aaaaaa" />

    <!-- 指定了形状四个圆角的半径 -->
    <corners android:radius="5dp" />

    <!-- 指定了形状四个方向的间距 -->
    <padding
        android:bottom="2dp"
        android:left="2dp"
        android:right="2dp"
        android:top="2dp" />

</shape>

上述的 shape_edit_normal.xml 定义了一个灰色的圆角矩形,可在未输入时展示该形状。正在输入时候的形状要改为蓝色的圆角矩形,其中轮廓线条的色值从 aaaaaa(灰色)改成 0000ff(蓝色),具体定义放在 shape_edit_focus.xml。

接着编写编辑框背景的状态列表图形文件,主要在 selector 节点下添加两个 item,一个 item 设置了获得焦点时刻(android:state_focused="true")的图形为@drawable/shape_edit_focus;另一个 item 设置了图形@drawable/shape_edit_normal但未指定任何状态,标识其他情况都展示该图形。完整的状态列表图形定义示例如下:

(完整代码见 chapter05\src\main\res\drawable\editext_selector.xml)

xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:drawable="@drawable/shape_edit_focus" android:state_focused="true" />
    <item android:drawable="@drawable/shape_edit_normal" />

</selector>

然后编写测试页面的 XML 布局文件,一共添加 3 个 EditText 标签,第一个 EditText 采用默认的编辑框背景;第二个 EditText 将 background 属性值设置为 @null,此时编辑框不显示任何背景;第三个 EditText 将 background 属性值设为 @drawable/editext_selector,其背景由 editext_selector.xml 所定义的状态列表图形决定。详细的 XML 文件内容如下所示:

(完整代码见 chapter05\src\main\res\layout\activity_edit_border.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"
    android:padding="5dp">

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="这是默认边框"
        android:inputType="text" />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:background="@null"
        android:hint="我的边框不见了"
        android:inputType="text" />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:background="@drawable/editext_selector"
        android:hint="我的边框是圆角"
        android:inputType="text" />
</LinearLayout>

最后运行测试 App,更换背景之后的编辑框界面如图 5-25 所示,可见第三个编辑框的背景成功变为了圆角矩形边框。

焦点变更监听器 ​

虽然编辑框 EditText 提供了 maxLength 属性,用来设置可输入文本的最大长度,但是它没提供对应的 minLength 属性,也就无法设置可输入文本的最小长度。譬如手机号码为固定的 11 位数字,用户必须输满 11 位才是合法的,然而编辑框不会自动检查手机号码是否达到 11 位,既是用户少输一位只输入十位数字,编辑框依然认为这是合法的手机号。比如图 5-26 所示的登陆页面,有手机号码编辑框,有密码编辑框,还有登录按钮。

既然编辑框不会自动校验手机号是否达到 11 位,势必要求代码另行检查。一种想法是在用户点击登录按钮时再判断,不过通常此时已经输完手机号与密码,为啥不能再输入密码之前就判断手机号的位数呢?早点检查可以帮助用户早点发现错误,特别是表单元素较多的时候,更能改善用户的使用体验。就上面的登录例子而言,手机号编辑框下方为密码框,那么能否给密码框注册点击事件,以便在用户准备输入密码时就校验手机号的位数呢?

然而实际运行 App 却发现,先输入手机号码再输入密码,一开始并不会出发密码框的点击事件,再次点击密码框才会触发点击事件。缘由是编辑框比较特殊,要点及两次后才会触发点击事件,因为第一次点击只触发焦点变更事件,第二次点击才触发点击事件。编辑框的所谓焦点,直观上就看那个山东的光标,哪个编辑框有光标,焦点就落在哪里。光标在编辑框之间切换,便产生了焦点变更事件,所以对于编辑框来说,应当注册焦点变更监听器,而非注册点击监听器。

焦点变更监听器来自于接口 View.OnFocusChangeListener,若想注册该监听器,就要调用编辑框对象的 setOnFocusChangeListener 方法,即可在光标切换之时(获得光标和失去光标)触发焦点变更事件。下面是给密码框注册焦点变更监听器的代码例子:

(完整代码见 chapter05\src\main\java\com\example\chapter05\EditFocusActivity.java)

java
EditText et_password = findViewById(R.id.et_password);
et_password.setOnFocusChangeListener(this);

以上代码把焦点变更监听器设置到当前页面,则需让活动页面实现接口 View.OnFocusChangeListener,并重写该接口定义的 onFocusChange 方法,判断如果是密码框获得焦点,就检查输入的手机号码是否达到 11 位。具体的焦点变更处理方法如下所示:

java
@Override
public void onFocusChange(View v, boolean hasFocus) {
    if (hasFocus) {
        String phone = et_phone.getText().toString();
        // 手机号码不足11位
        if (TextUtils.isEmpty(phone) || phone.length() < 11) {
            // 手机号码编辑框请求焦点,也就是把光标移回手机号码编辑框
            et_phone.requestFocus();
            Toast.makeText(this, "请输入11位手机号码", Toast.LENGTH_SHORT).show();
        }
    }
}

改好代码重新运行 App,当手机号不足 11 位时点击密码框,界面底部果然弹出了相应的提示文字,如图 5-27 所示,并且光标依然留在手机号码编辑框,说明首次点击密码框的确触发了焦点变更事件。

文本变化监听器 ​

输入法的软键盘往往会遮住页面下半部分,使得“登录”“确认”“下一步”等按钮看不到了,用户若想点击这些按钮还得再点依次返回键才能关闭软键盘。为了方便用户操作,最好在满足特定条件时自动关闭软键盘,比如手机号码输入满 11 位后自动关闭软键盘,又如密码输入满 6 位后自动关闭软键盘,另一个是如何判断已输入的文字达到指定位数,分别说明如下。

如何关闭软键盘 ​

诚然按下返回键就会关闭软键盘,但这是系统自己关闭的,而非开发者在代码中关闭。因为输入法软键盘由系统服务 INPUT_METHOD_SERVICE 管理,所以关闭软键盘也要由该服务处理,下面是使用系统服务关闭软键盘的代码例子:

(完整代码见 chapter05\src\main\java\com\example\chapter05\util\ViewUtil.java)

java
public static void hideOneInputMethod(Activity act, View v) {
    // 从系统服务中获取输入法管理器
    InputMethodManager imm = (InputMethodManager) act.getSystemService(Context.INPUT_METHOD_SERVICE);
    // 关闭屏幕上的输入法软键盘
    imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
}

注意上述代码里面的视图对象 v,虽然控件类型为 View,但它必须是 EditText 类型才能正常关闭软键盘。

如何判断已输入的文字达到指定位数 ​

该功能点要求实时监控当前已输入的文本长度,这个监控操作用到文本监听器接口 TextWatcher,该接口提供了 3 个监控方法,具体说明如下:

  • beforeTextChanged:在文本改变之前触发。
  • onTextChanged:在文本改变过程中触发。
  • afterTextChanged:在文本改变之后触发。

具体到编码实现,需要自己写个监听器实现 TextWatcher 接口,再调用编辑框对象的 addTextChangedListener 方法注册文本监听器。监听操作建议在 afterTextChanged 方法中完成,如果同时监听 11 位的手机号码和 6 位的密码,一旦输入文字达到指定长度就关闭键盘,则详细的监听器代码如下所示:

(完整代码见 chapter05\src\main\java\com\example\chapter05\EditHideActivity.java)

java
// 定义一个编辑框监听器,在输入文本达到指定长度时自动隐藏输入法
private class HideTextWatcher implements TextWatcher {
    // 声明一个编辑框对象
    private EditText mView;
    // 声明一个最大长度变量
    private int mMaxLength;

    public HideTextWatcher(EditText v, int maxLength) {
        this.mView = v;
        this.mMaxLength = maxLength;
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {

    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {

    }

    // 在编辑框的输入文本变化后触发
    @Override
    public void afterTextChanged(Editable s) {
        // 获得已输入的文本字符串
        String str = s.toString();
        // 输入文本达到11位(如手机号码),或者达到6位(如登录密码)时关闭输入法
        if (str.length() == mMaxLength) {
            // 隐藏输入法软键盘
            ViewUtil.hideOneInputMethod(EditHideActivity.this, mView);
        }
    }
}

写好文本监听器代码,还要给手机号码编辑框和密码编辑框分别注册监听器,注册代码示例如下:

java
EditText et_phone = findViewById(R.id.et_phone);
EditText et_password = findViewById(R.id.et_password);

et_phone.addTextChangedListener(new HideTextWatcher(et_phone, 11));
et_password.addTextChangedListener(new HideTextWatcher(et_password, 6));

然后允许测试 App,先输入手机号码的前 10 位,因为还没达到 11 位,所以软键盘依然展示,如图 5-28 所示。接着输入最后一位手机号,总长度达到 11 位,于是软键盘自动关闭,如图 5-29 所示。

对话框 ​

本节介绍几种常用的对话框控件,包括:如何使用提醒对话框处理不同的选项,如何使用日期对话框获取用户选择的日期,如何使用时间对话框获取用户选择的时间。

提醒对话框 AlertDialog ​

AlertDialog 名为提醒对话框,它是 Android 中最常用的对话框,可以完成常见的交互操作,例如提示、确认、选择等功能。由于 AlertDialog 没有公开的构造方法,因此必须借助建造器 AlertDialog.Builder 才能完成参数设置,AlertDialog.Builder 的常用方法说明如下。

  • setIcon:设置对话框的标题图标。
  • setTitle:设置对话框的标题文本。
  • setMessage:设置对话框的内容文本。
  • setPositiveButton:设置肯定按钮的信息,包括按钮文本和点击监听器。
  • setNegativeButton:设置否定按钮的信息,包括按钮文本和点击监听器。
  • setNeutralButton:设置中性按钮的信息,包括按钮文本和点击监听器,该方法比较少用。

通过 AlertDialog.Builder 设置完对话框参数,还需调用建造起的 create 方法才能生成对话框实例。最后调用对话框实例的 show 方法,在页面上弹出提醒对话框。

下面是构建并显示提醒对话框的 java 代码例子:

(完整代码见 chapter05\src\main\java\com\example\chapter05\AlertDialogActivity.java)

java
// 创建提醒对话框的建造器
AlertDialog.Builder builder = new AlertDialog.Builder(this);
// 设置对话框的标题文本
builder.setTitle("尊敬的用户");
// 设置对话框的内容文本
builder.setMessage("你真的要卸载我吗?");
// 设置对话框的肯定按钮文本及其点击监听器
builder.setPositiveButton("残忍卸载", (dialog, which) -> {
    tv_alert.setText("虽然依依不舍,但是只能离开了");
});
// 设置对话框的否定按钮文本及其点击监听器
builder.setNegativeButton("我再想想", (dialog, which) -> {
    tv_alert.setText("让我再陪你三百六十五个日夜");
});

// 根据建造器构建提醒对话框对象
AlertDialog dialog = builder.create();
// 显示提醒对话框
dialog.show();

提醒对话框的弹窗效果如图 5-30 所示,可见该对话框有标题和内容,还有两个按钮。

点击不同的对话框按钮会触发不同的处理逻辑。例如,图 5-31 为点击“我再想想”按钮后的页面,图 5-32 为点击“残忍卸载”按钮后的页面。

日期对话框 DatePickerDialog ​

虽然 EditText 提供了inputType="date"的日期输入,但是很少有人会手工输入完整日期,况且 EditText 还不支持xx 年 xx 月 xx 日这样的中文日期,所以系统提供了专门的日期选择器 DatePicker,供用户选择具体的年月日。不过,DatePicker 并非弹窗模式,而是在当前页面章句一块区域,并且不会自动关闭。按习惯来说,日期控件应该弹出对话框,选择完日期就要自动关闭对话框。因此,很少直接在界面上显示 DatePicker,而是利用已经废庄好的日期选择对话框 DatePickerDialog。

DatePickerDialog 相当于在 AlertDialog 上装载了 DatePicker,编码时只需调用构造方法设置当前的年、月、日,然后调用 show 方法即可弹出日期对话框。日期选择时间则由监听器 OnDateSetListener 负责响应,在该监听器的 onDateSet 方法中,开发者获取用户选择的具体日期,再做后续处理。特别注意 onDateSet 的月份参数,它的起始值不是 1 而是 0.也就是说,一月份对应的参数值为 0,十二月份对应的参数值为 11,中间月份的数值以此类推。

在界面上内嵌显示 DatePicker 的效果如图 5-33 所示,其中,年、月、日通过上下滑动选择。单独弹出日期对话框的效果如图 5-34 所示,其中年、月、日按照日历风格展示。

java
public class DatePickerActivity extends AppCompatActivity implements View.OnClickListener, DatePickerDialog.OnDateSetListener {

    private DatePicker dp_date;
    private TextView tv_date;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_date_picker);
        findViewById(R.id.btn_ok).setOnClickListener(this);
        findViewById(R.id.btn_date).setOnClickListener(this);
        tv_date = findViewById(R.id.tv_date);
        dp_date = findViewById(R.id.dp_date);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_ok:
                String desc = String.format("您选择的日期是%d年%d月%d日", dp_date.getYear(), dp_date.getMonth() + 1, dp_date.getDayOfMonth());
                tv_date.setText(desc);
                break;
            case R.id.btn_date:
                // 获取日历的一个实例,里面包含了当前的年月日
                /*Calendar calendar = Calendar.getInstance();
                calendar.get(Calendar.YEAR);
                calendar.get(Calendar.MONTH);
                calendar.get(Calendar.DAY_OF_MONTH);*/
                DatePickerDialog dialog = new DatePickerDialog(this, this, 2090, 5, 11);
                // 显示日期对话框
                dialog.show();
                break;
        }
    }

    @Override
    public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) {
        String desc = String.format("您选择的日期是%d年%d月%d日", year, month + 1, dayOfMonth);
        tv_date.setText(desc);
    }
}

时间对话框 TimePickerDialog ​

既然有了日期选择器,还得有对应的时间选择器。同样,实际开发中也很少直接用 TimePicker,而是用封装好的时间选择对话框 TimePickerDialog。该对话框的用法类似 DatePickerDialog,不同之处主要有两个:

  1. 构造方法传的是当时的小时与分钟,最后一个参数表示是否采取 24 小时制,一般为 true 表示小时的数值范围为 0~23;若为 false 则表示采取 12 小时制。
  2. 时间选择监听器为 OnTimeSetListener,对应需要实现 onTimeSet 方法,在该方法中可获得用户选择的小时和分钟。

在界面上内嵌显示 TimePicker 的效果如图 5-35 所示,其中,小时与分钟可通过上下滑动选择。单独弹出时间对话框的效果如图 5-36 所示,其中小时与分钟按照钟表风格展示。

下面是使用时间对话框的 Java 代码例子,包括弹出时间对话框和处理时间监听事件:

(完整代码见 chapter05\src\main\java\com\example\chapter05\TimePickerActivity.java)

java
package com.dongnaoedu.chapter05;

import androidx.appcompat.app.AppCompatActivity;

import android.app.TimePickerDialog;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.TimePicker;

import java.util.Calendar;

public class TimePickerActivity extends AppCompatActivity implements View.OnClickListener, TimePickerDialog.OnTimeSetListener {

    private TimePicker tp_time;
    private TextView tv_time;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_time_picker);
        findViewById(R.id.btn_ok).setOnClickListener(this);
        findViewById(R.id.btn_time).setOnClickListener(this);
        tp_time = findViewById(R.id.tp_time);
        tp_time.setIs24HourView(true);
        tv_time = findViewById(R.id.tv_time);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_ok:
                String desc = String.format("您选择的时间是%d时%d分", tp_time.getHour(), tp_time.getMinute());
                tv_time.setText(desc);
                break;
            case R.id.btn_time:
                // 获取日历的一个实例,里面包含了当前的时分秒
                Calendar calendar = Calendar.getInstance();

                // 构建一个时间对话框,该对话框已经集成了时间选择器。
                TimePickerDialog dialog = new TimePickerDialog(this, this,
                        calendar.get(Calendar.HOUR_OF_DAY),
                        calendar.get(Calendar.MINUTE),
                        true); // true表示24小时制,false表示12小时制
                dialog.show();
                break;
        }
    }

    @Override
    public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
        String desc = String.format("您选择的时间是%d时%d分", hourOfDay, minute);
        tv_time.setText(desc);
    }
}

实战项目:找回密码 ​

在移动互联网时代,用户是每家 IT 企业最宝贵的资源,对于 App 而言,吸引用户注册并登录是万分紧要之事,因为用户登陆之后才有机会产生商品交易。登录校验通常是用户名+密码组合,可是每天总有部分用户忘记密码,为此要求 App 提供找回密码的功能,如何简化密码找回步骤,同时兼顾安全性,就是一个值得认真思考的问题。

需求描述 ​

各家电商 App 的登录页面大同小异,要么是用户名与密码组合登录,要么是手机号码与验证码组合登录,若是做好一点的,则会提供找回密码与记住密码等功能。先来看一下登录页面是什么样,因为有两种组合登录方式,所以登陆页面也分成两个效果图。如图 5-37 所示,这是选中密码登录时的界面;如图 5-38 所示,这是选中验证码登录时的界面。

从以上两个登陆效果图可以看到,密码登录与验证码登录的界面主要存在以下几点区别:

  1. 密码输入框和验证码输入框的左侧标题以及输入框内部的提示语各不相同。
  2. 如果是密码登录,则需要支持找回密码;如果是验证码登录,则需要支持想用户手机发送验证码。
  3. 密码登录可以提供记住密码功能,而验证码的数值每次都不一样,无须也没法记住验证码。

对于找回密码功能,一般直接跳到找回密码页面,在该页面输入和确认新密码,并校验找回密码的合法性(通过短信验证码检查),据此勾勒出密码找回页面的轮廓概貌,如图 5-39 所示。

在找回密码的操作过程当中,为了更好地增强用户体验,有必要在几个关键节点处提醒用户。比如成功发送验证码之后,要及时提示用户注意查收短信,这里暂且做成提醒对话框的形式,如图 5-40 所示。又比如密码登录成功之后,也要告知用户已经修改成功登录,注意继续后面的操作,登录成功的提示弹窗如图 5-41 所示。

真是想不到,原来简简单单的一个登录功能,就得考虑这么多的需求场景。可是仔细想想,这些需求场景都是必要的,其目的是为了让用户能够更加便捷地顺利登录。正所谓“台上十分钟,台下十年功”,每个好用的 App 背后,都离不开开发者十年如一日的辛勤工作。

界面设计 ​

用户登录与找回密码看似简单,用到的控件却不少。按照之前的界面效果图,大致从上到下、从左到右分布着下列 Android 控件:

单选按钮 RadioButton:用来区分是密码登录还是验证码登录。

  • 文本视图 TextView:输入框左侧要显示此处还是验证码登录。
  • 编辑框 EditText:用来输入手机号码、密码和验证码。
  • 复选框 CheckBox:用于判断是否记住密码。
  • 按钮 Button:除了“登录”按钮,还有“忘记密码”和“获取验证码”两个按钮。
  • 线性布局 LinearLayout:整体界面从上往下排列,用到了垂直方向的线性布局。
  • 相对布局 RelativeLayout:忘记密码的按钮与密码输入框是叠加的,且“忘记密码”与上级视图右对齐。
  • 单选组 RadioGroup:密码登录和验证码登录这两个单选按钮,需要放在单选组之中。
  • 提醒对话框 AlertDialog:为了演示方便,获取验证码与登录成功都通过提醒对话框向用户反馈结果。

另外,由于整个登录模块由登录页面和找回密码页面组成,因此这两个页面之间需要进行数据交互,也就是在页面跳转之时传递参数。譬如,从登陆页面跳到找回密码页面,要携带唯一标示的手机号码作为请求参数,不然密码找回页面不知道要给哪个手机号码修改密码。同时,从找回密码页面回到登陆页面,也要将修改之后的新密码作为应答参数传回去,否则登陆页面不知道密码被改成什么了。

关键代码 ​

为了方便读者更好地完成登录页面与找回密码页面,下面列举几个重要功能的代码片段:

关于自动清空错误的密码 ​

这里有个细微的用户体验问题:用户会去找回密码,肯定是发现输入的密码不对;那么修改密码后回到登录页面,如果密码框里还是刚才的错误密码,用户只能先清空错误密码,然后才能输入新密码。一个 App 要想让用户觉得好用,就得急用户之所急,想用户之所想,像刚才那个错误密码的情况,应当由 App 在返回登录页面时自动清空原来的错误密码。

自动清空密码框的操作,放在 onActivityResult 方法中处理是个办法,但这样有个问题,如果用户直接按返回键回到登录页面,那么 onActivityResult 方法发现数据为空便不做处理。因此应该这么处理:判断当前是否为返回页面动作,只要是从找回密码页面返回到当前页面,则不管是否携带应答参数,都要自动清空密码输入框。对应的 Java 代码则为重写登录页面的 onRestart 方法,在该方法中强制清空密码。这样一来,不管用户是修改密码完成回到登录页,还是点击返回键回到登录页,App 都会自动清空密码框了。

下面是重写 onRestart 方法之后的代码例子:

(完整代码见 chapter05\src\main\java\com\example\chapter05\LoginMainActivity.java)

java
@Override
protected void onRestart() {
    super.onRestart();
    et_password.setText("");
}

关于自动隐藏输入法面板 ​

在输入手机号码或者密码的时候,屏幕下方都会弹出输入法面板,供用户按键输入数字和字母。但是输入法面板往往占据屏幕下方大块空间,很是碍手碍脚,用户输入完 11 位的手机号码时,还得再按一下返回键来关闭输入法面板,接着才能继续输入密码。理想的做法是:一旦用户输完 11 位手机号码,App 就要自动隐藏输入法。同理,一旦用户输完 6 位密码或者 6 位验证码,App 也要自动隐藏输入法。要想让 App 具备这种只能的判断功能,就得给文本编辑框添加监听器,只要当前编辑框输入文本长度达到 11 位或者 6 位,App 就自动隐藏输入法面板。

下面是实现自动隐藏软键盘的监听器代码例子:

(完整代码见 chapter05\src\main\java\com\example\chapter05\LoginMainActivity.java)

java
// 定义一个编辑框监听器,在输入文本达到指定长度时自动隐藏输入法
private class HideTextWatcher implements TextWatcher {
    private EditText mView;
    private int mMaxLength;

    public HideTextWatcher(EditText v, int maxLength) {
        this.mView = v;
        this.mMaxLength = maxLength;
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {

    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {

    }

    @Override
    public void afterTextChanged(Editable s) {
        if (s.toString().length() == mMaxLength) {
            // 隐藏输入法软键盘
            ViewUtil.hideOneInputMethod(LoginMainActivity.this, mView);
        }
    }
}

关于密码修改的校验操作 ​

由于密码对于用户来说是很重要的信息,因此必须认真校验新密码的合法性,务必做到万无一失才行。具体的密码修改校验可分作下列 4 个步骤:

  1. 新密码和确认输入的新密码都要是 6 位数字。
  2. 新密码和确认输入的新密码必须保持一致。
  3. 用户输入的验证码必须和系统下发的验证码一致。
  4. 密码修改成功,携带修改后的新密码返回登录页面。

根据以上的校验步骤,对应的代码逻辑示例如下:

(完整代码见 chapter05\src\main\java\com\example\chapter05\LoginForgetActivity.java)

java
String password_first = et_password_first.getText().toString();
String password_second = et_password_second.getText().toString();
if (password_first.length() < 6) {
    Toast.makeText(this, "请输入正确的密码", Toast.LENGTH_SHORT).show();
    return;
}

if (!password_first.equals(password_second)) {
    Toast.makeText(this, "两次输入的新密码不一致", Toast.LENGTH_SHORT).show();
    return;
}

if (!mVerifyCode.equals(et_verifycode.getText().toString())) {
    Toast.makeText(this, "请输入正确的验证码", Toast.LENGTH_SHORT).show();
    return;
}

Toast.makeText(this, "密码修改成功", Toast.LENGTH_SHORT).show();
// 以下把修改好的新密码返回给上一个页面
Intent intent = new Intent();
intent.putExtra("new_password", password_first);
setResult(Activity.RESULT_OK, intent);
finish();

小结 ​

本章主要介绍了 App 开发中的中级控件的相关知识,包括:定制简单的图形(图形的基本概念、形状图形、九宫格图片、状态列表图形)、操纵几种选择按钮(复选框 CheckBox、开关按钮 Switch、单选按钮 RadioButton)、高效地输入文本(编辑框 EditText、焦点变更监听器、文本变化监听器)、获取对话框的选择结果(提醒对话框 AlertDialog、日期对话框 DatePickerDialog、时间对话框 TimePickerDialog)。最后设计了一个实战项目“找回密码”,在该项目的 App 编码中用到了前面介绍的大部分控件,从而加深了对所学知识的理解。

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

  1. 学会定制几种简单的图形。
  2. 学会操纵常见的选择按钮。
  3. 学会高效且合法地输入文本。
  4. 学会通过对话框获取用户选项。