前言
Android 开发过程中大家应该都有经历过需要自定义 View 的需求,虽然 Android 已经为我们提供了很多控件,但是为了更好的 UI 效果难免需要自己定义一些 View。所以本篇文章来讲一讲 View 的工作原理,一起探究一下 View 的测量、布局和绘制流程,了解 View 的工作原理,才是学会自定义 View 的第一步。
1. 从 Activity 探究 View 的布局
在前面的文章中,我们提到过顶层 View —— DecorView 是如何被创建,如何加载资源的。不明白的小伙伴可以看看这篇文章 View 系列 —— 小白也能看懂的 DecorView。那么创建好的 DecorView 怎么被加载到界面中显示呢?
这个问题需要涉及到一些 Activity 创建过程的一些知识,这部分会在后面详细写一篇文章记录。在这里我们来简单说一下,当我们启动了 Activity 后,系统最终会执行到 ActivityThread 的 handleLaunchActivity 方法中:
final Activity a = performLaunchActivity(r, customIntent);
这里我们只截取了重要一行代码,在 performLaunchActivity 中执行的就是 Activity 的创建逻辑,因此也会进行 DecorView 的创建,此时的 DecorView 只是进行了初始化,添加了布局文件,对用户来说,依然是不可见的。onCreate 结束后,我们来看下 onResume 对应的 handleResumeActivity 方法:
@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, String reason) {
...
// 1.performResumeActivity 回调用 Activity 的 onResume
if (!performResumeActivity(r, finalStateRequest, reason)) {
return;
}
...
final Activity a = r.activity;
...
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
// 2.获取 decorview
View decor = r.window.getDecorView();
// 3.decor 现在还不可见
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
...
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
// 4.decor 添加到 WindowManger中
wm.addView(decor, l);
} else {
a.onWindowAttributesChanged(l);
}
}
}
...
}
到这里,DecorView 已经被加载到了 Window 中,但是还要经过一些流程才能将 View 绘制出来,下面我们就来看看这一过程。
上面说到 DecorView 是通过 WindowManager 执行了 addView() 方法后加载到 Window 的,而该方法实际上是会最终调用到 WindowManagerGlobal 的 addView() 中:
// WindowManagerGlobal.addView()
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);
可以看到代码中先创建了 ViewRootImpl 实例,然后将 DecorView 作为参数传给了 ViewRootImpl,通过 setView 方法进行 View 的处理。最终会调用 performTraversals 方法完成对 View 的绘制。
在这期间还会经过 scheduleTraversals() 等流程,具体可以看这篇文章 终于搞明白了什么是同步屏障。
这里我们主要看一下 performTraversals 方法。该方法代码很长,我们挑重点来看一下:
private void performTraversals() {
...
if (!mStopped || mReportNextDraw) {
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
...
if (didLayout) {
performLayout(lp, mWidth, mHeight);
...
}
...
if (!cancelDraw && !newSurface) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
performDraw();
}
...
}
可以看到,performTraversals 主要就是通过 performMeasure、performLayout 和 performDraw 三个方法来进行 View 的宽高、位置和绘制的。这里也是 View 的三大流程的入口,话不多,说我们一起进入方法中详细了解一下。
2. Measure 解析
为了便于分析,我们将 performTraversals 中涉及到 performMeasure 的代码单拎出来看。
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
在这个方法中,大家应该可以看出 MeasureSpec 的身影,那么 MeasureSpec 到底是什么呢?
2.1 MeasureSpec
MeasureSpec 是位于 View 中的一个内部类,简单来说 MeasureSpec 其实代表的就是一个规格尺寸,可以将父容器的 MeasureSpec 和自身 View 的 LayoutParams 转换成 MeasureSpec,再在 onMeasure 的时候对其解包,来确定 View 的宽高。
private static final int MODE_SHIFT = 30;
// 11000000000000000000000000000000
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// 00000000000000000000000000000000
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// 01000000000000000000000000000000
public static final int EXACTLY = 1 << MODE_SHIFT;
// 10000000000000000000000000000000
public static final int AT_MOST = 2 << MODE_SHIFT;
// 打包操作
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
// 兼容 API 17
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
// 解包
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
上面就是 MeasureSpec 中比较重要的代码,通过方法可以看出来 MeasureSpec 主要包括 size 和 mode 两部分,通过 makeMeasureSpec 方法将 size 和 mode 打包成一个 int 值。
(1)mode:测量模式,包括 3 种。
UNSPECIFIED:未指定模式。父控件没有对自控件有任何的大小限制,想多大就多大。EXACTLY:精确模式。父控件已经指定了子控件的大小,子控件只能这么大。AT_MOST:最大模式。子控件最大不能超过父控件给定的 size 值。
(2)size:在某种测量模式下的规格大小。
举个例子,假如 size 为 100,此时 mode 为 AT_MOST 模式,那么执行 makeMeasureSpec 方法后:
// ~MODE_MASK
00111111111111111111111111111111
// size & ~MODE_MASK
00000000000000000000000001100100
// mode & MODE_MASK
10000000000000000000000000000000
// makeMeasureSpec 的值
10000000000000000000000001100100
可以看到,打包后的值为 10000000000000000000000001100100,高两位代表的就是 mode 的值,后面的 30 位就是 size 的值,是不是非常巧妙!把 size 和 mode 打包成一个值,可以减少对象的内存分配,使用位运算,效率也很高。并且 MeasureSpec 中还提供了解包的方法 getMode 和 getSize,使用起来也很方便。
总结一下,对于每一个 View 来说,其内部都有一个 MeasureSpec,用来保存该 View 的尺寸和规格。MeasureSpec 不是只由 View 的 LayoutParams 决定的,而是由父容器的 MeasureSpec 和自身的 LayoutParams 一起来决定的。这里可以先看一下 ViewGroup 中的 measureChild 方法,后面我们还会再次提到该方法:
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 的 MeasureSpec 和父容器的 MeasureSpec、自身的 LayoutParams 甚至是 padding 都存在关系。它们共同影响着子 View 的 MeasureSpec。
那么对于顶层 View DecorView 来说,它没有父容器,是如何确定 MeasureSpec 的呢?还记得我们前面看的涉及 performMeasure 方法的代码吗?
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
明白了 MeasureSpec 的含义,接下来就可以去看看 getRootMeasureSpec 方法里面做了什么了,该方法从名字就可以看出来是获取根 View 的 MeasureSpec,不就是 DecorView 吗?
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;
}
上面的代码相信大家都能理解,很显然 DecorView 的 MeasureSpec 是由窗口的尺寸和自身的 LayoutParams 一起决定的。明白了上面的内容,接下来就可以走进 Measure 的流程了。如果只是一个单独的 View,那么只需要执行其 measure 方法就可以完成其宽高的测量。但如果是 ViewGroup,需要去执行 ViewGroup 的 measure 方法,还要遍历其中的子 View 并分别执行 measure 方法,下面就让我们一起来看看吧。
2.2 View 的 Measure 过程
在上面的内容中,我们讲述了 DecorView 是如何被加载到 Window 中,若要在界面显示 DecorView,会 以ViewRootImpl 的 performTraversals 方法作为入口,经历 performMeasure、performLayout 和 performDraw 三个方法来进行 View 的宽高、位置和绘制。本节主要是对 Measure 过程进行分析,那就来看看 performMeasure 方法做了什么工作吧~
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
代码很清晰,performMeasure 内部会去调用 View 的 measure 方法,在 measure 方法的注释中有这样一句描述:
This is called to find out how big a view should be. The parent supplies constraint information in the width and height parameters.
The actual measurement work of a view is performed in onMeasure(int, int), called by this method. Therefore, only onMeasure(int, int) can and must be overridden by subclasses.
上面描述的含义就是 measure 方法实际的测量过程是在 onMeasure 方法中,并且 measure 方法是 final 的,所以只有 onMeasure 允许被重写。既然官方都明确表示实际的测量过程是在 onMeasure 中,那么节省时间,我们直接去 onMeasure 里面看一看。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
内容很简单,就只是调用了 setMeasuredDimension,setMeasuredDimension 方法也很清晰,即通过其传参设置 View 的宽高。因此我们的重点放在该方法的传参上,先看一下 getSuggestedMinimumWidth 方法的内部实现吧:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
如果 View 没有设置背景,那么就取值 mMinWidth,对应着布局文件里面的 android:minWidth 这个属性。没有指定的话,mMinWidth 默认是 0。如果有背景,就取 View 的 mMinWidth 和背景 drawable 的 最小宽度之间的最大值。
接下来看一看 getDefaultSize 方法:
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 方法,返回的是 View 的最小宽度,measureSpec 指的就是父容器施加约束后的 MeasureSpec。可以看到只有在 UNSPECIFIED 情况下,getDefaultSize 才会返回 getSuggestedMinimumWidth 的值,AT_MOST 和 EXACTLY 都是返回 specSize 也就是测量后的大小。
因此,我们可以得到一个结论,如果是直接继承自 View 的控件,wrap_content 和 match_parent 效果是一样的,因为这两种属性对应的 AT_MOST 和 EXACTLY 在 getDefaultSize 中都等于 specSize,所以需要自己重写 onMeasure 方法。
2.3 ViewGroup 的 Measure 过程
ViewGroup 是一个抽象类,它并没有实现 View 的 onMeasure 方法,而是将其交给具体的子类来实现。比如说 LinearLayout、RelativeLayout 等等。之所以不由 ViewGroup 统一进行实现,是由于不同的布局特性不同,测量细节也不同,ViewGroup 没有办法做一个很好的统一。 但是 ViewGroup 提供了一个测量子 View 的方法 measureChildren。
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) {
// 遍历每个子 View 调用 measureChild
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
// 自身 LayoutParams
final LayoutParams lp = child.getLayoutParams();
// 根据父容器的 MeasureSpec 和自身 LayoutParams 等信息,获取到子 View MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
逻辑很简单,遍历 ViewGroup 中的每一个子 View,执行 measureChild 方法,在该方法中,会根据自身的
LayoutParams 和 父容器的 MeasureSpec 包括 padding 信息,一起计算得到子 View 的 MeasureSpec 值。接着就可以调用上一节说到的 View 的 measure 方法进行宽高的测量了。
对 ViewGroup 子类是如何重写 onMeasure 方法的细节,本篇文章就不做分析了,后面会单独写一篇文章详细说明。
3. Layout 解析
测量完 View 的宽高后,接下来会对 View 的位置进行确定,也就是 Layout 的过程。在 performLayout 中会调用到 View 的 layout 方法:
// 传参 View 相对于父容器上下左右的距离。
public void layout(int l, int t, int r, int b) {
...
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);
...
}
...
}
这里简化了一下 layout 方法,首先 isLayoutModeOptical 就是判断当前 layout 的模式,对应了 android:layoutMode,根据 isLayoutModeOptical 的结果,可能会执行 setOpticalFrame 或 setFrame,而 setOpticalFrame 的内部最终也是执行 setFrame 方法:
// 如果新的大小和位置与之前的不同,则为 true
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
// 将新旧进行对比,只要不相同就说明 View 的位置发生了变化
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
// 计算新旧位置下的宽高,判断尺寸是否有改变
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
invalidate(sizeChanged);
// 将新的位置参数赋值
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
// 渲染
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
...
return changed;
}
setFrame 方法会更新 mLeft,mTop,mRight,mBottom 的值,将其与新的坐标值相比较,返回是否发生了位置的改变。执行过该方法后,View 的新位置就确定了,接下来就会执行 onLayout 方法:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
View 的 onLayout 方法是一个空方法,那去看看它的子类 ViewGroup 的 onLayout 方法吧,好家伙也是空的。看来 onLayout 方法是需要自己去实现的,既然如此,可以简单看一下 LinearLayout 的 onLayout 方法:
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);
}
}
// 以 layoutVertical 为例
void layoutVertical(int left, int top, int right, int bottom) {
int childTop;
int childLeft;
...
// 1. 根据父容器的 gravity 属性,确定子 View 的起始位置
switch (majorGravity) {
case Gravity.BOTTOM:
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
// 2. 遍历所有的子 View
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();
// 3. 子 View 的 gravity 值
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
// 4. 根据 gravity 的值获取水平方向上的起始位置
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
// 5. childTop 会不断的累加,使得子 View 可以一个接着一个的垂直排列下去
childTop += lp.topMargin;
// 6. setChildFrame 方法会调用 View 的 layout,给子 View 设置位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
注释 1 处会根据父容器的 gravity 来确定子 View 的起始位置,确定了该位置后,就可以遍历所有的子 View了。注释 3 处根据子 View 的 gravity 确定水平方向上的起始距离,然后可以通过布局的设置,不断去增加 childTop 的值,确定了其位置后,就可以在注释 6 处调用 setChildFrame 方法,其内部就是 View 的layout 方法来给子 View 设置位置了。因为 childTop 的不断累加,可以使子 View 在竖直方向上一个接一个的排列下去,不会重叠。
4. Draw 解析
结束了 View 的测量和位置判定后,接下来就是对 View 的绘制了,入口是在 performDraw 方法中,会调用 View 的 draw 方法,该方法的源码中给出了非常清晰的注释,说明了 draw 方法的各个步骤:
(1)绘制背景
(2)如果需要的话,保存当前 canvas 层
(3)绘制自己
(4)绘制子 View
(5)如果需要的话,绘制边缘,类似阴影
(6)绘制装饰,比如滚动条
(7)如果需要的话,绘制默认焦点高亮效果
public void draw(Canvas canvas) {
// Step 1, draw the background, if needed
drawBackground(canvas);
// skip step 2 & 5 if possible (common case)
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
return;
}
...
}
这里我们可以重点看一下(1)(3)(4)(6)这几个步骤,其他步骤并不是必要的。
步骤 1 绘制背景
draw 的绘制背景操作是通过 drawBackground 方法进行的:
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
// 背景边界的一些绘制
setBackgroundBounds();
...
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
// 1. 偏移值不为 0 时,对 canvas 进行偏移后再绘制
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
注释 1 处可知,该方法会考虑滚动的偏移量,如果有偏移则会对 canvas 进行偏移后,再绘制,绘制完成后,复原 canvas。
步骤 3 绘制 View
protected void onDraw(Canvas canvas) {
}
该方法是一个空方法,用于自定义 View 时进行实现。
步骤 4 绘制子 View
绘制子 View 会调用 dispatchDraw 方法,该方法也是一个空方法:
protected void dispatchDraw(Canvas canvas) {
}
但是在 ViewGroup 中重写了该方法:
@Override
protected void dispatchDraw(Canvas canvas) {
...
// 遍历所有子 View
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
// 绘制子 View
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
...
}
可以看到 dispatchDraw 中简单来说就是遍历所有的子 View,然后执行 drawChild 方法绘制。
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
这里直接调用了 View 的 draw 方法,但是这个方法和之前的 draw 方法不一样,参数是不同的。
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
...
// 不使用缓存
if (!drawingWithDrawingCache) {
if (drawingWithRenderNode) {
// 硬件加速
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
((RecordingCanvas) canvas).drawRenderNode(renderNode);
} else {
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
// 1. 如果当前 View 不需要绘制,那么继续分发,绘制其子 View
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {
// 2. 绘制该 View
draw(canvas);
}
}
} else if (cache != null) {
// 使用缓存显示
...
}
...
}
该方法会区分是否有缓存来进行绘制,注释 1 处,如果当前的 View 不需要绘制,那么继续使用 dispatchDraw 分发绘制其子 View,否则直接调用 draw 进行绘制。可以看到,虽然这两个 draw 方法的参数不同,但是最终都会走到 draw(canvas) 方法。
步骤 6 绘制装饰
public void onDrawForeground(Canvas canvas) {
onDrawScrollIndicators(canvas);
onDrawScrollBars(canvas);
final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
if (foreground != null) {
...
foreground.draw(canvas);
}
}
这个方法并不是重点方法,主要是对 ScrollBar 和一些其他装饰进行绘制。