本人只是
Android
小菜一个,写技术文档只是为了总结本身在最近学习到的知识,历来不敢为人师,若是里面有些不正确的地方请你们尽情指出,谢谢!java
在进行Android
应用开发时,能够选择系统提供的各式各样的控件,但有时原生控件在功能和效果上并不能知足需求,这时就要求必须根据实际需求来定义新的控件,能够经过继承View
,也能够继承某些已经存在的原生控件,来实现自定义控件。本文将选择直接继承View
来实现一个最简单的控件。android
自定义控件包含了Android
中和View
相关的不少知识,学习自定义控件也能帮组学习和理解相关知识。canvas
要想自定义出功能强大效果酷炫的控件,要求必须对View
体系有深刻的理解,在这点我还差的不少,因此本文并不能教你们怎样去实现这样的控件。本文只是从自定义View
的基本规范方面,跟你们探讨下在自定义一个控件的过程当中,有哪些方面须要注意的,或者说有哪些功能是须要实现的,主要包括:控件属性
、控件测量
、控件绘制
和控件交互
。app
当咱们在xml
中定义控件的时候,确定须要对控件具备的某些属性进行设置,例如宽高
、背景颜色
、文本
等等,下面是在使用 TextView
的一个示例:ide
<TextView android:id="@+id/main_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#FF0000" android:text="Hello World!" />
复制代码
在自定义控件的时候,为了可以让用户灵活定义控件的某些特性,也须要经过属性的方法把用户指定的值传入控件,而不是在控件内部使用预约义的值,这也就要求在自定义控件的时候使用到自定义属性。函数
自定义属性须要在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
类型以下表:this
类型 | 含义 | 取值 |
---|---|---|
boolean |
布尔类型 | 只能是true 或false |
string |
字符串类型 | 任意字符串值 |
integer |
整数类型 | 只能是整数 |
float |
浮点数类型 | 只能是浮点和整型 |
fraction |
百分比类型 | 只能以% 结尾 |
color |
颜色类型 | 能够是颜色值或者指向color 的资源 |
dimension |
尺寸类型 | 能够是具体尺寸值或指向尺寸的资源 |
reference |
引用类型 | 只能是指向某一资源的ID |
enum |
枚举类型 | 只能是定义的枚举值 |
flag |
位标志类型 | 只能是定义的位值 |
在这里只定义了一个简单的color
类型的属性,其余类型的属性你们可自行定义,方法是相似的。spa
在定义了属性后,能够直接在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
的使用场景。
在前面已经讲了如何定义和在控件内部获取属性,可是咱们知道有时控件属性是须要根据不一样的场景进行修改的,而在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
后,就能够在使用控件的时候,动态获取和修改属性值了。
测量的目的是要肯定控件在显示的时候具体的显示尺寸,你们可能会奇怪:不是在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
两种状况都直接使用了父控制传递过来的尺寸。固然这只是一种最简单的状况,当要自定义高能复杂的控件时,宽高的肯定须要结合的因素会更多,计算也会更复杂。
测量控件后就能够知道控件的最终宽高信息,这时须要作的就是进行实际的绘制,只有经过绘制,控件才能真正地显示出来。绘制控件须要实现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
。
经过上面的几个过程,已经能在界面上显示自定义控件了,但显示不是最终的目的,真正的目的仍是但愿能与控件进行交互,最重要的是可以响应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
的重写能够实现让控件随着手指会移动,固然这里只是一个简单演示,还存在一些问题,好比控件会被移出屏幕以外,这是由于在移动时并无判断当前控件的位置,把这个条件加上就能够保证控件只在界面以内移动。
本文经过一个简单的自定义圆形的例子,大体讲解了自定义View的基本规范
,其中包括属性、测量、绘制、交互
,你们能够把它当作自定义控件的入门知识,但相信在了解了这些基本规范
后,再加上勤奋的练习,之后也能定义出功能复杂效果炫酷的控件,一块儿加油!