谈谈 View 绘制流程

本文详细探讨了Android中View的绘制流程,从ViewRootImpl的performTraversals()开始,涵盖performMeasure()、performLayout()和performDraw()等关键步骤。在performMeasure()阶段,重点介绍了View和ViewGroup如何测量尺寸,对于ViewGroup如LinearLayout,解释了遍历子View进行测量的过程。在performLayout()中,讲解了View和ViewGroup如何确定位置,特别是ViewGroup对子View的布局。performDraw()阶段解析了绘制背景、自身、子View及装饰的顺序。最后,提到了onDraw()和dispatchDraw()在绘制过程中的作用和区别。
摘要由CSDN通过智能技术生成

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

前言

关于 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 举例:

@Override
protected 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() 方法来将它们的某些值做一些汇总然后拼凑成自己的宽高。

在这里插入图片描述

实际上上述源码中的 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 进行布局。

在这里插入图片描述

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 了,所以笔者再此也不做扩展了。

在这里插入图片描述

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 backgrounds
if ((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() 方法)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值