本人只是
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的基本规范
,其中包括属性、测量、绘制、交互
,大家可以把它当做自定义控件的入门知识,但相信在了解了这些基本规范
后,再加上勤奋的练习,以后也能定义出功能复杂效果炫酷的控件,一起加油!