Android---View的工作流程

摘要:一直关注 Hencoder 的教程,前阵子刚好出了一期 View 的工作流程系列,然后结合《Android开发艺术探索》相关章节,做一下笔记。

MeasureSpec

在很大程度上,MeasureSpec 决定了一个 View 的尺寸,作所以说“很大程度上”,是因为这个过程还会受到父容器的影响,因为父容器会硬性 View 的 MeasureSpec 的创建过程。在测量过程中,系统会将 View 的 LayoutParams 根据父容器施加的规则转换成 MeasureSpec,然后再根据这个 MeasureSpec 来测量出 View 的宽高。

MeasureSpec 是一个 32 位的 int 值,高 2 位代表SpecMode,低30位代表SpecSize。

  • SpecMode:测量模式
  • SpecSize:在某一测量模式下的规格大小
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public static class MeasureSpec {
    ...
    public static int makeMeasureSpec(int size,int mode) {
    if (sUseBrokenMakeMeasureSpec) {
    return size + mode;
    } else {
    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);
    }
    }
    ...

MeasureSpec 通过将 SpecMode 和 SpecSize 打包成一个 int 值来避免过多的对象内存,为了方便操作,其提供了打包和解包的方法。

SpecMode有三类,

UNSPECIFIED

父容器不对 View 有任何显示,要多大给多大,这种模式一般是用于系统内部绘制。

EXACTLY

对应于 LayoutParams 的 match_content 和具体数值两种情况。表示父容器已经检测出 View 所需要的精确大小,这个大小由 SpecSize 给出。

AT_MOST

对用于 LayoutParams 中的 wrap_content 模式。表示父容器制定了一个可用大小,即 SpecSize,View的大小不能大于这个值,具体是多少要看不同View的具体实现。

MeasureSpec 和 LayoutParams 的对应关系

在View 测量的时候,系统会将 View 的 LayoutParams 参数在父容器的约束之下转换成MeasureSpec,然后根据这个 MeasureSpec 来决定 View 测量后的宽高。什么叫做父容器的约束呢?也就父容器的 MeasureSpec,所以对于普通 View 来说,其 MeasureSpec 就是由父容器的MeasureSpec 和其自身的 LayoutParams 共同决定的。

上面说的是一个总结,这个总结,体现在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
69
70
71
72
73
74
75
76
77
/**
* @param spec The requirements for this view
* @param padding The padding of this view for the current dimension and
* margins, if applicable
* @param childDimension How big the child wants to be in the current
* dimension
* @return a MeasureSpec integer for the child
*/
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);
}

这个方法看上去这么长,其实总结起来就是上面那句话:View 的 LayoutParams 参数在父容器的约束之下转换成 MeasureSpec。

这个方法是在 measureChildWithMargins() 方法中会调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
  • 在第 7 行可以看到,调用了 getChildMeasureSpec 方法,这个方法传入的第一个参数是parentWidthMeasureSpec,第三个参数是lp.width,这就充分说明上面的总结:一个 View 的MeasureSpec和其本身的LayoutParams和父容器的 MeasureSpec 相关。
  • 最后一行,是拿到 View 的 MeasureSpecHeight 和 MeasureSpecWidth 值,去调用View的measure 方法,View 的 measure 方法放在下一节。

这么说起来,还是有点模糊,总结如下(也就是 getChildMeasureSpec 方法的表格呈现形式):

其中 parentSize 指的是父容器中当前可用的大小

image

按子View的LayoutParams总结如下:

  • 当View采用固定宽高的时候,无论父容器的SpecMode是什么,View的SpecMode都是EXACTLY,SpecSize遵循LayoutParams中的大小。
  • 当View采用match_parent时,如果父容器的SpecMode是EXACTLY,那么View的SpecMode也是EXACTLY,SpecSize是父容器的剩余空间;如果父容器的SpecMode是AT_MOST,那么View的SpecMode也是AT_MOST,并且SpecMode不会超过父容器的剩余空间。
  • 当View采用wrap_content时,无论父容器的SpecMode是什么,View的SpecMode都是AT_MOST,SpecSize不得超过父容器的剩余空间。

