注:本文使用 sdk 23 作为源码参考。

  • 前言
  • ViewRootImpl#performTraversals()
    • ViewRootImpl#performMeasure()
    • ViewRootImpl#performLayout()
    • ViewRootImpl#performDraw()
  • some tips
    • onDraw()
    • dispatchDraw()

前言

关于 View 的绘制流程,网上铺天盖地的文章已经都把这个机制说烂了,笔者撰写此文一面为了方面自己后期回顾,一面也试着使用更通俗一点的方式来阐述这个机制。

ViewRootImpl#performTraversals()

众所周知, ViewRootImpl#performTraversals() 是触发 View 绘制流程的起始点,而在 ViewRootImpl#performTraversals() 中会触发相应的 ViewRootImpl#performMeasure()ViewRootImpl#performLayout()ViewRootImpl#performDraw() 对应着测量布局绘制三个过程。(不得不说函数、变量的命名对源码的理解还是有很大的帮助的,从 ViewRootImpl#performTraversal() 这个函数名就应该能猜到这个函数是完成遍历的过程,完成什么的遍历?完成测量的遍历、布局的遍历、绘制的遍历),为了加深各位读者的理解,笔者将在源码解析的过程中,一步一步绘制流程图,比起文章尾部丢上一张完整的流程图,笔者认为这样的做法会更加友善——

ViewRootImpl#performMeasure()

首先是测量过程,删除无关代码后简化代码如下:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);}

可以看到,实际上在 ViewRootImpl#performMeasure() 中调用了 View 的 measure() 方法,打开 View#measure() 方法看一看,可以看到这个方法是 fianl 的,所以对于它的子类来说是不能够覆写该方法的,而在其内部调用了 View#onMeasure() 方法,那么接下来就该看看 onMeasure() 方法的实现了,在 View 中 onMeasure() 方法实现如下:

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

setMeasuredDimension(measuredWidth, measuredHeight) 方法用官方文档的意思就是用来保存测量后的宽高。

那么 ViewGroup 中的实现呢?实际上在源码中可以看到,ViewGroup 并没有按照常理将其设置为 abstract 类型,但是 onMeasure() 上方的注释文档提到:子类需要重写它才能够获取到精确、有效的测量值,所以对于 View 的子类来说,都应该去重写该方法,所以实际上不仅仅是针对于 ViewGroup 来说,对于 TextView、ImageView 来说也是需要重写 onMeasure() 方法的。对于 View 的 onMeasure() 方法本文就不加以扩展了,毕竟测量这种操作对于纯粹的 View 来说就是测量自己的大小,所以不妨着重看看 ViewGroup 的实现,以 LinearLayout 举例:

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    if (mOrientation == VERTICAL) {        measureVertical(widthMeasureSpec, heightMeasureSpec);    } else {        measureHorizontal(widthMeasureSpec, heightMeasureSpec);    }}

LinearLayout 根据 orientation 的设置来选择测量方式,笔者这里选择 LinearLayout#measureVertical() 来阐述,LinearLayout#measureHorizontal() 意义等同。LinearLayout#measureVertical() 源码删减后如下:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {    for (int i = 0; i < count; ++i) {            final View child = getVirtualChildAt(i);            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();            if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {                // ...            } else {                // ...                measureChildBeforeLayout(                       child, i, widthMeasureSpec, 0, heightMeasureSpec,                       totalWeight == 0 ? mTotalLength : 0);            }    }    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),            heightSizeAndState);}

for 循环是对子 View 的遍历。if 判断语句的执行条件中有一点是 lp 的高度是0,也就意味着子 View 的 layout_height 设置为了 0,这种情况就不需要对子 View 进行 measure 操作了,因为对于竖直 LinearLayout 来说,它更关注于子 View 的高度。这也就是意味着正常情况下都是会走 else 分支,也就是说正常情况下每个 view 都会被执行 LinearLayout#measureChildBeforeLayout() 方法,跟踪 LinearLayout#measureChildBeforeLayout() 方法可以发现实际上底层是调用了 View 的 measure() 方法——

