Android系统提供了丰富的视图组件,如TextView、ImageView、Button等,以及一些容器组件,如ConstraintLayout、LinearLayout、FrameLayout等,使用这些组件可以实现各种常见的视图效果。然而,在某些情况下,我们可能需要实现更加个性化和独特的展示效果,而系统提供的组件很难满足这种需求。这时候,就需要使用自定义View来实现了。

自定义View的实现流程大致分为以下几方面:

  1. 创建自定义View类继承自View/现有控件类
  2. View的测量过程onMeasure
  3. View的绘制过程onDraw

下面将依序实现各流程

创建自定义View类

继承View创建一个自定义View类

自定义View类可以直接继承自View实现,也可以根据业务场景直接继承自特定的控件。

重写构造方法

1
2
3
4
5
6
7
8
9
10
11
public class MyView extends View {
public MyView(Context context) {
super(context, null);
init(context);
}

public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
}

定义View的自定义属性

自定义属性属于styleable,这样我们就可以在xml布局文件中直接声明自定义View的属性来增加View的灵活性

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyView">
<attr name="MyText" format="string|reference" />
<attr name="MySize" format="dimension|reference" />
</declare-styleable>
</resources>

使用自定义属性

1
2
3
4
5
6
7
8
9
10
11
12
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray typedArray = context.obtainStyledAttributes(attrs,
R.styleable.MyView, defStyleAttr, 0);

text = typedArray.getString(R.styleable.MyView_customText);
dimension = typedArray.getDimension(R.styleable.MyView_customSize, 30);

typedArray.recycle();

//执行初始化逻辑
}

自定义View的测量:onMeasure

MeasureSpec

MeasureSpec是View的内部类,它封装了一个View的尺寸,在onMeasure()当中会根据这个MeasureSpec的值来确定View的宽高。

MeasureSpec的值保存在一个int值当中。一个int值有32位,前两位表示模式mode后30位表示大小size。即MeasureSpec =mode + size

MeasureSpec当中一共存在三种mode:UNSPECIFIEDEXACTLYAT_MOST

MeasureSpec.EXACTLY

在布局中指定了宽高 是一个确切的值(比如100dp 可以直接使用)对应match_parent。

MeasureSpec.ATMOST

AT_MOST是最大的意思,这时需要测量自己控件需要的的宽高,如果大于父控件的,那么就用父控件的大小,如果小于父控件,就使用父控件大小 总结下来就是尽可能地给最大 但是不能超过父控件。对应wrap_content。

MeasureSpec.UNSPECIFIED

表示尽可能大的显示在listview和scrollview中就是这样测量宽高的,因为是滚动控件,一般自己定义时很少用到 看scrollview的源码会看到它的模式是获取最大的显示方式。

通常,可以使用MeasureSpec类的getSize()getMode()方法来获取宽度和高度的建议值以及测量模式。调用setMeasuredDimension()方法将计算出的宽高传入。

1
2
3
4
5
6
// 获取测量模式(Mode)
int specMode = MeasureSpec.getMode(measureSpec)
// 获取测量大小(Size)
int specSize = MeasureSpec.getSize(measureSpec)
// 通过Mode 和 Size 生成新的SpecMode
int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);

在View当中,MeasureSpace的测量代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
//当父View要求一个精确值时,为子View赋值
case MeasureSpec.EXACTLY:
//如果子view有自己的尺寸,则使用自己的尺寸
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//当子View是match_parent,将父View的大小赋值给子View
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
//如果子View是wrap_content,设置子View的最大尺寸为父View
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父布局给子View了一个最大界限
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
//如果子view有自己的尺寸,则使用自己的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 父View的尺寸为子View的最大尺寸
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//父View的尺寸为子View的最大尺寸
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父布局对子View没有做任何限制
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
//如果子view有自己的尺寸,则使用自己的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//因父布局没有对子View做出限制,当子View为MATCH_PARENT时则大小为0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//因父布局没有对子View做出限制,当子View为WRAP_CONTENT时则大小为0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

自定义View的绘制:onDraw

draw流程也就是把View绘制到屏幕上的过程,整个流程的入口在Viewdraw()方法之中,而源码注释也写的很明白,整个过程可以分为6个步骤。

  1. 如果需要,绘制背景。
  2. 有过有必要,保存当前canvas。
  3. 绘制View的内容。
  4. 绘制子View。
  5. 如果有必要,绘制边缘、阴影等效果。
  6. 绘制装饰,如滚动条等等。

自定义控件被创建并且测量代码写好后,接下来就调用onDraw()来绘制View。以下是一个绘制圆形的示例:

1
2
3
4
5
6
7
8
9
@Override
protected void onDraw(Canvas canvas) {
//调用父类的onDraw函数,因为View这个类实现了一些基本的绘制功能,比如绘制背景颜色和背景图片
super.onDraw(canvas);
//半径
int r = getMeasuredWidth()/2;
int centerX = getLeft() + r;
int centerY = getTop() + r;
canvas.drawCircle(centerX,centerY,r,mPaint);