View的工作流程

View的工作流程主要包括measure、layout、draw三个,即测量布局和绘制,其中measure确定View的测量宽高,layout确定View的最终宽高和四个顶点的位置,而draw将View绘制到屏幕上。

measure过程

measure() 方法被父 View 调用,在 measure() 中做一些准备和优化工作后,调用 onMeasure() 来进行实际的自我测量。 onMeasure() 做的事,View 和 ViewGroup 不一样:

  • View:View 在 onMeasure() 中会计算出自己的尺寸然后保存;
  • ViewGroup:ViewGroup 在 onMeasure() 中会调用所有子 View 的 measure() 让它们进行自我测量,并根据子 View 计算出的期望尺寸来计算出它们的实际尺寸和位置(实际上 99.99% 的父 View 都会使用子 View 给出的期望尺寸来作为实际尺寸)然后保存。同时,它也会根据子 View 的尺寸和位置来计算出自己的尺寸然后保存;

那么久针对 View 和 ViewGroup 这两种情况分析了。

View 的 measure 过程

View 的 measure 过程是由其 measure 方法完成的,在这个方法中又会去调用 onMeasure 方法,onMeasure实现:

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

就是调用了一个 setMeasureDimension 方法,将 View 的宽高传递进去,这个方法在自定义 View 的时候经常用到,就是在确定了自定义 View 的宽高值之后,在 onMeasure 方法中最后调用的,用于确定自定义 View 的测量宽高。

这里对宽高传入的都是 getDefaultSize() 函数的返回值,那么久看看这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}
  • 就是根据 specMode 的不同值,返回不同的大小,当 AT_MOST 和 EXACTLY 模式下,就是返回 specSize 的值,也就是 View 测量后的大小。
  • 在 UNSPECIFIED 模式下,View 的大小就是 getDefaultSize 方法的第一个参数 size ,即宽高分别为 getSuggestedMinimumWidth() 和 getSuggestedMinimumHeight() 这两个函数的返回值。在看下这两个函数(只贴出width的代码):
1
2
3
4
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
  • 若 View 没有指定背景,那么 View 的宽度为 mMinWidth ,这个值是由 View 的 android:minWidth 属性指定的,若没有指定这个属性,那么这个 mMinWidth 为 0。
  • 若 View 指定了背景,那么 View 的宽度就是 mMinWidth 和 mBackground.getMinimumWidth() 两者中较大的一个。前者上面已经说了是什么,那么后者又是什么东西呢?mBackground 是一个 Drawable,那么点进 Drawable 里面去看就知道了:
1
2
3
4
5
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

getMinimumWidth 返回的就是 Drawable 的原始高度,前提是这个 Drawable 有原始高度,不然就返回0;

ViewGroup 的 measure 过程

ViewGroup 是一个抽象类,因此他没有重写 View 的 onMeasure 方法,它提供了一个 measureChildren 方法:

1
2
3
4
5
6
7
8
9
10
11
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}

这个方法就是调用 ViewGroup 的所有子 View 的 measureChild 方法,这个 measureChild方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

就是拿到子 View 的 LayoutParams ,然后通过 getChildMeasureSpec 方法生成子 View 的 MeasureSpec,接着就将生成的 MeasureSpec 直接传递给子 View 的 measure 方法进行测量。 getChildMeasureSpec 的逻辑上述已经说明。

可以发现 ViewGroup 并没有定义其本身具体的测量过程,因为 ViewGroup 是一个抽象类, onMeasure 需要各个具体的子类去实现,不想 View 一样,对 onMeasure 方法做具体实现,是因为不同的 ViewGroup 的实现类,有不同的布局特性,这导致他们的测量细节各不相同,比如 LinearLayout 和 RelativeLayout 两者的布局特性显然不同。

layout 过程

  • layout 方法确定 View 本身的位置
  • onLayout 方法确定子 View 的位置