protected void measureChildWithMargins(View child,        int parentWidthMeasureSpec, int widthUsed,        int parentHeightMeasureSpec, int heightUsed) {    // ...    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}

所以实际上对于 ViewGroup 来说,它的 onMeasure() 实际上就是调用各个子 View 的 measure() 方法来将它们的某些值做一些汇总然后拼凑成自己的宽高。

[外链图片转存中…(img-DDJjZO65-1590468520904)]

实际上上述源码中的 measureChildWithMargins() 只是 ViewGroup 提供的一种测量子 View 的函数,与此类似的还有 ViewGroup#measureChild()。除了测量单个子 View 的函数外,ViewGroup 还有提供 measureChildren() 这种子 View 遍历测量的函数——

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 子类实现中运用的不多,像 LinearLayout/FrameLayout/RelativeLayout 中都是自行实现子 View 遍历,底层只会调用 measureChild() 或者 measureChildWithMargins()

注:笔者见部分书籍和博客常谈到 measureChildren() 这个函数,且甚至有博客笼统地称 ViewGroup 均会调用该函数来遍历测量子 View,故特此撰写 tip。且笔者认为从使用率上来说,该函数在 ViewGroup 中运用地太少,意义并不是很大。

ViewRootImpl#performLayout()

ViewRootImpl#performLayout() 其实与 ViewRootImpl#performMeasure() 类似,底层实现通过调用 View#layout() 来实现的——

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,        int desiredWindowHeight) {    // ...    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());    // ...}

所以不妨打开 layout() 方法看一看——

public void layout(int l, int t, int r, int b) {    setFrame(l, t, r, b);    onLayout(changed, l, t, r, b);}

可以看到,实际上 View#layout() 做了两步,一步是调用 setFrame() 设置自身上下左右四个顶点的位置,这样自身的位置就已经布好了,第二步是 onLayout() ——

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

View 中 onLayout() 方法竟然是一个空实现,不仅如此,ViewGroup 作为抽象类,onLayout() 是它唯一声明的抽象方法,可见该方法对于 ViewGroup 来说有多重要了,它的官方注释说到:当 View 需要给它的孩子设置大小和位置的时候应该被调用。所以从这里可以看出,对于纯粹的 View 来说,onLayout() 的意义可能不是很大(从源码看来,View 中只有 TextView 对其稍有扩展),但是对于 ViewGroup 来说确是至关重要的一个函数。毕竟作为一个 ViewGroup 来说,多样性的体现就在于对子 View 的摆放。

同样的,拿 LinearLayout 来举例,看看 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) {    for (int i = 0; i < count; i++) {        final View child = getVirtualChildAt(i);        final int childWidth = child.getMeasuredWidth();        final int childHeight = child.getMeasuredHeight();                final LinearLayout.LayoutParams lp =                (LinearLayout.LayoutParams) child.getLayoutParams();                childTop += lp.topMargin;        setChildFrame(child, childLeft, childTop + getLocationOffset(child),                childWidth, childHeight);        childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);    }}

内部会对子 View 进行遍历,并调用 setChildFrame(),而 setChildFrame() 的底层实现也就是 View#layout()——

private void setChildFrame(View child, int left, int top, int width, int height) {            child.layout(left, top, left + width, top + height);}

所以,对于 ViewGroup 来说,它的 layout() 方法先会对自己进行定位(setFrame()),再遍历调用子 View 的 layout()(/setFrame()) 将所有的子 View 进行布局。

[外链图片转存中…(img-6VjtBkFI-1590468520906)]

ViewRootImpl#performDraw()

等同于 performMeasure()performLayout()ViewRootImpl#performDraw() 底层会调用 draw() 方法——

private void performDraw() {    // ...    draw();    // ...}

当然,这里可以注意到这个 draw() 方法是位于 ViewRootImpl 中而不是 View 中的,打开 draw() 并删除无关代码:

private void draw(boolean fullRedrawNeeded) {    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {        return;    }}

继续打开 drawSoftwar() 方法:

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,        boolean scalingRequired, Rect dirty) {    mView.draw(canvas);}

所以最终 ViewRootImpl#draw() 最终底层还是通过 View#draw() 来实现的。

来看看 View 的 draw() 方法的实现:

