自定义View原理

一些基础知识

View简介

  • View就是显示在屏幕上的各种视图

  • View类是Android各种组件的基类

  • 视图View的分类

    1. 单一视图(例如:TextView、Button)
    2. 视图组:由多个单一视图组成的ViewGroup(例如LinearLayout、RelativeLayout)
  • View的构造函数一共有四个

    1. 1
      2
      //在Java代码中新建一个实例,则调用这个构造函数
      public View(Context context)
    2. 1
      2
      //如果view是在xml文件中声明的,则调用这个构造函数
      public View(Context context, @Nullable AttributeSet attrs)
    3. 1
      2
      //这个不会自动调用,一般来说这个是在第二个构造函数中主动调用(在view有style属性时)
      public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr)
    4. 1
      2
      3
      4
      5
      /**
      * api21之后才使用
      * 一般在第二个构造函数中主动调用
      */
      public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)
  • View的视图结构

  • 位置的获取

    • view的位置是通过view.get×××()获取的
    • MotionEvent中的位置获取
  • Android坐标系

  • 角度:在默认的屏幕坐标系中角度增大方向为顺时针

  • android中的颜色

    颜色模式 解释
    ARGB8888 四通道高精度(共32位),每个通道8位
    ARGB4444 四通道低精度(共16位)
    RGB565 Android屏幕默认的模式(16位)
    Alpha8 仅有透明通道(8位)
    类型 解释 取值范围(0x00-0xff)
    A(Alpha) 透明度 透明-不透明
    R(Red) 红色 无色-红色
    G(Green) 绿色 无色-绿色
    B(Blue) 蓝色 无色-蓝色

View的Measure过程

  1. 本过程作用:测量View的宽/高

  2. ViewGroup.LayoutParams类:用来传递宽/高的值的类

    ViewGroup的子类有相应的ViewGroup.LayoutParams子类,例如与RelativeLayoutParams对应的便是RelativeLayoutParams

    具体作用是用来记录视图的heightwidth等布局参数

  3. MeasureSpecs类:用来记录父布局对子视图测量要求的类

    测量规格(MeasureSpec) = 测量模式(mode)+ 测量大小(size)

    测量规格一共32位(int),其中测量模式占MeasureSpec的高2位,测量大小占MeasureSpec的低30位

  4. 测量模式

  5. MeasureSpec的值的计算

    子View的MeasureSpec值的计算是根据子View的LayoutParams和父容器的MeasureSpec值计算得来的。具体的逻辑都封装在getChildMeasureSpec()里

  6. ps:顶级View(DecorView)的测量规格MeasureSpec计算逻辑:取决于自身布局参数和窗口尺寸

measure过程详解

measure过程根据view的类型分为单一View的测量ViewGroup的测量

  1. 测量流程

  2. 这里重点讲一下getDefaultSize()方法,其他的请自行查阅源代码

    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
    /**
    * size:提供的默认大小
    * measureSpec:宽/高的测量规格(含模式 & 测量大小)
    */
    public static int getDefaultSize(int size, int measureSpec) {

    // 默认大小
    int result = size;

    // 获取宽/高测量规格的模式 & 测量大小
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    // 模式为UNSPECIFIED时,使用提供的默认大小 = 参数Size
    case MeasureSpec.UNSPECIFIED:
    result = size;
    break;

    // 模式为AT_MOST,EXACTLY时,使用View测量后的宽/高值 = measureSpec中的Size
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
    result = specSize;
    break;
    }

    // 返回View的宽/高值
    return result;
    }

    在这里的size是默认数值,那这个默认数值是怎么来的呢,先看看onMeasure()中,调用getDefaultSize()方法:getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)。这里就可以看出传进去的默认的大小是getSuggestedMinimumWidth()

    1
    2
    3
    protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth());
    }

    从这儿看,如果View没有设置背景,那么View的宽度就是mMinwidth

View的layout过程

流程图

