View & ViewGroup 绘制原理

概要

Activity 界面的根布局其实是一个名为 DecorViewFrameLayout, 在创建 DecorView 的时候,会相应的创建一个 ViewRootImpl 类来控制 UI 的绘制,其实也就是控制 DecorView。绘制的过程分为了 measure, layout, draw 三个过程。 以下分别对 ViewViewGroup 三个过程进行说明。

View 绘制

measure


public final void measure(int widthMeasureSpec, int heightMeasureSpec)

两个参数 widthMeasureSpecheightMeasureSpecparent 提供过来的,parent 用这两个参数告诉 View ,我希望你的宽高是多大。这两个 参数可以由 View 中的一个静态类 MeasureSpec 来解析,MeasureSpec 是由 sizemode 组成,可由下面方法获得。

int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

size 很好理解 ,就是 parentChild View 的大小。 而 mode 呢,有三种形式。

  1. UNSPECIFIEDparentChild View 没有任何限制,Child View 想多大就多大。
  2. EXACTLYparentchild view 说,我只希望是这个固定的大小,也就是上面代码解析出来的 specSize
  3. AT_MOSTparent 告诉 child view,你最大绘制范围就是 specSize

那么 View.onMeasure() 就是利用 parent 传过来的关于宽高限制的参数来决定自己的宽度 ,然而可以看到这个方法是 final ,不允许重写的。 当然,实际的测量是在 View.onMeasure() 方法中。

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

系统调用了 setMeasuredDimension(),这是测量之后必须要调用的方法,它用来存储的测量的宽高,也就是 getMeasuredWidth()getMeasuredHeight() 的值。

系统的实现中还用到了 getSuggestedMinimumWidth()getSuggestedMinimumHeight()

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

为何要调用这个?有时候,我可能设置了 android:backgound 或者 android:minWidth , android:minHeight,所以我们自定义的 View 不应该小于这个值吧。

当然我们还想知道,系统的默认实现中设置的测量宽高是什么呢,这个是要看 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;
}

如果解析出来的 modeUNSPECIFIED,也就说 parentChild View 没有限制,那就用 getSuggestedMinimumXXX() 的值。

如果解析出来的 modeAT_MOSTEXACTLY, 就用 parent 提供过来的大小。 那也就是说,如果调用系统的方法super.onMeasure()进行测试,默认就是填充整个父布局。 我们自定义 ViewViewGroup 的时候,有时候并不想这样,因此需要自己测量,最后调用 setMeasuredDimension() 方法。

draw


    @CallSuper
    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);
        }

        // Step 2, save the canvas' layers
        // ...

        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        // Step 5, draw the fade effect and restore layers
        // ...

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
    }

View.draw() 方法并不是 final 的,因此我们可以重写它,不过一定记得调用 super.draw(canvas), 因为系统帮我们绘制了 backgroundscrollBar,而且最重要的是,如果是 ViewGroup 的话,会调用 dispatchDraw() 来让 Child View 进行绘制。这也正是 @CallSuper 注解的意思。

从代码中的 step 3 可以看出,onDraw() 才是绘制自己的内容区域。而 ViewonDraw() 方法是一个空方法

    /**
     * Implement this to do your drawing.
     *
     * @param canvas the canvas on which the background will be drawn
     */
    protected void onDraw(Canvas canvas) {
    }

layout


public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
    }
    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);
    }
}

Viewlayout()parent(ViewGroup) 调用,用来确定 Child View 的位置,顺便也确定了实际大小。如果是自定义 View 的话,完全没必要在意这个方法,当然也不用在意在 View.layout() 调用的 View.onLayout() 方法。而在实际中,只需要注意 onSizeChange() 方法,这个方法告诉你,你的 View 的大小发生了改变。而 onSizeChange() 就是在 setOpticalFrame() 或者 setFrame() 中调用的,而这个方法是 layout 过程的关键所在 ,它们决定了 View 的实际宽高,我们取其中一个方法看看样子

protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;
    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;
        if (sizeChanged) {
            // 调用了 onSizeChanged()
            sizeChange(newWidth, newHeight, oldWidth, oldHeight);
        }
    }
    return changed;
}

setFrame() 检测了 View 的大小是否改变了,然后更新了坐标值。 坐标值的改变也意味着 getWidth()getHeight() 的值可以确定了。

    public final int getWidth() {
        return mRight - mLeft;
    }

    public final int getHeight() {
        return mBottom - mTop;
    }

如果大小改变了,就调用 sizeChange() 方法

    private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
        onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
        // ...
    }

sizeChange() 方法中首先就调用了 onSizeChanged() 方法。

那么我们梳理下 layout 过程,layout() -> onSizeChanged() -> onLayout()

ViewGroup 绘制

measure


Viewmeasure() 方法为 final 类型,因此子类不能复写这个方法,只能复写 onMeasure() 方法,那么 ViewGroup 作为 View 的子类,也不例外。

我们还可以知道,onMeasure() 是要完成自身的测量,并通过调用 setMeasuredDimension() 方法来存储测量的结果。 然而对于 ViewGroup 来说,它需要提供一个 MeasureSpec 给它的每个 Child View 用来完成测量,然后根据 Child View 测量结果来决定自身的测量值。

ViewGroup 用来测量 Child View 的方法有三个,分别是 measureChild(), measureChildren(), measureChildWithMargins()

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);
    }


    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);
    }

首先整体观看 measureChild() 方法,它只考虑了 padding,而并没有考虑经常会出现的 margin,因此,如果自定义 View 的时候没有 margin 可以考虑用这个方法完成 Child View 的测量。

measureChild() 是调用 getChildMeasureSpec() 来为 Child View 生成相应的 MeasureSpec,它的第二个参数虽然名为 padding,但是在实际使用的时候,它其实在更严格的意义上讲,是指已经使用的长度或宽度。

mesureChildren()

    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);
            }
        }
    }

measureChildren() 在原理还是调用了 measureChild() 方法,只是有一点需要注意,如果 Child View 的可见性为 GONE,是不会测量的。

measureChildWithMargins()

    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);
    }

measureChildWithMargins() 这个方法就比较全面,首先考虑了 paddingmargin,其次它需要传入两个参数,分为代表已经使用过的宽度和高度,这样我们就很方便计算并排的 Child View 的宽高。

FrameLayout.onMeasure()

说了这么多很空泛,举个例子。 我们知道,根布局其实就是一个名为 DecorViewFrameLayout,它的 onMeasure() 方法的部分源码如下

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();

        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                //...
            }
        }

        // ...

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

        // ...
    }

在第 17 行,它使用的是 measureChildWithMargins() 方法来完成 Child View 的测量,由于 FrameLayout 设计为层层覆盖的特性,因此 measureChildWithMargins() 的第三个参数和第五个参数都为0,代表永远没有使用过的宽度来高度,只考虑 paddingmargin 即可。

layout


    @Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            super.layout(l, t, r, b);
        } else {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
    }

    @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

ViewGrouplayout() 方法其实也是调用 Viewlayout() 完成布局的。 然而我们注意到 ViewGrouponLayout 是一个抽象方法,子类必须实现,这也是提示我们,自定义 ViewGroup 就有责任为 Child View 布局。

draw


ViewGroup 如果自身没有特别的绘制需要,就不需要复写 draw() 或者 onDraw() 方法,它的重点在布局。draw() 方法会绘制一些基本的东西,例如背景,滚动条等等,因此如果想复写 draw() 方法记得调用 super.draw(),而如果复写 onDraw() 只需要完成自己的绘制即可 。

参考文章

https://developer.android.google.cn/training/custom-views/create-view.html
https://realm.io/news/360andev-huyen-tue-dao-measure-layout-draw-repeat-custom-views-and-viewgroups-android/
http://blog.csdn.net/yanbober/article/details/46128379

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值