layout 的作用是 ViewGroup 用于确定子 View 的位置,当 ViewGroup 的位置确定了之后,它会在 onLayout 中遍历所有子 View ,并且调用其 layout 方法,而在子 View 的 layout 方法中,onLayout 方法又会被调用,先看 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
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
...
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
...
}
  • 14 行调用了 setFrame 方法,这个方法就是用于确定 View 的四个顶点的位置,一旦四个顶点确定了,那么 View 在 ViewGroup 中的位置也就确定了。贴出 setFrame 中的一段代码,稍后用于说明问题。
1
2
3
4
5
6
7
8
9
protected boolean setFrame(int left, int top, int right, int bottom) {
...
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
...
}
  • 17 行,会调用 onLayout 方法,这个方法的用途是父容器确定子 View 的位置,和 onMeasure 方法类似,onLayout 的实现和具体的布局相关,所以 View 和 ViewGroup 都没有实现这个方法。看看 LinearLayout 中的 onLayout 方法:
1
2
3
4
5
6
7
8
9
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
  • 这里分为竖直方向上的 layout 和水平方向上的 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
void layoutVertical(int left, int top, int right, int bottom) {
final int count = getVirtualChildCount();
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
...
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
  • 首先遍历竖直方向上的所有子 View ,并且调用 setChildFrame 方法来为子 View 确定位置
  • 注意 childTop 值会逐渐增加,这个增量包括分割线宽度、margin 值、childHeight,这样一来,在竖直方向上就符合 LinearLayout 的特性了。

接下来看看 setChildFrame 方法:

1
2
3
4
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
  • 就是直接调用子 View 的 layout 方法,这样 LinearLayout 是父容器,父容器在 layout 中完成自己的定位之后,就通过 onLayout 去调用子 View 的 layout 方法,让子 View 完成其对自身的 layout 过程,然后在子 View 的 layout 方法中,又会通过 onLayout 方法去调用下一级子 View 的 layout 方法… 这样一层一层的传递下去之后,就会遍历完整个 View 树。

测量宽高和最终宽高的区别

这个问题可以具体为:getMeasureWidth/height 和 getWidth/height 有什么区别。

前者很明显,就是 measure 过程中得到的宽高,那么重点在后者,先看看 View 中的 getWidth 方法:

1
2
3
4
public final int getWidth() {
return mRight - mLeft;
}

现在就是要搞清楚 mRight 和 mLeft 两个变量是在什么时候赋值的。

  • 还是看看 LinearLayout 的竖直方向的 layout 过程,也就是上面的 layoutVertical 方法,在第 9、10 行可以看到:
1
2
3
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
  • 然后在 22 行的 setChildFrame 方法,将 childWidth 和 childHeight 作为参数传入,
1
2
3
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
  • 然后在 setChildFrame 中会去调用子 View 的 layout 方法,继续讲参数传递
1
2
child.layout(left, top, left + width, top + height);
  • 在 View 的 layout 方法中会调用 setFrame(l, t, r, b),这里的 l、t、r、b 和上面的参数对应,在 setFrame 中:
1
2
3
4
5
6
7
8
9
protected boolean setFrame(int left, int top, int right, int bottom) {
...
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
...
}

这段代码之前提到过,在这里,就将 mLeft mTop mRight mBottom 给赋值了,这个值就是在 LinearLayout 中通过 getMeasureWidth 和 getMeasureHeight 方法得到的。

  • 现在可以知道区别了在 View 的默认实现中,View 的测量宽高和最终宽高是相等的,只是两者的赋值时机不同,测量宽高形成于View 的 measure 过程,而最终宽高形成于View 的 layout 过程,在日常开发中,就可以认为 View 的测量宽高就等于 View 的最终宽高。

  • 一个好习惯就是:在 onLayout 方法中去拿 View 的测量宽高或者是最终宽高,因为在某些极端的情况下,系统需要经过多次的 measure 才能确定最终的宽高,这种情况下,在 onMeasure 方法中拿到的测量宽高可能是不准确的。

draw 过程

一个完整的绘制过程会依次绘制以下几个内容:

  1. 背景
  2. 主体(onDraw())
  3. 子 View(dispatchDraw())
  4. 滑动边缘渐变和滑动条
  5. 前景

一般来说,一个 View(或 ViewGroup)的绘制不会这几项全都包含,但必然逃不出这几项,并且一定会严格遵守这个顺序。例如通常一个 LinearLayout 只有背景和子 View,那么它会先绘制背景再绘制子 View;一个 ImageView 有主体,有可能会再加上一层半透明的前景作为遮罩,那么它的前景也会在主体之后进行绘制。需要注意,前景的支持是在 Android 6.0(也就是 API 23)才加入的;之前其实也有,不过只支持 FrameLayout,而直到 6.0 才把这个支持放进了 View 类里。

绘制背景

它的绘制发生在一个叫 drawBackground() 的方法里,但这个方法是 private 的,不能重写,你如果要设置背景,只能用自带的 API 去设置(xml 布局文件的 android:background 属性以及 Java 代码的 View.setBackgroundXxx() 方法),而不能自定义绘制

绘制主体

这个过程是在 onDraw 方法中执行的,但是在 View 中,这个方法是没有实现的,因为具体的 View 需要如何绘制,需要 View 的子类去具体的定制。所以当我们自定义 View 的绘制的时候,就就可以直接重写 onDraw 方法。

1
2
3
4
5
6
7
8
9
10
11
12
public class AppView extends View {
...
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
... // 自定义绘制代码
}
...
}

