本人只是
Android小菜一个,写技术文档只是为了总结自己在最近学习到的知识,从来不敢为人师,如果里面有些不正确的地方请大家尽情指出,谢谢!
1. 概述
在进行Android应用开发时,可以选择系统提供的各式各样的控件,但有时原生控件在功能和效果上并不能满足需求,这时就要求必须根据实际需求来定义新的控件,可以通过继承View,也可以继承某些已经存在的原生控件,来实现自定义控件。本文将选择直接继承View来实现一个最简单的控件。
自定义控件包含了Android中和View相关的很多知识,学习自定义控件也能帮组学习和理解相关知识。
要想自定义出功能强大效果酷炫的控件,要求必须对View体系有深入的理解,在这点我还差的很多,所以本文并不能教大家怎样去实现这样的控件。本文只是从自定义View的基本规范方面,跟大家探讨下在自定义一个控件的过程中,有哪些方面需要注意的,或者说有哪些功能是需要实现的,主要包括:控件属性、控件测量、控件绘制和控件交互。
2. 控件属性
当我们在xml中定义控件的时候,肯定需要对控件具有的某些属性进行设置,例如宽高、背景颜色、文本等等,下面是在使用 TextView的一个示例:
<TextView
android:id="@+id/main_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#FF0000"
android:text="Hello World!" />
在自定义控件的时候,为了能够让用户灵活定义控件的某些特性,也需要通过属性的方法把用户指定的值传入控件,而不是在控件内部使用预定义的值,这也就要求在自定义控件的时候使用到自定义属性。
2.1 定义属性
自定义属性需要在res/values/attrs.xml里面定义,如果这个文件不存就自己创建一个。结合一个例子进行介绍:
<declare-styleable name="custom_view">
<attr name="default_color" format="color"></attr>
</declare-styleable>
declare-styleable name="custom_view"指定了自定义属性集合的name信息,这个值可以是任意值,但一般为了方面使用都是直接使用自定义控件的名字。
<attr name="default_color" format="color"></attr>指定了自定义属性集合里的具体属性和该属性对应的类型,本例中使用的是color类型,表明这个属性需要的是一个颜色值,能够支持的format类型如下表:
| 类型 | 含义 | 取值 |
|---|---|---|
boolean |
布尔类型 | 只能是true或false |
string |
字符串类型 | 任意字符串值 |
integer |
整数类型 | 只能是整数 |
float |
浮点数类型 | 只能是浮点和整型 |
fraction |
百分比类型 | 只能以%结尾 |
color |
颜色类型 | 可以是颜色值或者指向color的资源 |
dimension |
尺寸类型 | 可以是具体尺寸值或指向尺寸的资源 |
reference |
引用类型 | 只能是指向某一资源的ID |
enum |
枚举类型 | 只能是定义的枚举值 |
flag |
位标志类型 | 只能是定义的位值 |
在这里只定义了一个简单的color类型的属性,其他类型的属性大家可自行定义,方法是类似的。
2.2 使用属性
在定义了属性后,可以直接在xml使用这些属性,使用方法和原生控件属性一样,只需根据不同类型设置值即可。在上面定义一个属性default_color,现在就可以在xml里使用了:
<com.test.androidtest.CustomView
android:id="@+id/custom_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:default_color="#ffff00"/>
需要注意的是,在这里使用了新的命名空间app,其声明是xmlns:app="http://schemas.android.com/apk/res-auto",如果大家使用的Android Studio,这个命名空间是自动添加的,无须自行处理。
当xml使用了自定义属性后,在创建这个控件的时候,就会把这些属性传入控件,在控件内部就可以获取并使用到该属性值了。
// 在代码里通过 new 方式创建控件实例时使用
public CustomView(Context context) {
super(context);
}
// 在 xml 定义控件时使用,会获取到定义的属性
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
// 获取定义的属性集合
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.custom_view);
// 获取特定的属性值
if (array != null) {
default_color = array.getColor(R.styleable.custom_view_default_color, -1);
}
}
上述代码演示了如何在控件内部获取自定义属性,在成功获取到属性值后就可以利用该值进行后续的控件绘制工作了。
需要注意的是在自定义控件是需要实现两个不同的构造函数,分别对应于在
java和xml的使用场景。
2.3 修改属性
在前面已经讲了如何定义和在控件内部获取属性,但是我们知道有时控件属性是需要根据不同的场景进行修改的,而在xml只能指定属性的初始值,无法进行不断的修改。这就要求必须针对有些属性提供取值器和设值器,也就是常说的getter和setter,这里之所以说是“有些属性”,是因为并不是所有属性都需要支持动态修改的。
还是针对前面定义的default_color属性,现在对其设置取值器和设值器:
public int getColor() {
return default_color;
}
public void setColor(int color) {
default_color = color;
// 调用 onDraw,重新刷新控件.
invalidate();
}
取值器比较简单,只要返回当前属性值就可以了。设值器除了要更新当前属性值外,更重要的是,在更新完当前属性值外,要对当前的控件进行第二次的绘制,以更新控件状态,这里直接调用invalidate(),它会把当前view标志为DIRTY,在下一帧绘制时调用控件的onDraw()方法完成对控件的更新。设置了属性的getter和setter后,就可以在使用控件的时候,动态获取和修改属性值了。
3. 控件测量
测量的目的是要确定控件在显示的时候具体的显示尺寸,大家可能会奇怪:不是在xml已经指定了控件大小了吗?为什么还要再测量一次呢?这是因为在xml指定控件大小的时候有不同的方式,每种方式最终导致分配给控件的尺寸也不一样。
| 指定尺寸方式 | 含义 |
|---|---|
wrap_content |
根据控件具体内容分配尺寸 |
match_parent |
根据父控件剩余大小给控件分配尺寸 |
具体数值 |
根据给定的数值进行分配控件尺寸 |
为了能够测了控件,需要实现onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法,先看下View中该方法声明:
/**
* Measure the view and its content to determine the measured width and the
* measured height. This method is invoked by {@link #measure(int, int)} and
* should be overridden by subclasses to provide accurate and efficient
* measurement of their contents.
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
这里提到:onMeasure是用来决定控件的宽高信息的,为了能够提供更准确和高效的控件测量,子类最好要重写这个方法,所以自定义控件最好也要实现这个方法。
这里的参数widthMeasureSpec和heightMeasureSpec代表是什么意思?是不是就是控件的宽高呢?当然不是,如果它们表示的就是控件宽高就不需要我们继续测量了。widthMeasureSpec和heightMeasureSpec里面都包含了两个信息:size和mode,其中size表示的是父控件告诉子控件的建议宽高,mode表示当前的测量模式,具体有AT_MOST,EXACTLY和UNSPECIFIED,其含义如下:
| 测量模式 | 尺寸模式 | 含义 |
|---|---|---|
AT_MOST |
wrap_content |
父控件提供一个最大值,子控件不要超过父控件提供的尺寸大小。 |
EXACTLY |
match_parent或者具体值 |
父控件提供一个确切值,子控件可以直接使用这个尺寸来设置大小。 |
UNSPECIFIED |
暂无 |
父控件不提供,子控件可以任意设置大小。 |
从上面的表格可以看到:UNSPECIFIED一般是遇不到的,而AT_MOST和EXACTLY都会提供一个建议值,可以根据这个值和测试模式来确定子控件大小。
本文中的自定义控件的onMeasure如下:
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 使用宽高中的最小值把宽高设置为等值,因为控件的最终目的是画一个圆。
int dimension = Math.min(getSize(widthMeasureSpec), getSize(heightMeasureSpec));
// 设置最终的宽高信息,如果少了这步,得到的宽高将无法应用到控件中。
setMeasuredDimension(dimension, dimension);
}
private int getSize(int measureSpec) {
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
// EXACTLY 和 AT_MOST 直接使用父控件提供的宽高信息。
switch (mode) {
case MeasureSpec.EXACTLY:
case MeasureSpec.AT_MOST:
return size;
default:
// UNSPECIFIED 返回预定义的宽高信息,一般不会遇到。
return mMeasureWidthHeight;
}
}
在本文的自定义控件中,最终的目的是要显示一个圆形,在onMeasure里设置了等值宽高,而在获取宽高时针对AT_MOST和EXACTLY两种情况都直接使用了父控制传递过来的尺寸。当然这只是一种最简单的情况,当要自定义高能复杂的控件时,宽高的确定需要结合的因素会更多,计算也会更复杂。
4. 控件绘制
测量控件后就可以知道控件的最终宽高信息,这时需要做的就是进行实际的绘制,只有通过绘制,控件才能真正地显示出来。绘制控件需要实现onDraw(Canvas canvas)方法,和onMeasure一样,先看下在View中的声明:
/**
* Implement this to do your drawing.
*
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(Canvas canvas) {
}
可以发现:View并没有实现onDraw,这是因为View 是所有控件的父类,但其本身并不是一个可以直接显示的控件,这就要求所有需要显示的控件都必须实现这个方法,它的参数是Canvas类,就是常说的画布。为了显示控件,我们需要做的就是用Paint在Canvas上把需要显示的图像画出来,正如我们在电脑上经常在画图软件上画图一样。
现在看下本例中自定义控件的onDraw(Canvas canvas)的实现:
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 初始化画笔,这个对象需要在控件初始化时初始化,这里正常不会走到。
if (mPaint == null) {
mPaint = new Paint();
}
// 设置画笔的颜色。
mPaint.setColor(default_color);
// 在画布上画出一个圆形。
int radius = getMeasuredWidth() / 2;
canvas.drawCircle(getLeft() + radius, getTop() +radius, radius, mPaint);
}
上面的示例代码只是实现一个根据用户传入的颜色来进行画圆功能,其效果如下:

Canvas除了画圆,还可以画出更多更复杂的图形,Paint也可以有更多的控制,其大家自行查阅相关API。
5. 控件交互
通过上面的几个过程,已经能在界面上显示自定义控件了,但显示不是最终的目的,真正的目的还是希望能与控件进行交互,最重要的是能够响应touch事件,接下来就通过实现一个简单的随手指移动功能:
private int mLastX;
private int mLastY;
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - mLastX;
int offsetY = y - mLastY;
//重新放置新的位置
layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
break;
default:
break;
}
return true;
}
这次对onTouchEvent的重写可以实现让控件随着手指会移动,当然这里只是一个简单演示,还存在一些问题,比如控件会被移出屏幕之外,这是因为在移动时并没有判断当前控件的位置,把这个条件加上就可以保证控件只在界面之内移动。
6. 总结
本文通过一个简单的自定义圆形的例子,大致讲解了自定义View的基本规范,其中包括属性、测量、绘制、交互,大家可以把它当做自定义控件的入门知识,但相信在了解了这些基本规范后,再加上勤奋的练习,以后也能定义出功能复杂效果炫酷的控件,一起加油!