图形定制
图形Drawable
Android把所有能够显示的图形都抽象为Drawable类(可绘制的)。这里的图形不止是图片,还包括色块、画板、背景等。
包含图片在内的图形文件放在res目录的各个drawable目录下,其中drawable目录一般保存描述性的XML文件,而图片文件一般放在具体分辨率的drawable目录下。例如:
- drawable-ldpi里面存放低分辨率的图片(如240×320),现在基本没有这样的智能手机了。
- drawable-mdpi里面存放中等分辨率的图片(如320×480),这样的智能手机已经很少了。
- drawable-hdpi里面存放高分辨率的图片(如480×800),一般对应4英寸!4.5英寸的手机(但不绝对,同尺寸的手机有可能分辨率不同,手机分辨率就高不就低,因为分辨率低了屏幕就会有模糊的感觉)。
- drawable-xhdpi里面存放加高分辨率的图片(如720×1280),一般对应5英寸~5.5英寸的手机。
- drawable-xxhdpi里面存放超高分辨率的图片(如1080×1920),一般对应6英寸~6.5英寸的手机。
- drawable-xxxhdpi里面存放超超高分辨率图片(如1440×2560),一般对应7英寸以上的手机。
基本上,分辨率每加大一级,宽度和高度就要增加二分之一或三分之一像素。如果各自目录存在同名图片,Android就会根据手机的分辨率分别适配对应文件夹里的图片。在开发App时,为了兼容不同的手机屏幕,在各个目录存放不同分辨率的图片,才能达到最合适的显示效果。例如,在drawable-hdpi里面放了一张背景图片bg.png(分辨率480×800),其他目录没放,使用分辨率为480×800的手机查看该App界面没有问题,但是使用分辨率为720×1280的手机查看该图片就会模糊,原因是Android为了让bg.png适配高分辨率的屏幕,强行把bg.png拉伸到了720×1280,拉伸的结果就是图片模糊了。
在XML布局文件中引用图片文件中引用图形文件可用“@drawable/不含扩展名的文件名称”这种形式,如各视图的background
属性、ImageView
和ImageButton
的src属性、TextView
和Button
四个方向的drawable***系列属性都可以引用图形文件。
形状图形
Shape图形又称形状图形,用来描述常见的几何形状,包括矩形、圆角矩形、圆形、椭圆等。用好形状图形可以让App页面不在呆板,还可以节省美工不少工作量。
形状图形的定义文件放在srawable目录下,它是以shape标签为根节点的XML描述文件。根节点下定义了6个节点,分别是size
(尺寸)、stroke
描边、corners
(圆角)、solid
(填充)、padding
(间隔)、gradient
(渐变),各节点的属性值主要是宽和高、半径、角度以及颜色。下面是形状各个节点及其属性的简要说明。
shape(形状)
Shape是形状图形文件的根节点,它描述了当前是哪几种几何图形。下面是shape节点的常用属性说明。
- shape:字符串类型,表示图形的形状。形状取值说明见下表
形状类型 | 说明 |
---|---|
rectangle | 矩形。默认值 |
oval | 椭圆。此时corners节点会失效 |
Line | 直线。此时必须设置stroke节点,不然会报错 |
ring | 圆环 |
size(尺寸)
size是shape的下级节点,它描述了形状图形的宽高尺寸。若无size节点,则表示宽高与宿主视图一样大小。下面是size节点的常用属性说明。
- height:像素类型,图形高度。
- width:像素类型,图形宽度
stroke(描边)
stroke是shape的下级节点,它描述形状图形的描边规格。若无stroke节点,则表示不存在描边。下面是stroke节点的常用属性说明。
- color:颜色类型,描边的颜色。
- dashGap:像素类型,每段虚线之间的间隔。
- dashWidth:像素类型,每段虚线的宽度。若dashGap和dashWidth有一个值为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:像素类型,与下方的间隔。
- right:像素类型,与右边的间隔。
- left:像素类型,与左边的间隔。
gradient(渐变)
gradient是shape的下级节点,它描述了形状图形的颜色渐变。若无gradient节点,则表示没有渐变效果。下面是gradient节点的常用属性说明。
- angle:整型,渐变的起始角度。为0时表示始终的9点位置,值增大表示往逆时针方向旋转。
- type:字符串类型,渐变类型。渐变类型取值见下表:
渐变类型 | 说明 |
---|---|
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描述文件。往该文件填入下面的圆角矩形内容定义:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 指定了形状内部的填充颜色 --><solid android:color="#ffdd66" />
<!-- 指定了形状轮廓的粗细与颜色 --><strokeandroid:width="1dp"android:color="#aaaaaa" />
<!-- 指定了形状4个圆角的半径 --><corners android:radius="10dp" />
</shape>
接着再创建一个椭圆图形,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"android:shape="oval">
<!-- 指定了形状内部的填充颜色 --><solid android:color="#ff66aa" />
<!-- 指定了形状轮廓的粗细与颜色 --><strokeandroid:width="1dp"android:color="#aaaaaa" />
</shape>
执行代码,切换两种图形显示,就能看到如下两种效果图:
九宫格图片
将某张图片设置成视图背景时,如果图片尺寸太小,则系统会自动拉伸图片使之填满背景。但是一旦图片拉伸过大,其画面容易变得模糊,如下图:
如上图所示,上面的按钮的背景图片被拉得很宽,此时左右两边的边缘即变宽又变得模糊。需要注意的是,我们在xml文件里通过背景属性这样android:background="@drawable/button_normal_orig"
直接设置背景图片可能显示不出来。这时只需要将按钮控件用<android.widget.Button
代替<Button
就可以了。
为了解决拉伸模糊问题,Android设计了点九图片。点九图片的扩展名是.png,文件后面常带有“.9”字样。因为该图片划分了3×3的九宫格区域,所以得名点九图片,也叫做九宫格图片。如果背景是个形状图形,其stroke节点的width属性已经设置了固定数值(如1dp),那么无论该图形被拉到多大,描边宽度始终是1dp。九宫格图片的实现原理与之类似,即拉伸图形时,只拉伸内部区域,不拉伸边缘线条。
为了演示九宫格图片的展示效果,利用Android Studio制作一张点九图片。首先在drawable目录下找到待加工的原始图片button_pressed_orig.png,右击它弹出右键菜单,如下图:
选择上图右键显示菜单的Create 9-Patch file...
,并在随后弹出的对话框中单击OK按钮。接着drawable目录自动生成一个名为“button_pressed_orig.9.png
”的图片,双击该文件,主界面右侧弹出如下图显示的点九图片的加工窗口:
上图的左侧窗口时图片加工区域,右侧窗口是图片预览区域,从上到下依次是纵向拉伸预览,横向拉伸预览、两方向同时拉升预览。在左侧窗口图片四周的马赛克处单击会出现一个黑点,把黑点左右或上下拖动会拖出一段黑线,不同方向上的黑线表示不同效果。
下图所示,界面上的黑线指的是水平方向的拉伸区域。在水平方向拉伸图片时,只有黑线区域内的图像会被拉伸,黑线以外的图像保持原状,从而保证左右两边的边框厚度不变。
下图所示,界面左边的黑线指的是垂直方向的拉伸区域。在垂直方向拉伸图片时,只有黑线区域内的图像会被拉伸,黑线以外的图像保持原状,从而保证上下两侧的边框厚度不变。
下图所示,界面下边的黑线指的是该图片作为控件背景时,控件内部的文字左右边界只能放在黑线区域内,这里Horizontal Padding
的效果就相当于android:paddingLeft
与android:paddingRight
。
下图所示,界面右边的黑线指的是该图片作为控件背景时,控件内部的文字上下边界只能放在黑线区域内。这里Vertical Padding
的效果相当于android:paddingTop
与android:paddingBottom
。
注意:如果点九图片被设置为视图背景,且该图片指定了Horizontal Padding
和Vertical Padding
,那么视图内部将一直与视图边缘保持固定间距,无论怎么调整XML文件和Java代码都无法缩小,缘由时点九图片早已在水平和垂直都设置了padding。
状态列表图形
常见的图形文件一般为静态文件,但有时会用到动态文件,比如按钮控件的背景在正常情况下是凸起的,在按下时是凹陷的,从按下到弹起的过程,用户便知道点击了该按钮。根据不同的触摸情况变更形态,这种情况用到了Drawable的一个子类StateListDrawable(状态列表图形),它在XML文件中规定了不同状态所呈现的图形列表。
接下来演示一下状态列表图形的界面效果,右击drawable目录,并依次选择右键菜单的New->Drawable resource file,在弹窗中输入文件名再单击OK按钮,即可自动生成一个XML描述文件。往该文件填入下面的状态列表图形定义:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"><item android:state_pressed="true"android:drawable="@drawable/button_pressed" /><item android:drawable="@drawable/button_normal" />
</selector>
上述XML文件的关键点是state_pressed
属性,该属性表示按下状态,值为true表示按下时显示button_pressed图像,其余情况显示button_normal图像。
接下来做个验证,首先将“定制样式的按钮”这一按钮控件的android:background
属性设置为@drawable/btn_nine_selector
,然后再屏幕上点击按钮,观察发现按下按钮时的界面如下图所示:
未点击时的状态如下:
状态列表图形不仅用于按钮控件,还可用于其他拥有多种状态的控件,这取决于开发者在XML文件中指定了哪种状态类型。各种状态类型取值说明取值如下表:
状态类型的属性名称 | 说明 | 适用的控件 |
---|---|---|
state_pressed | 是否按下 | 按钮Button |
state_checked | 是否勾选 | 复选框CheckBox、单选按钮RadioButton |
state_foused | 是否获取焦点 | 文本编辑框EditText |
state_selected | 是否选中 | 各控件通用 |
选择按钮
复选框CheckBox
在学习复选框之前,先了解一下CompoundButton类。在Android体系中,CompoundButton类是抽象的符合按钮,因为是抽象类,所以它不能直接使用。在实际开发中用的是CompoundButton的几个派生类,主要有复选框CheckBox、单选按钮RadioButton以及开关按钮Switch,这些派生类均可使用CompoundButton的属性和方法。加之CompoundButton本身继承了Button类,故以上几种按钮同时具备Button的属性和方法,它们之间的继承关系如下图所示:
CompoundButton在XML文件中主要使用下面两个属性。
- checked:指定按钮的勾选状态,true表示勾选,false则表示未勾选。默认为未勾选。
- button:指定左侧勾选图标的图形资源,如果不指定就使用系统的默认图标。
CompoundButton在Java代码中主要使用下列4个方法。
- setChecked:设置按钮的勾选状态。
- setButtonDrawable:设置左侧勾选图标的图形资源。
- setOnCheckedChangeListener:设置勾选状态变化的监听器。
- isChecked:判断按钮是否勾选。
复选框CheckBox是CompoundButton一个最简单的实现控件,点击复选框将它勾选,再次点击取消勾选。复选框对象调用setOnCheckedListener
方法设置勾选监听器,这样在勾选和取消勾选时就会触发监听器的勾选事件。
为了验证,页面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"><CheckBoxandroid:id="@+id/ck_system"android:layout_width="match_parent"android:layout_height="wrap_content"android:padding="5dp"android:checked="false"android:text="这是系统的CheckBox"android:textColor="@color/black"android:textSize="17sp" /><android.widget.CheckBoxandroid:id="@+id/ck_custom"android:layout_width="match_parent"android:layout_height="wrap_content"android:button="@drawable/checkbox_selector"android:padding="5dp"android:checked="false"android:text="这个CheckBox换了图标"android:textColor="@color/black"android:textSize="17sp" />
</LinearLayout>
复选框的状态有选中和没选中两种,图形XML文件checkbox_selector编写如下:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"><item android:state_checked="true" android:drawable="@drawable/check_choose" /><item android:drawable="@drawable/check_unchoose" />
</selector>
页面对应的java代码编写如下:
// 该页面实现了接口OnCheckedChangeListener,意味着重写勾选监听器的onCheckedChanged方法
public class CheckBoxActivity extends AppCompatActivity implements CompoundButton.OnCheckedChangeListener {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_check_box);// 给ck_system设置勾选监听器,一旦用户点击复选框,就触发监听器的onCheckedChanged方法((CheckBox)findViewById(R.id.ck_system)).setOnCheckedChangeListener(this);// 给ck_custom设置勾选监听器,一旦用户点击复选框,就触发监听器的onCheckedChanged方法((CheckBox)findViewById(R.id.ck_custom)).setOnCheckedChangeListener(this);}@Overridepublic void onCheckedChanged(CompoundButton compoundButton, boolean b) {String desc = String.format("您%s了这个CheckBox", b? "勾选":"取消勾选");compoundButton.setText(desc);}
}
运行App,不点击,两个复选框显示如下:
点击时,复选框变化如下:
开关按钮Switch
Switch是开关按钮,它像是一个高级版本的CheckBox,在选中与取消中时可展现的界面元素比复选框丰富。Switch控件新添加的XML属性说明如下:
- textOn:设置右侧开启时的文本。
- textOff:设置左侧关闭时的文本。
- track:设置开关轨道的背景。
- thumb:设置开关标识的图标。
虽然开关按钮时升级版的复选框,但它在实际开发中用的不多。原因之一是大家觉得Switch的默认界面不够美观,图标感觉有点小巧,效果如下:
不过我们可以使用CheckBox实现一个大气一点的开关按钮,通过借助状态列表图形,创建一个图形专用的XML文件,给状态列表指定选中与未选中时的开关图标,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"><item android:state_checked="true" android:drawable="@drawable/switch_on" /><item android:drawable="@drawable/switch_off" />
</selector>
然后把CheckBox
标签的background
属性设置为@drawable/switch_selector
,同时将button
属性设置为@null
。完整的CheckBox标签内容如下:
<CheckBoxandroid:id="@+id/ck_status"android:layout_width="60dp"android:layout_height="30dp"android:background="@drawable/switch_selector"android:button="@null" />
这里解释一下,为什么状态图片设置到android:background
属性而不是android:button
属性。因为android:button
有局限性,无论图片多大,都只显示一个和我们设置的宽高大小的图片部分,因此我们在这还是设置图片到android:background
。最后的效果如下图:
单选按钮RadioButton
单选按钮,指的是在一组按钮中选择一项不能多选。通过RadioGroup这个容器确定单选组的范围,同一组RadioButton都放在同一个RadioGroup下。可以通过RadioGroup的android:orientation
属性指定下级控件的排列方向,该属性的值为horizontal
时,单选按钮在水平方向排列;该属性的值为vertical
时,单选按钮在垂直方向排列。
其实RadioGroup下面除了放RadioButton还可以放其他子控件(如TextView、ImageView等)。如此看来,单选按钮相当于特殊的线性布局,他们主要有以下两个区别:
- 单选组多了管理单选按钮的功能,而线性布局不具备该功能。
- 如果不指定orientation属性,那么单选组默认垂直排列,而线性布局默认水平排列
下面是RadioGroup在Java代码中3个常用的方法:
- check:选中指定资源编号的单选按钮
- getCheckedRadioButtonId:获取已经选中的单选按钮资源编号
- setOnCheckedChangedListener:设置单选按钮勾选变化的监听器
与CheckBox不同的是,RadioButton默认未选中,点击后才会显示选中,再次点击不会取消选中。只有点击其他同组单选按钮时,原来选中的单选按钮才会取消选中。另外单选按钮的选中事件不是由RadioButton处理,而是由RadioGroup处理。
为了显示效果,我们活动页面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"><TextViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:text="请选择您的性别"android:textColor="@color/black"android:textSize="17sp" /><RadioGroupandroid:id="@+id/rg_sex"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal" ><RadioButtonandroid:id="@+id/rb_male"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:checked="false"android:text="男"android:textColor="@color/black"android:textSize="17sp" /><RadioButtonandroid:id="@+id/rb_female"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:checked="false"android:text="女"android:textColor="@color/black"android:textSize="17sp" /></RadioGroup><TextViewandroid:id="@+id/tv_sex"android:layout_width="match_parent"android:layout_height="wrap_content"android:textColor="@color/black"android:textSize="17sp" />
</LinearLayout>
界面对应的Java代码编写如下:
public class RadioHorizontalActivity extends AppCompatActivity implements RadioGroup.OnCheckedChangeListener {private TextView tv_sex; // 声明一个文本视图对象@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_radio_horizontal);// 从布局文件中获取视图控件tv_sextv_sex = findViewById(R.id.tv_sex);// 注册监听器,一旦点击组内单选按钮,就触发监听器的onCheckChanged方法((RadioGroup)findViewById(R.id.rg_sex)).setOnCheckedChangeListener(this);}@Overridepublic void onCheckedChanged(RadioGroup radioGroup, int checkedId) {if (R.id.rb_male == checkedId) {tv_sex.setText("性别为男");} else if (R.id.rb_female == checkedId) {tv_sex.setText("性别为女");}}
}
运行App,一开始没选中时页面显示如下,此时两个单选按钮均未选中:
单击单选按钮,则界面变化如下:
文本输入
编辑框EditText
编辑框EditText用于接收软键盘输入的文字,例如用户名、密码、评论内容等,它由文本视图派生而来,除了TextView已有的属性和方法外,EditText还支持下列XML属性。
- inputType:指定输入的文本类型。输入类型的取值说明见下表,若同时使用多种类型,则可使用竖线(|)把多种文本类型拼接起来。
输入类型 | 说明 |
---|---|
text | 文本 |
textPassword | 文本密码。显示时用圆点(●)代替 |
number | 整型数 |
numberSigned | 带符号的数字。允许在开头带负号(-) |
numberDecimal | 带小数点的数字 |
numberPassword | 数字密码。显示时用(●)代替 |
datetime | 时间日期格式。除了数字外,还允许输入横线(_)、斜杠(/)、空格( )、冒号(:) |
date | 日期格式。除了数字外,还允许输入横线(_)和斜杠(/) |
time | 时间格式。除了数字外,还允许输入冒号(:) |
- maxLength:指定文本允许输入的最大长度。
- hint:指定提示文本的内容。
- textColorHint:指定提示文本的颜色。
接下来进行效果验证,编写页面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:padding="5dp"android:orientation="vertical"><TextViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="5dp"android:text="下面是登录信息"android:textColor="@color/black"android:textSize="17sp" /><EditTextandroid:layout_width="match_parent"android:layout_height="wrap_content"android:inputType="text"android:maxLength="10"android:hint="请输入用户名"android:textColor="@color/black"android:textSize="17sp" /><EditTextandroid:layout_width="match_parent"android:layout_height="wrap_content"android:inputType="textPassword"android:maxLength="8"android:hint="请输入密码"android:textColor="@color/black"android:textSize="17sp" /><TextViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="10dp"android:text="下面是手机信息"android:textColor="@color/black"android:textSize="17sp" /><EditTextandroid:layout_width="match_parent"android:layout_height="wrap_content"android:inputType="number"android:maxLength="11"android:hint="请输入11位手机号码"android:textColor="@color/black"android:textSize="17sp" /><EditTextandroid:layout_width="match_parent"android:layout_height="wrap_content"android:inputType="numberPassword"android:maxLength="11"android:hint="请输入6位服密码"android:textColor="@color/black"android:textSize="17sp" />
</LinearLayout>
运行App后,进入初始的编辑界面如下图:
在XML文件中通过inputType
属性对各个输入框的输入内容进行了限制,输入内容后,界面显示如下:
在实际开发中,我们可能有自定义输入框边框的需求,在输入与为输入时可能会显示两种不同边框以表示不同状态。那么此时就可以通过background
属性,对两种状态进行修改。下面将演示默认边框、无边框和圆角矩形边框三种形态。
首先编写未输入(为获取焦点)时圆角矩形的形状图形文件,该文件显示灰色圆角矩形轮廓,对应的XML文件定义如下:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"><!-- 指定形状内部填充的颜色 --><solid android:color="#ffffff" /><!-- 指定了形状轮廓的粗细与颜色 --><strokeandroid:width="1dp"android:color="#aaaaaa" /><!-- 指定了形状四个圆角的半径 --><corners android:radius="5dp" /><!-- 指定了形状四个方向的间距 --><paddingandroid:bottom="2dp"android:left="2dp"android:right="2dp"android:top="2dp" />
</shape>
然后编写的是输入(获取焦点)时圆角矩形的形状图形文件,该文件显示蓝色圆角矩形轮廓,对应的XML文件定义如下:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"><!-- 指定了形状内部的填充颜色 --><solid android:color="#ffffff" /><!-- 指定了形状轮廓的粗细与颜色 --><strokeandroid:color="#0000ff"android:width="1dp" /><!-- 指定了形状四个圆角的半径 --><corners android:radius="5dp" /><!-- 指定了圆角四个方向的间距 --><paddingandroid:top="2dp"android:right="2dp"android:left="2dp"android:bottom="2dp" />
</shape>
接着编写编辑框的状态列表图形文件editext_selector,主要在selector
节点下添加两个item
:一个item
设置了获取焦点时(android:state_focused="true"
)的图形为@drawable/shape_edit_focus
;另一个item
设置了图形@drawable/shape_edit_normal
但未指定任何状态,表示其他状态都展示该图形。完整状态列表图形定义如下:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"><item android:state_focused="true" android:drawable="@drawable/shape_edit_focus" /><item android:drawable="@drawable/shape_edit_normal" />
</selector>
最后编写页面XML文件,一共有3个EditText控件:第一个采用默认编辑框;第二个将编辑框的background
属性设置为@null
表示不显示边框;第三个将background
属性设置为@drawable/editext_selector
,其背景由editext_selector.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:padding="5dp"android:orientation="vertical"><EditTextandroid:layout_width="match_parent"android:layout_height="wrap_content"android:inputType="text"android:hint="这是默认边框"android:textColor="@color/black"android:textSize="17sp" /><EditTextandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="5dp"android:inputType="text"android:hint="我的边框不见了"android:background="@null"android:textColor="@color/black"android:textSize="17sp" /><EditTextandroid:layout_width="match_parent"android:layout_height="wrap_content"android:inputType="text"android:hint="我的边框是圆角"android:background="@drawable/editext_selector"android:textColor="@color/black"android:textSize="17sp" />
</LinearLayout>
运行App,更换边框背景的编辑框界面显示如下:
焦点变更监听
当点击编辑框后,EditText当即获取焦点,此时就会发生焦点状态变更触发onFocusChange
方法。编辑框的焦点,直观地看就是编辑框地光标闪动,哪个编辑框有光标,焦点就落在哪里。需要注意的是,编辑框比较特殊,要点击两次才会触发点击事件,因为第一次点击触发地是焦点变更。因此对于编辑框来说,一般都注册焦点监听器,而非点击监听器。
下面是以输入手机号码以及密码为例,给密码框注册焦点变更监听器地代码例子:
// 从布局文件中获取密码编辑框et_password
EditText et_password = findViewById(R.id.et_password);
// 给密码编辑框注册一个焦点变化监听器,一旦焦点发生变化,就触发onFocusChange方法
et_password.setOnFocusChangeListener(this);
为了能成功注册焦点变更监听器,还需要让活动实现接口View.OnFocusChangeListener
,并重写该接口方法。我们的代码逻辑是,当密码输入框获取焦点时,判断手机号长度是否正确,不正确则将焦点重新设置到手机号输入框。焦点变更监听器的代码如下:
// 焦点变更事件的处理方法,hasFocus表示当前控件是否获得焦点。
// 为什么光标进入事件不选onClick?因为要点两下才会触发onClick动作(第一下是切换焦点动作)
@Override
public void onFocusChange(View view, boolean hasFocus) {// 判断密码编辑框是否获得焦点。hasFocus为true表示获得焦点,为false表示失去焦点if (view.getId()==R.id.et_password && hasFocus) {String phone = et_phone.getText().toString();if (TextUtils.isEmpty(phone) || phone.length()<11) {// 手机号码编辑框请求焦点,也就是把光标移回手机号码编辑框et_phone.requestFocus();Toast.makeText(this, "请输入11位手机号码", Toast.LENGTH_SHORT).show();}}
}
运行App,当手机输入框数据长度不够时,点击密码输入框弹出提示如下:
文本变化监听器
有时我们在输入账号密码时,可能在我们输入内容后想直接点击登录而不必点击键盘的Enter键收起键盘后再点击登录。那么我们只需要在满足某些条件后关闭软键盘即可,例如手机号码输入满11位或者密码输入满6位,等等。
现在明确需求条件后,达到指定长度关闭软键盘功能,现在我们可以分解为功能点:一个是如何关闭软键盘;另一个是如何判断已输入的文字是否达到指定字数。分别说明如下:
- 如何关闭软键盘
输入法键盘由系统服务Context.INPUT_METHOD_SERVICE
管理,所以关闭键盘也要由该服务处理,下面是使用系统服务关闭软键盘代码样例:
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位密码,一旦输入长度达到指定长度就关闭软键盘,详细监听器代码如下:
// 定义一个编辑框监听器,在输入文本达到指定长度时自动隐藏输入法
private class HideTextWatcher implements TextWatcher {private EditText mView; // 声明一个编辑框对象private int mMaxLength; // 声明一个最大长度变脸HideTextWatcher(EditText editText, int maxLength) {super();mView = editText;mMaxLength = maxLength;}// 在编辑框的输入文本变化前触发@Overridepublic void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {}// 在编辑框的输入文本变化时触发@Overridepublic void onTextChanged(CharSequence charSequence, int start, int before, int count) {}// 在编辑框的输入文本变化后触发@Overridepublic void afterTextChanged(Editable editable) {String str = editable.toString(); // 获取已输入的文本字符// 输入字符串长11位(手机号),或者达6位(密码)时关闭输入法if ((11 == str.length() && 11 == mMaxLength) || (6 == str.length() && 6 == mMaxLength)) {ViewUtil.hideOneInputMethod(EditHideActivity.this, mView);}}
}
写好监听器代码,还要给手机编辑框和密码编辑框分别注册监听器,注册代码示例如下:
// 获取编辑框et_phone
EditText et_phone = findViewById(R.id.et_phone);
// 获取编辑框et_password
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,输入手机号,但手机号长度没达到11位时软键盘依旧显示,如下图:
当输入总长度达11位后,软键盘收起,如下图:
对话框
提醒对话框AlertDialog
AlertDialog名为提醒对话框,它是Android中最常用的对话框,可以完成常见的交互操作,例如提示、确认、选择等功能。由于AlertDialog没有公开的构造方法,因此必须借助建造器AlertDialog.Builder
才能完成参数设置,AlertDialog.Builder
常用方法如下:
- setIcon:设置对话框的标题图标。
- setTitle:设置对话框的标题文本。
- setMessage:设置对话框的内容文本。
- setPositiveButton:设置肯定按钮的信息,包括按钮文本和监听器。
- setNegativeButton:设置否定按钮的信息,包括按钮文本和监听器。
- setNeturalButton:设置中性按钮的信息,包括按钮文本和点击监听器,该方法比较少用。
通过AlertDialog.Builder
设置完成对话框参数,还需要调用建造器的create
方法才能生成对话框实例。最后调用对话框实例的show
方法,在页面上弹出提醒对话框。
下面是构建并显示提醒对话框的Java代码例子:
// 创建提示对话框的建造器
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("尊敬的用户"); // 设对话框标题
builder.setMessage("你真的要卸载我吗?"); // 设置对话框的提示内容
// 设置对话框的肯定按钮文本及其监听器
builder.setPositiveButton("残忍卸载", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialogInterface, int which) {tv_alert.setText("虽然依依不舍,但是只能离开了");}
});
// 设置对话框的否定按钮文本及其监听器
builder.setNegativeButton("我再想想", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialogInterface, int which) {tv_alert.setText("让我再陪你三百六十五个日夜");}
});
AlertDialog alert = builder.create(); // 根据构造器构建提醒对话框对象
alert.show();
提示对话框如下图所示,可以看见对话框由标题和内容,还有两个按钮:
日期对话框DatePickerDialog
系统专门提供了日期选择器DatePicker
,供用户选择具体的年、月、日。不过,DatePicker
并非弹窗模式,而是在当前页面占据一块区域,并且不会自动关闭。使用习惯上,日期控件应该是需要时弹出,使用完毕后关闭。因此,界面上很少直接显示DatePicker
,而是通过系统封装好的DatePickerDialog
实现。
DatePickerDialog
相当于在AlertDialog
上装载了DatePicker
,编码时只需要调用构造方法设置当前的年、月、日,然后调用show
方法即可弹出日期对话框。日期选择事件则由监听器DatePickerDialog.OnDateSetListener
负责响应,在该监听器的onDateSet
方法中,开发者获取用户选择的具体日期,再做后续处理。需要注意的是,onDateSet
方法的月份参数,它的起始值是从0开始的,也就是说一月份对应的数值是0而十二月份对应的数值是11,中间值以此类推。
在界面上嵌入DatePicker
的效果如下图所示:
单独弹出日期对话框的效果如下图所示效果:
下面是使用日期对话框的Java代码例子,包括弹出日期对话框和处理日期监听事件:
// 该页面类实现了接口OnDateSetListener,意味着要重写日期监听器的onDateSet方法
public class DatePickerActivity extends AppCompatActivity implementsView.OnClickListener, DatePickerDialog.OnDateSetListener {private TextView tv_date; // 声明一个文本控件private DatePicker dp_date; // 声明一个日期选择器对象@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_date_picker);// 从布局文件中获取文本控件tv_datetv_date = findViewById(R.id.tv_date);// 从布局文件中获取日期选择器dp_datedp_date = findViewById(R.id.dp_date);findViewById(R.id.btn_date).setOnClickListener(this);findViewById(R.id.btn_ok).setOnClickListener(this);}@Overridepublic void onClick(View view) {if (R.id.btn_date == view.getId()) {// 获取日历的一个实例,里面包含了当前的年月日Calendar calendar = Calendar.getInstance();// 构建一个日期对话框,该对话框已经集成了日期选择器// DatePickerDialog的第二个参数指定了日期监听器DatePickerDialog dialog = new DatePickerDialog(this, this,calendar.get(Calendar.YEAR), // 年份calendar.get(Calendar.MONTH), // 月份calendar.get(Calendar.DATE)); // 日子dialog.show(); // 显示日期对话框} else if (R.id.btn_ok == view.getId()) {// 获取日期选择器dp_date设定的年月份String desc = String.format("您选择的日期是%d年%d月%d日",dp_date.getYear(), dp_date.getMonth() + 1, dp_date.getDayOfMonth());tv_date.setText(desc);}}@Overridepublic void onDateSet(DatePicker datePicker, int year, int monthOfYear, int dayOfMonth) {// 获取日期对话框设定的年月份String desc = String.format("您选择的日期是%d年%d月%d日",year, monthOfYear + 1, dayOfMonth);tv_date.setText(desc);}
}
时间对话框TimePickerDialog
安卓同样给开发者提供了时间选择。同样的,在实际开发中也不常直接用TimePicker
,而是用封装好的时间选择器对话框TimePickerDialog
。该对话框的用法类似DatePickerDialog
,不同之处主要有以下两点:
- 构造方法传入的是当前的小时和分钟,最后一个参数表示是否采取24小时制,true表示小时的数值取值0~23;若为false则表示采取的12小时制。
- 时间选择监听器为
TimePickerDialog.OnTimeSetListener
,对应的实现方法为onTimeSet
,在该方法中可获得用户选择得小时和分钟。
在界面上显示的TimePicker
效果如下图所示:
单独弹出的小时和分钟按照钟表的形式显示如下:
下面是使用时间对话框的Java代码,包括弹出时间对话框和处理时间监听事件:
// 该页面类实现了接口OnTimeSetListener,意味着要重写时间监听器的onTimeSet方法
public class TimePickerActivity extends AppCompatActivity implementsView.OnClickListener, TimePickerDialog.OnTimeSetListener {private TextView tv_time; // 声明一个文本对象private TimePicker tp_time; // 声明一个时间选择器对象@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_time_picker);// 从布局文件中获取文本对象tv_timetv_time = findViewById(R.id.tv_time);// 从布局文件中获取时间选择器对象tp_timetp_time = findViewById(R.id.tp_time);findViewById(R.id.btn_time).setOnClickListener(this);findViewById(R.id.btn_ok).setOnClickListener(this);}@Overridepublic void onClick(View view) {if (R.id.btn_time == view.getId()) {// 获取日历的一个实例,里面包含当前的分秒Calendar calendar = Calendar.getInstance();// 创建一个对话框,该对话框已经集成了时间选择器// TimePickerDialog的第二个参数指定了时间监听器TimePickerDialog dialog = new TimePickerDialog(this, this,calendar.get(Calendar.HOUR_OF_DAY), // 小时calendar.get(Calendar.HOUR_OF_DAY), // 分钟true); // 使用24小时制;false表示12小时制dialog.show(); // 显示时间对话框} else if (R.id.btn_ok == view.getId()) {// 获取定时器tp_time的时间String desc = String.format("您选择的时间是%d时%d分",tp_time.getHour(), tp_time.getMinute());tv_time.setText(desc);}}// 一旦点击时间对话框上的确定按钮,就会触发监听器的onTimeSet方法@Overridepublic void onTimeSet(TimePicker timePicker, int hourOfDay, int minute) {// 获取时间对话框设定的小时和分钟String desc = String.format("您选择的时间是%d时%d分", hourOfDay, minute);tv_time.setText(desc);}
}
工程源码
所有样例代码都可点击工程源码下载。