这里注意,是将自定义绘制的代码写在 super 的下面,不过这里写在 super 的上面和下面其实都是一样的, 因为上面提到,View 的这个方法是一个空实现,所以。

下面来讨论自定义 View 的绘制方法时,自定义的代码写在 super 上下的区别。

写在 super.onDraw() 的下面

把绘制代码写在 super.onDraw() 的下面,由于绘制代码会在原有内容绘制结束之后才执行,所以绘制内容就会盖住控件原来的内容。这是最为常见的情况:为控件增加点缀性内容。比如,在 Debug 模式下绘制出 ImageView 的图像尺寸信息:
image

写在 super.onDraw() 的上面

如果把绘制代码写在 super.onDraw() 的上面,由于绘制代码会执行在原有内容的绘制之前,所以绘制的内容会被控件的原内容盖住。 这种方式可以实现马克笔的效果:

image

绘制子 View

有部分的遮盖关系是无法通过 onDraw 方法来实现的,例如,你继承了一个 LinearLayout,重写了它的 onDraw() 方法,在 super.onDraw() 中插入了你自己的绘制代码,使它能够在内部绘制一些斑点作为点缀:

1
2
3
4
5
6
7
8
9
10
public class SpottedLinearLayout extends LinearLayout {
...
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
... // 绘制斑点
}
}

image

没毛病。

但是当添加了子 View 之后,

1
2
3
4
5
6
7
8
9
10
<SpottedLinearLayout
android:orientation="vertical"
... >
<ImageView ... />
<TextView ... />
</SpottedLinearLayout>

image

造成这种情况的原因是 Android 的绘制顺序:在绘制过程中,每一个 ViewGroup 会先调用自己的 onDraw() 来绘制完自己的主体之后再去绘制它的子 View。对于上面这个例子来说,就是你的 LinearLayout 会在绘制完斑点后再去绘制它的子 View。那么在子 View 绘制完成之后,先前绘制的斑点就被子 View 盖住了。

具体来讲,这里说的「绘制子 View」是通过另一个绘制方法的调用来发生的,这个绘制方法叫做:dispatchDraw()。也就是说,在绘制过程中,每个 View 和 ViewGroup 都会先调用 onDraw() 方法来绘制主体,再调用 dispatchDraw() 方法来绘制子 View

注:虽然 View 和 ViewGroup 都有 dispatchDraw() 方法,不过由于 View 是没有子 View 的,所以一般来说 dispatchDraw() 这个方法只对 ViewGroup(以及它的子类)有意义。

回到刚才的问题:怎样才能让 LinearLayout 的绘制内容盖住子 View 呢?只要让它的绘制代码在子 View 的绘制之后再执行就好了。

写在 super.dispatchDraw() 的下面

只要重写 dispatchDraw(),并在 super.dispatchDraw() 的下面写上你的绘制代码,这段绘制代码就会发生在子 View 的绘制之后,从而让绘制内容盖住子 View 了。

