自定义控件可以让代码的重用达到一个新的高度,就跟用
Button
、ImageView
一样方便。
这一篇是不带手势和触摸的自定义控件,后面会补一篇,原因是这两个都是内容比较多的,所以分开说。
概述
Android中所有显示在界面上的东西,称之为控件,每一个控件都有一个共同的父类:android.view.View
,熟悉的Button
、ImageView
都是android.view.View
的子类,所以自定义控件也是如此,本质上都是继承android.view.View
。
自定义控件的方式很多:
- 继承
android.view.View
一般情况下的控件只需要继承View
- 继承
android.view.ViewGroup
ViewGroup
也是继承的View
,如果控件本身是一个布局或者有自己的子控件,那么继承ViewGroup
就好了 - 继承已有的其他控件
如果自定义控件是对现有控件的扩展,直接继承这个控件即可。
自定义控件的步骤
这篇中最重要的只有三个点,后面的东西都是围绕这三个点进行:
- 计算(
onMeasure
) - 布局(
onLayout
) - 绘制(
onDraw
)
计算:是计算控件的大小,什么情况下应该是多大,如果这是一个ViewGroup
还需要计算给子控件的大小,总之就是计算宽高用的。
布局:自定义控件的控件位置的摆放,在布局的过程中,可以获取到控件的大小以便计算。
绘制:通过Canvas
可以绘制各种形状和内容,也可以实现动画。
这三者的调用顺序是:
onMeasure
->onLayout
->onDraw
。在这过程中onMeasure
可能会被多次调用。
先了解一下这三者的功能和基本的使用方式,后面用例子来完整的介绍整个过程。
onMeasure
在介绍onMeasure
之前,先看一个重要的工具类:MeasureSpec
。一个MeasureSpec
封装了父布局传递给子布局的布局要求,每个MeasureSpec
代表了一组宽度和高度的要求,一个MeasureSpec
由大小和模式组成它有三种模式:
UNSPECIFIED
(未指定)
父元素不对子元素施加任何束缚,子元素可以得到任意想要的大小EXACTLY
(完全)
父元素决定子元素的确切大小,子元素将被限定在给定的边界里面而忽略它本身大小AT_MOST
(至多)
子元素至多达到指定大小的值。
它常用的三个函数:
– static int getMode(int measureSpec)
根据提供的测量值(格式)提取模式(上述三个模式之一)
– static int getSize(int measureSpec)
根据提供的测量值(格式)提取大小值(这个大小也就是通常所说的大小)
– static int makeMeasureSpec(int size,int mode)
根据提供的大小值和模式创建一个测量值(格式)
在计算控件本身的空间的时候,会根据三种不同的模式,按照自己的想法计算控件大小。现在实现一个简单的自定义控件来详细说明这个onMeasure
是何物,有三个要求:
– 自定义控件的默认大小是100 * 100
当布局不能明确确定控件大小的时候,比如wrap_content
,控件大小是100 * 100
– 然后最大可以跟随父控件的大小
布局中,父控件给子控件的空间是多大,子控件就使用多大空间
– 最后是可以明确的指定大小
布局文件中对应的layout属性是直接写死的,比如50dp
,但是,如果这个时候父控件给定的空间比50dp
要小,那么以父控件的大小为准。
现在先来第一个条件,默认大小是100 * 100
:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int desired = (int) (100 * getResources().getDisplayMetrics().density + 0.5f);
widthSize = Math.min(desired, widthSize);
heightSize = Math.min(desired, heightSize);
setMeasuredDimension(widthSize, heightSize);
}
上面4行关于padding的,是用来计算大小的时候考虑到padding的情况用的。用之前说的方法获取到宽和高分别的模式和提供出来的大小,然后设置一个默认的100dp
的大小desired
,然后宽和高都跟默认值比,取最小的一个值作为控件的大小,现在是什么效果呢?
- 如果控件布局是
wrap_content
,这时候传进来可以给子控件用的大小是屏幕分辨率尺寸,那么控件的大小是100 * 100
。 - 如果控件布局是大于
100 * 100
,同第一条一样还是取最小值是100 * 100
。 - 如果控件布局小于
100 * 100
,那么控件大小就是自己设定的值。
注意:
setMeasuredDimension(widthSize, heightSize);
方法是设置确定子控件占的空间位置的,必须调用,或者使用super.onMeasure(int widthMeasureSpec, int heightMeasureSpec)
,这个方法最后也会调用这个函数来确定控件大小。
那现在怎么让子控件的大小跟随父控件大小变化呢?这个很简单,因为onMeasure(int widthMeasureSpec, int heightMeasureSpec)
中传过来的宽高就是父控件能够给子控件宽高的界限,所以只需要设置成传过来的宽高:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
}
最后一条,如何明确的指定大小?代码跟上面一模一样,为什么呢?因为如果子控件布局有明确的值的时候比如:android:layout_width="100dp"
在onMeasure
中传过来的值就是这个明确的值,只有父控件不能确定子控件大小的时候这个值是屏幕的分辨率。
总结一下onMeasure
的三个特点:
- 父控件会限制子控件获得空间的界限。
- 子控件布局中有确定值的时候,父控件能够明确知道子控件需要多大的空间,所以传过来的值就是子控件设置的值。
- 子控件通过
model
,也就是模型来区分自己应该在什么情况下占用多少空间
之前那三条需求是分开的,也就是说他们之间是冲突的满足其中一个就不能满足另外的,那么下面实现一个完整的,类似系统控件的onMeasure
来适应各种情况,在实际项目中,可以直接按照这样来做:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 先计算控件的宽暂用的空间
if(widthMode == MeasureSpec.EXACTLY)
{// 确定的大小就是通过MeasureSpec.getSize()获取出来的值
}
else
{// 这里就是大小不确定,主要wrap_content或者父控件大小不确定的时候的match_parent
int desired = (int) (100 * getResources().getDisplayMetrics().density + 0.5f);
if (widthMode == MeasureSpec.AT_MOST)
{
widthSize = Math.min(desired, widthSize);
}
}
// 计算控件的高暂用的空间
if(heightMode == MeasureSpec.EXACTLY)
{// 确定的大小就是通过MeasureSpec.getSize()获取出来的值
}
else
{
int desired = (int) (100 * getResources().getDisplayMetrics().density + 0.5f);
if (heightMode == MeasureSpec.AT_MOST)
{
heightSize = Math.min(desired, heightSize);
}
}
setMeasuredDimension(widthSize, heightSize);
}
主要解释一下else
中的东西,如果宽或者高有明确的大小,直接使用设置的明确的值,如果不是确定的值就有很多种可能,这个时候如果是MeasureSpec.AT_MOST
,也就是父控件给子控件的空间是一个最大的界限,这时候直接取最小就可以了。
如何才能快速掌握onMeasure
呢?各种模式的关系还有调用机制,需要自己亲自去体验,写一个例子去调值来理解,多写多看其他控件的onMeasure
实现就能理解了,没有理解清楚的时候,如果要实现一个漂亮的onMeasure
参照上面的代码,按照需求定制就行了,一般情况下就够用了,慢慢的就有知道怎么回事了。
onLayout
先看看函数声明:onLayout(boolean changed, int left, int top, int right, int bottom)
,有5个参数,第一个的意思是控件的尺寸是否改变,如果前一次和这一次尺寸有变化,则是true
,为什么会有几次的说法呢?原因是存在界面刷新(分为系统刷新和手动刷新)或者手动修改控件尺寸的情况,而且android在绘制控件的时候,特别是层级复杂的时候这个方法会多次调用,这个值用于提高性能。后面4个值是相对于父控件的位置,比如left
则是相对于父控件的left的值,如果父控件相对于屏幕左边是100,那么子控件的位置相对于屏幕是100,但是这个left
值是可能是0,如果也是100的话,它相对于屏幕左边就是200的距离了。
一般情况下,如果自定义控件是没有子控件的,这个直接使用默认实现就好了,如果这个自定义控件本身可以包含子控件,那么可以在这里对子控件的位置进行摆放,很简单就是你想把子控件放哪里你就用子控件的layout()
方法摆放就完了:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
super.onLayout(changed, l, t, r, b);
ArrayList<PointF> points = mView.getKeyPoints(getPointCount());
for (int i = 1; i < getChildCount(); ++i)
{
View childAt = getChildAt(i);
if (childAt instanceof RewardView || childAt instanceof ImageView)
{
continue;
}
PointF point = points.get(i - 1);
final int start = (int) (mView.getPaddingLeft() + point.x);
childAt.layout(start - childAt.getWidth() / 2, (int) point.y - childAt.getHeight() / 2, start + childAt.getWidth() / 2, (int) point.y + childAt.getHeight() / 2);
}
if (points.size() > mArrowIndex + 1 && mArrowIndex != -1)
{
addArrow(points.get(mArrowIndex), points.get(mArrowIndex + 1));
}
}
后面会有一个完整的例子来说明如何onLayout
。
onDraw
绘制,跟onLayout
相反,这个主要是用于没有子控件的自定义控件用的,一般继承View
。通过Canvas
可以绘制各种图形,这里可以给控件添加想要的图形效果。
到此,三个步骤就介绍完了,接下来实现一个
构造函数
属性获取
实例
一些可能会遇到的坑
- 自定义属性和原生属性冲突