View的绘制流程浅析

参考:

前言

要彻底的掌握自定义View, 当然要理解 View 的测量、布局及绘制原理, 现在才发现以前写的那些自定义View其实也只是一知半解, 所以开一篇博客特意梳理总结一下.

View的绘制流程

首先看一下Activity的架构图

我们的Activity布局就在这个ContentView里面
看一下用Layout Inspector探测的布局

在Activity中我们通过setContentView所设置的布局文件其实就是被加到内容栏之中, 而内容栏的id是content, 我们的布局的确加到了id为content的FrameLayout中.
如何得到DecorView?可以这样:

1
View decorView = activity.getWindow().getDecorView();

如何得到content?可以这样:

1
FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content);

如果要得到我们设置的View, 就可以这样

1
View contentView = content.getChildAt(0);

ViewRoot对应于ViewRootImpl类, 它是连接WindowManager和DecorView的纽带, View的三大流程均是通过ViewRoot来完成的. View的绘制流程是从ViewRootImpl的performTraversals方法开始的, 它经过measure、layout和draw三个过程才能最终才将一个View画出来.
measure确定View的测量宽/高, layout确定View的最终宽/高和四个顶点的位置, draw将View绘制到屏幕上.
View的绘制流程大概能够以下面这张图表示

performTraversals会依次调用performMeasure, performLayout和performDraw三个方法, 这三个方法依次分别完成顶级View的measure, layout和draw这三大流程, 其中在performMeasure中会调用measure方法, 在measure中又会调用onMeasure方法, 在onMeasure方法中则会对所有的子元素进行measure过程, 这个时候measure流程就从父元素传递到子元素了, 这样就完成了一次measure过程.接着子元素会重复父容器的measure过程, 如果反复就完成了整个View树的遍历.同理performLayout和performDraw的传递路程和performMeasure是类似的, 唯一不同的是, performDraw的传递是在draw方法中通过dispatchDraw来实现的, 不过这并没有本质区别.
我们还是简略的看一下源码吧.

这是performTraversals方法

measure流程

这是在performTraversals里的performMeasure方法

刚才说到performTraversal会调用顶级View的方法, 在这里打断点可知, 这个顶级View就是DecorView.
我们再跳转到View的measure()方法里

可以看到高亮处, onMeasure方法被调用了, 我们跳转到onMeasure方法看看.

可以看到onMeasure中有一个默认实现, 但是在写自定义View的时候, 我们很多时候都会重写这个onMeasure方法

理解MeasureSpec

我们看到onMeasure方法中有两个参数, widthMeasureSpec和heightMeasureSpec, 那么MeasureSpec是什么意思呢?
MeasureSpec翻译过来叫”测量说明书”.
MeasureSpec代表一个32位的int值, 高2位代表SpecMode, 低30位代表SpecSize.


MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配, 为了方便操作, 提供了打包和解包的方法.我们自己写一个类测试一下:

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
public class MeasureSpecTest {

private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;

public static void main(String[] args) {
String modeMask = Integer.toBinaryString(MODE_MASK);
System.out.println("modeMask = " + modeMask);
String exactly = Integer.toBinaryString(EXACTLY);
System.out.println("exactly = " + exactly);

String atMost = Integer.toBinaryString(AT_MOST);
System.out.println("atMost = " + atMost);

String unSpecified = Integer.toBinaryString(UNSPECIFIED);
System.out.println("unSpecified = " + unSpecified);
System.out.println();

System.out.println("打包");
int measureSpec = makeMeasureSpec(1233, EXACTLY);
System.out.println("measureSpec = " + Integer.toBinaryString(measureSpec));

System.out.println("解包");
int mode = getMode(measureSpec);
System.out.println("mode = " + Integer.toBinaryString(mode));

int size = getSize(measureSpec);
System.out.println("size = " + size);
System.out.println("size = " + Integer.toBinaryString(size));
}

public static int makeMeasureSpec(int size, int mode) {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}

public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}

public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
modeMask = 11000000000000000000000000000000
exactly = 1000000000000000000000000000000
atMost = 10000000000000000000000000000000
unSpecified = 0

打包
measureSpec = 1000000000000000000010011010001
解包
mode = 1000000000000000000000000000000
size = 1233
size = 10011010001

可以看到 EXACTLY是01后面跟30个0, AT_MOST是10后面跟30个0, UNSPECIFIED是00后面跟30个0.具体的计算我就不说了, 其实也很简单, 主要涉及左移, 非, 以及最重要的通过来按位与来屏蔽某些位的操作.
下面来说说SpecMode的含义:

  • UNSPECIFIED
    父容器不对View有任何限制, 要多大给多大, 这种情况一般用于系统内部, 表示一种测量状态(listView和scrollView等)
  • EXACTLY
    父容器已经检测出View所需要的精确大小, 这个时候View的最终大小就是SpecSize所指定的值.它对应于LayoutParams中的match_parent和具体的数值这两种模式
  • AT_MOST
    父容器指定了一个可用大小即SpecSize, View的大小不能大于这个值, 具体是什么值要看不同View的具体实现, 它对应于LayoutParams中的wrap_content