1
2
3
4
5
6
7
8
9
10
11
public class SpottedLinearLayout extends LinearLayout {
...
// 把 onDraw() 换成了 dispatchDraw()
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
... // 绘制斑点
}
}

image

写在 super.dispatchDraw() 的上面

同理,把绘制代码写在 super.dispatchDraw() 的上面,这段绘制就会在 onDraw() 之后、 super.dispatchDraw() 之前发生,也就是绘制内容会出现在主体内容和子 View 之间。而这个……

其实和前面讲的,重写 onDraw() 并把绘制代码写在 super.onDraw() 之后的做法,效果是一样的。

onDrawForeground()

滑动边缘渐变和滑动条以及前景,这两部分被合在一起放在了 onDrawForeground() 方法里,这个方法是可以重写的。

enter image description here

滑动边缘渐变和滑动条可以通过 xml 的 android:scrollbarXXX 系列属性或 Java 代码的 View.setXXXScrollbarXXX() 系列方法来设置;前景可以通过 xml 的 android:foreground 属性或 Java 代码的 View.setForeground() 方法来设置。而重写 onDrawForeground() 方法,并在它的 super.onDrawForeground() 方法的上面或下面插入绘制代码,则可以控制绘制内容和滑动边缘渐变、滑动条以及前景的遮盖关系。

写在 super.onDrawForeground() 的下面

如果你把绘制代码写在了 super.onDrawForeground() 的下面,绘制代码会在滑动边缘渐变、滑动条和前景之后被执行,那么绘制内容将会盖住滑动边缘渐变、滑动条和前景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AppImageView extends ImageView {
...
public void onDrawForeground(Canvas canvas) {
super.onDrawForeground(canvas);
... // 绘制「New」标签
}
}
<!-- 使用半透明的黑色作为前景,这是一种很常见的处理 -->
<AppImageView
...
android:foreground="#88000000" />

enter image description here

左上角的标签没有被前景遮盖住,而是保持了它本身的颜色

写在 super.onDrawForeground() 的上面

如果你把绘制代码写在了 super.onDrawForeground() 的上面,绘制内容就会在 dispatchDraw() 和 super.onDrawForeground() 之间执行,那么绘制内容会盖住子 View,但被滑动边缘渐变、滑动条以及前景盖住:

1
2
3
4
5
6
7
8
9
10
public class AppImageView extends ImageView {
...
public void onDrawForeground(Canvas canvas) {
... // 绘制「New」标签
super.onDrawForeground(canvas);
}
}

enter image description here

由于被黑色的前景给遮住了,这里看到的标签也是这种半透明的黑色

想在滑动边缘渐变、滑动条和前景之间插入绘制代码?

很简单:不行。

虽然这三部分是依次绘制的,但它们被一起写进了 onDrawForeground() 方法里,所以你要么把绘制内容插在它们之前,要么把绘制内容插在它们之后。而想往它们之间插入绘制,是做不到的。

draw() 总调度的方法

除了 onDraw() dispatchDraw() 和 onDrawForeground() 之外,还有一个可以用来实现自定义绘制的方法: draw()。

draw() 是绘制过程的总调度方法。一个 View 的整个绘制过程都发生在 draw() 方法里。前面讲到的背景、主体、子 View 、滑动相关以及前景的绘制,它们其实都是在 draw() 方法里的。

1
2
3
4
5
6
7
8
9
10
11
12
13
// View.java 的 draw() 方法的简化版大致结构(是大致结构,不是源码哦):
public void draw(Canvas canvas) {
...
drawBackground(Canvas); // 绘制背景(不能重写)
onDraw(Canvas); // 绘制主体
dispatchDraw(Canvas); // 绘制子 View
onDrawForeground(Canvas); // 绘制滑动相关和前景
...
}

从上面的代码可以看出,onDraw() dispatchDraw() onDrawForeground() 这三个方法在 draw() 中被依次调用,因此它们的遮盖关系也就像前面所说的——dispatchDraw() 绘制的内容盖住 onDraw() 绘制的内容;onDrawForeground() 绘制的内容盖住 dispatchDraw() 绘制的内容。而在它们的外部,则是由 draw() 这个方法作为总的调度。所以,你也可以重写 draw() 方法来做自定义的绘制。