public void draw(Canvas canvas) {    /*    * Draw traversal performs several drawing steps which must be executed    * in the appropriate order:    *    *      1. Draw the background    *      2. If necessary, save the canvas' layers to prepare for fading    *      3. Draw view's content    *      4. Draw children    *      5. If necessary, draw the fading edges and restore layers    *      6. Draw decorations (scrollbars for instance)    */    // Step 1, draw the background, if needed    int saveCount;    if (!dirtyOpaque) {        drawBackground(canvas);    }    // skip step 2 & 5 if possible (common case)    final int viewFlags = mViewFlags;    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;    if (!verticalEdges && !horizontalEdges) {        // Step 3, draw the content        if (!dirtyOpaque) onDraw(canvas);        // Step 4, draw the children        dispatchDraw(canvas);        // Overlay is part of the content and draws beneath Foreground        if (mOverlay != null && !mOverlay.isEmpty()) {            mOverlay.getOverlayView().dispatchDraw(canvas);        }        // Step 6, draw decorations (foreground, scrollbars)        onDrawForeground(canvas);        // we're done...        return;    }}

源码注释已经说得很清楚了:

1.绘制背景
2.如果有需要的话,保存图层,为渐变做准备
3.绘制自身
4.绘制子 View
5.如果有需要的话,绘制渐变并重新存储图层
6.绘制装饰

从上方的源码也可以看出,大部分2、5点是可以跳过的,所以日常开发中需要注意到1、3、4、6的执行顺序就好了。而从这里可以看出两点,其一对于 ViewGroup 来说,要关注到第4点也就是 dispatchDraw() 的实现了,实际上对于 ViewGroup 来说,它的实现也等同于测量和布局,也就是循环遍历调用子类的 draw() 方法;其二对于 View 来说,需要关注到第3点也就是 onDraw() 的实现了,实际上在日常开发自定义 View 中 onDraw() 方法应该是最常见覆写的 API 了,所以笔者再此也不做扩展了。

[外链图片转存中…(img-GntcTw5O-1590468520907)]

some tips

onDraw()

在本文中并未涉及,笔者照搬《Android 开发艺术探索》上的内容了——如果是继承自 View,需要自行支持 wrap_content,且 padding 也需要自行处理。这里需要加粗的地方就是继承自 View,如果是 TextView 等原生控件可以不考虑处理 wrap_content 和 padding,因为原生控件已经覆写了 onDraw() 方法。

dispatchDraw()

具有一定的经验的读者知道,对于 ViewGroup 来说,大部分情况下 draw(Canvas canvas) 方法是不会被调用,但是 dispatchDraw() 方法在正常情况下都是会被调用的。缘由在 View 中的 updateDisplayListIfDirty() 中有这么一段:

// Fast path for layouts with no backgroundsif ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {  mPrivateFlags &= ~PFLAG_DIRTY_MASK;  dispatchDraw(canvas);} else {  draw(canvas);}

可以看到会对一些 flag 进行判断,如果判断结果符合则执行 dispatchDraw() 方法,否则才执行 draw() 方法。那么这个 flag 如何设置呢?通过 View#setWillNotDraw(boolean) 就可以设置了,对于 View 来说是关闭这个 flag 的,而对于 ViewGroup 来说是默认开启这个 flag 的,当开发者可以手动开启或者调用部分绘制 API(如 drawBackground())的时候才会关闭这个 flag。那么这样做的目的是什么呢——对于 ViewGroup 来说,它存在的意义主要在于测量和布局的过程,而视图『具体效果』的展示其实都在子 View 身上,ViewGroup 的重点并不在此。各位读者可以结合实际情况想想是不是这么一回事,理解了这个概念就不再会困惑于为什么 ViewGroup 的 draw()/onDraw() 方法不一定会被调用了。所以对于自定义 ViewGroup 来说,在有相应的需求下尽量去覆写 dispatchDraw() 而不是 onDraw() 方法,但是事实上针对一般的 ViewGroup 来说也不需要去覆写该方法,沿用 ViewGruop 的即可(源码中 LinearLayout、FrameLayout、RelativeLayout 等常见布局沿用 ViewGroup 的 dispatchDraw() 方法)。

更多相关文章

  1. android很的意思的事情,关于Input…
  2. Android(安卓)面试整理
  3. Ubuntu 编译Android若干错误及解决方法(转)
  4. Android(安卓)MediaPlayer基本使用方式
  5. [Android] IntentInjector
  6. Android(安卓)读取内存文件返回byte数组
  7. Android(安卓)sqlite cursor的遍历
  8. Android工程导入时常见的错误解决方法
  9. Android(安卓)-- 倒计时的实现

随机推荐

  1. 分享几点Android(安卓)开发中的小技巧吧
  2. Android(安卓)和iOS 比较之我见
  3. 自己看的,随便写,贴。关于Android里面的Sty
  4. iOS 7 需要再和 Android(安卓)比比什么?
  5. Android实现自定义广播
  6. 从零学Android(四)、适配不同的Android设备
  7. Android(安卓)Canvas类介绍和Android(安
  8. 使用jni接口完成android本地程序的运行--
  9. 客觀評 Android、iOS、WP7
  10. 框架模式 MVC 在Android中的使用