我们再回过头去看onMeasure方法, onMeasure中的两个参数widthMeasureSpec和heightMeasureSpec都是从上级View传递进来的. 通过一定的处理(可以重写,自定义处理步骤),从中获取View的宽/高,调用setMeasuredDimension()方法,指定View的宽高,完成测量工作。

那么MeasureSpec又是如何确定的呢?
对于 DecorView,其确定是通过屏幕的大小,和自身的布局参数 LayoutParams。
我们看一下ViewRootImpl中传递给DecorView的layoutParams是如何确定的

这个lp.with中的lp是一个WindowManager.LayoutParams, mWidth就是窗口的大小了

我们在看看getRootMeasureSpec()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {

case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}

对于其他 View(包括 ViewGroup),系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec, 然后再根据这个measureSpec来测量出View的宽/高.
关于这里我们看一下ViewGroup的getChildMeasureSpec方法

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
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) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

可以看到, 这个方法的主要作用是根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec. 将结论整理成表格就是这样:

一般在自定义View时,当View的LayoutParams的布局格式是wrap_content,需要重写onMeasure方法,处理wrap_content时的情况,进行特别指定。这是为什么呢?我们看一下View的onMeasure()方法

1
2
3
4
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

看一些getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

从上面的表格可知, 当View的布局格式是wrap_content时, View的specMode是AT_MOST, 而这里, case MeasureSpec.AT_MOST时, 得到的宽度是specSize, 再从上面的表格可知, 当View的布局格式是wrap_content时, View的specSize是parentSize, 就是父容器当前剩余的空间大小.这种效果和使用match_parent完全一致, 所以需要我们需要重写onMeasure方法,处理wrap_content时的情况,进行特别指定。
下面看看ViewGroup中的measure流程, 以RelativeLayout举例, 在它的onMeasure()方法中:

再跟踪这个measureChild方法

可以看到最终调用了child.measure()方法, 也就是View的measure()方法, 这是测量流程就从当前RelativeLayout传递到子元素了.
看一下测量的完整流程, 需要说明的是这个measureChildre方法并不是必须调用的, 它只是ViewGroup提供的一个方法, 可以用它来方便的测量子View, 就不用自己再去写循环遍历测量的逻辑了.

在完全理解了View的测量流程之后下面提两个问题

  1. getMeasuredHeight()和getHeight()有什么区别?
  2. 在Activity的生命周期中, 因为View还没有测量完毕, 我们无法直接得到View的宽高, 有一种解决方法是手动对View进行测量, view.measure(0,0), 然后就能获得宽高了, 这是为什么呢?
    首先解决第一个问题, 我们看一下getMeasureHeight()的源码
1
2
3
4
5
6
7
8
9
10
/**
* Like {@link #getMeasuredHeightAndState()}, but only returns the
* raw height component (that is the result is masked by
* {@link #MEASURED_SIZE_MASK}).
*
* @return The raw measured height of this view.
*/
public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
}

可以看到注释写的是返回View的原始的测量高度
再看看getHeight()的源码

1
2
3
4
5
6
7
8
9
/**
* Return the height of your view.
*
* @return The height of your view, in pixels.
*/
@ViewDebug.ExportedProperty(category = "layout")
public final int getHeight() {
return mBottom - mTop;
}

返回的是View的mBottom减去mTop, 而mBottom和mTop是在onLayout()确定的.现在我就直接说结论了:

  1. getMeasuredHeight方法获得的值是setMeasuredDimension方法设置的值,它的值在measure方法运行后就会确定, 是View的测量高度
  2. getHeight方法获得是layout方法中传递的四个参数中的mRight-mLeft,它的值是在layout方法运行后确定的, 是View的最终高度
  3. 一般情况下在onLayout方法中使用getMeasuredHeight方法,而在除onLayout方法之外的地方用getHeight方法
  4. 一般情况下getMeasureHeight和getHeight是相等的
    所以不要再相信网络流传的那种所谓的”getHeight是获取控件在屏幕中的高度, getMeasureHeight是获取控件的完整高度”那种说法了, 要么实践, 要么看源码.
    可以看这一篇 Android开发之getMeasuredWidth和getWidth区别从源码分析

关于第二个问题, 这里的情况比较复杂, 我们先看一下View.measure()方法

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
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}

// Suppress sign extension for the low bytes
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

// Optimize layout by avoiding an extra EXACTLY pass when the view is
// already measured as the correct size. In API 23 and below, this
// extra pass is required to make LinearLayout re-distribute weight.
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