enter image description here

写在 super.draw() 的下面

由于 draw() 是总调度方法,所以如果把绘制代码写在 super.draw() 的下面,那么这段代码会在其他所有绘制完成之后再执行,也就是说,它的绘制内容会盖住其他的所有绘制内容。

它的效果和重写 onDrawForeground(),并把绘制代码写在 super.onDrawForeground() 下面时的效果是一样的:都会盖住其他的所有内容。

当然了,虽说它们效果一样,但如果你既重写 draw() 又重写 onDrawForeground() ,那么 draw() 里的内容还是会盖住 onDrawForeground() 里的内容的。所以严格来讲,它们的效果还是有一点点不一样的。

但这属于抬杠……

写在 super.draw() 的上面

同理,由于 draw() 是总调度方法,所以如果把绘制代码写在 super.draw() 的上面,那么这段代码会在其他所有绘制之前被执行,所以这部分绘制内容会被其他所有的内容盖住,包括背景。是的,背景也会盖住它。

是不是觉得没用?觉得怎么可能会有谁想要在背景的下面绘制内容?别这么想,有的时候它还真的有用。

例如我有一个 EditText:

enter image description here

它下面的那条横线,是 EditText 的背景。所以如果我想给这个 EditText 加一个绿色的底,我不能使用给它设置绿色背景色的方式,因为这就相当于是把它的背景替换掉,从而会导致下面的那条横线消失:

1
2
3
4
<EditText
...
android:background="#66BB6A" />

enter image description here

在这种时候,你就可以重写它的 draw() 方法,然后在 super.draw() 的上方插入代码,以此来在所有内容的底部涂上一片绿色:

1
2
3
4
5
6
7
8
9
10
public AppEditText extends EditText {
...
public void draw(Canvas canvas) {
canvas.drawColor(Color.parseColor("#66BB6A")); // 涂上绿色
super.draw(canvas);
}
}

enter image description here

###draw 过程注意

关于绘制方法,有两点需要注意一下:

  1. 出于效率的考虑,ViewGroup 默认会绕过 draw() 方法,换而直接执行 dispatchDraw(),以此来简化绘制流程。所以如果你自定义了某个 ViewGroup 的子类(比如 LinearLayout)并且需要在它的除 dispatchDraw() 以外的任何一个绘制方法内绘制内容,你可能会需要调用 View.setWillNotDraw(false) 这行代码来切换到完整的绘制流程(是「可能」而不是「必须」的原因是,有些 ViewGroup 是已经调用过 setWillNotDraw(false) 了的,例如 ScrollView)。

  2. 有的时候,一段绘制代码写在不同的绘制方法中效果是一样的,这时你可以选一个自己喜欢或者习惯的绘制方法来重写。但有一个例外:如果绘制代码既可以写在 onDraw() 里,也可以写在其他绘制方法里,那么优先写在 onDraw() ,因为 Android 有相关的优化,可以在不需要重绘的时候自动跳过 onDraw() 的重复执行,以提升开发效率。享受这种优化的只有 onDraw() 一个方法。

###draw 过程总结

enter image description here

另外别忘了上面提到的那两个注意事项:

  1. 在 ViewGroup 的子类中重写除 dispatchDraw() 以外的绘制方法时,可能需要调用 setWillNotDraw(false);
  2. 在重写的方法有多个选择时,优先选择 onDraw()。

总结

对 View 的绘制过程都清楚了之后,就可以进行各种自定义 View 了,Hencoder 说过,自定义 View 无非就是三个:绘制、布局、触摸反馈

其中绘制和布局这里总结了,在这两个操作过程中会大量使用到 Paint Canvas 和 Property Animation,这些后面再做总结。

参考引用

[1] 《Android 开发艺术探索》

[2] HenCoder Android 开发进阶:自定义 View 1-5 绘制顺序

实践

自定义流式布局,刚好是对上面总结内容的一个很好的实践。

View 工作流程的相关实践项目

共82.3k字
0%
.gt-container a{border-bottom: none;}