源码分析

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/**
* 源码分析:layout()
* 作用:确定View本身的位置,即设置View本身的位置
*/
public void layout(int l, int t, int r, int b) {

// 当前视图的四个顶点
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;

// 1. 确定View的位置:setFrame() / setOpticalFrame()
// 即初始化四个值、判断当前View大小和位置是否发生了变化 & 返回
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

// 2. 若视图的大小 & 位置发生变化
// 会重新确定该View所有的子View在父容器的位置:onLayout()
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {

onLayout(changed, l, t, r, b);
// 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现->>分析3
// 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需重写实现
...

}

/**
* 作用:根据传入的4个位置值,设置View本身的四个顶点位置
* 即:最终确定View本身的位置
*/
protected boolean setFrame(int left, int top, int right, int bottom) {
...
// 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点
// 从而确定了视图的位置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;

mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

}

/**
* 作用:根据传入的4个位置值,设置View本身的四个顶点位置
* 即:最终确定View本身的位置
*/
private boolean setOpticalFrame(int left, int top, int right, int bottom) {

Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;

Insets childInsets = getOpticalInsets();

// 内部实际上是调用setFrame()
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
}
// 回到调用原处

/**
* 分析3:onLayout()
* 注:对于单一View的laytou过程
* a. 由于单一View是没有子View的,故onLayout()是一个空实现
* b. 由于在layout()中已经对自身View进行了位置计算,所以单一View的layout过程在layout()后就已 * 完成了
* changed 当前View的大小和位置改变了
* left 左部位置
* top 顶部位置
* right 右部位置
* bottom 底部位置
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

小细节

getWidth()(getHeight())和getMeasuredWidth()(getMeasuredHeight())有什么区别

  • getWidth()(getHeight()):获取View的最终的宽/高
    • 取值在measure过程
    • 取值方法:setMeasuredDimension()
  • getMeasuredWidth()(getMeasuredHeight()):获取的View的测量的宽/高
    • 取值在layout过程
    • 取值方法:layout中传递四个参数之间的运算
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 获得View测量的宽 / 高
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
// measure过程中返回的mMeasuredWidth
}

public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
// measure过程中返回的mMeasuredHeight
}


// 获得View最终的宽 / 高
public final int getWidth() {
return mRight - mLeft;
// View最终的宽 = 子View的右边界 - 子view的左边界。
}

public final int getHeight() {
return mBottom - mTop;
// View最终的高 = 子View的下边界 - 子view的上边界。
}

ps:一般情况下(屏幕可包裹内容,无人为设置),这两个获取的值是相等的,但是也可以人为强行设置

1
2
3
4
5
6
7
8
9
10
@Override
public void layout( int l , int t, int r , int b){

// 改变传入的顶点位置参数
super.layout(l,t,r+100,b+100);

// 如此一来,在任何情况下,getWidth() / getHeight()获得的宽/高 总比 getMeasuredWidth() / getMeasuredHeight()获取的宽/高大100px
// 即:View的最终宽/高 总比 测量宽/高 大100px

}

View的Draw过程

流程图

绘制流程

  1. 绘制view背景
    1. 绘制view内容
    2. 绘制子View
    3. 绘制装饰(渐变框,滑动条等等)

源码分析

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
/**
* 源码分析:draw()
* 作用:根据给定的 Canvas 自动渲染 View(包括其所有子 View)。
* 注:
* a. 在调用该方法之前必须要完成 layout 过程
* b. 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)
* c. 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制
* d. 若自定义的视图确实要复写该方法,那么需先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制
*/
public void draw(Canvas canvas) {

...// 仅贴出关键代码

int saveCount;

// 步骤1: 绘制本身View背景
if (!dirtyOpaque) {
drawBackground(canvas);
}

// 若有必要,则保存图层(还有一个复原图层)
// 优化技巧:当不需绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过
// 因此在绘制时,节省 layer 可以提高绘制效率
final int viewFlags = mViewFlags;
if (!verticalEdges && !horizontalEdges) {

// 步骤2:绘制本身View内容
if (!dirtyOpaque)
onDraw(canvas);
// View 中:默认为空实现,需复写
// ViewGroup中:需复写

// 步骤3:绘制子View
// 由于单一View无子View,故View 中:默认为空实现
// ViewGroup中:系统已经复写好对其子视图进行绘制我们不需要复写
dispatchDraw(canvas);

// 步骤4:绘制装饰,如滑动条、前景色等等
onDrawScrollBars(canvas);

return;
}
...
}