if (forceLayout || needsLayout) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

resolveRtlPropertiesIfNeeded();

int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}

mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}

mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;

mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

这里在measure()方法中会进入onMeasure()方法

1
2
3
4
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

因为我们传入的widthMeasureSpec和heightMeasureSpec都是0, 所以这里的widthMeasureSpec和heightMeasureSpec也都是0, 我们看一下getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

这边传入的size是getSuggestedMinimumWidth(), 一般我们如果没有设置backgroud或者minwidh的话, 这个size就是0.因为传入的measureSpec是0, 所以解包出来的specMode和specSize也都是0, specMode对应MeasureSpec.UNSPECIFIED, 返回结果size, 也就是0.但是因为很多控件都会重写onMeasure方法, 所以这里返回的结果并不一定都是0.下面就说一下结论:
根据View的LayoutParams来分:

  1. match_parent
    直接放弃,无法measure出具体的宽/高。原因很简单,根据view的measure过程,构造此种MeasureSpec需要知道parentSize,即父容器的剩余空间,而这个时候我们无法知道parentSize的大小,所以理论上不可能测量处view的大小。
  2. wrap_content
1
2
3
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);

注意到(1<<30)-1,我们知道MeasureSpec的前2位为mode,后面30位为size,所以说我们使用最大size值去匹配该最大化模式,让view自己去计算需要的大小。

  1. 具体的数值(dp/px)
    这种模式下,只需要使用具体数值去measure即可,比如宽/高都是100px:
1
2
3
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);

Layout流程

测量完View大小后,就需要将View布局在Window中,View的布局主要通过确定上下左右四个点来确定的。

其中布局也是自上而下,不同的是ViewGroup先在layout()中确定自己的布局,然后在onLayout()方法中再调用子View的layout()方法,让子View布局。在Measure过程中,ViewGroup一般是先测量子View的大小,然后再确定自身的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void layout(int l, int t, int r, int b) {  

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

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

//如果视图的大小和位置发生变化,会调用onLayout()
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {

// onLayout():确定该View所有的子View在父容器的位置
onLayout(changed, l, t, r, b);
...

}

确定了自身的位置后,就要通过onLayout()确定子View的布局。onLayout()是一个可继承的空方法。

1
2
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

如果当前View就是一个单一的View,那么没有子View,就不需要实现该方法。

如果当前View是一个ViewGroup,就需要实现onLayout方法,该方法的实现个自定义ViewGroup时其特性有关,必须自己实现。

由此便完成了一层层的的布局工作。

View的布局流程:

Draw流程

View的绘制过程遵循如下几步:

  1. 绘制背景 background.draw(canvas)

  2. 绘制自己(onDraw)

  3. 绘制Children(dispatchDraw)

  4. 绘制装饰(onDrawScrollBars)

从源码中可以清楚地看出绘制的顺序。

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
public void draw(Canvas canvas) {
// 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)
// 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制。
// 如果自定义的视图确实要复写该方法,那么需要先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制。
...
int saveCount;
if (!dirtyOpaque) {
// 步骤1: 绘制本身View背景
drawBackground(canvas);
}

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

if (!dirtyOpaque)
// 步骤2:绘制本身View内容 默认为空实现, 自定义View时需要进行复写
onDraw(canvas);

......
// 步骤3:绘制子View 默认为空实现 单一View中不需要实现,ViewGroup中已经实现该方法
dispatchDraw(canvas);

........

// 步骤4:绘制滑动条和前景色等等
onDrawScrollBars(canvas);

..........
return;
}
...
}

无论是ViewGroup还是单一的View,都需要实现这套流程,不同的是,在ViewGroup中,实现了 dispatchDraw()方法,而在单一子View中不需要实现该方法。自定义View一般要重写onDraw()方法,在其中绘制不同的样式。
值得一提的是, 重写ViewGroup的onDraw()方法, ViewGroup默认是不进行绘制的, 如果需要通过onDraw来绘制内容, 需要调用setWillNotDraw(false)这句代码来显式地关闭WILL_NOT_DRAW这个标记位
View绘制流程:

总结

从View的测量、布局和绘制原理来看,要实现自定义View,根据自定义View的种类不同,可能分别要自定义实现不同的方法。但是这些方法不外乎:onMeasure()方法,onLayout()方法,onDraw()方法。

onMeasure()方法:单一View,一般重写此方法,针对wrap_content情况,规定View默认的大小值,避免于match_parent情况一致。ViewGroup,若不重写,就会执行和单子View中相同逻辑,不会测量子View。一般会重写onMeasure()方法,循环测量子View。

onLayout()方法:单一View,不需要实现该方法。ViewGroup必须实现,该方法是个抽象方法,实现该方法,来对子View进行布局。

onDraw()方法:无论单一View,或者ViewGroup都需要实现该方法,因其是个空方法

0%