天天看点

View & ViewGroup 绘制原理概要View 绘制ViewGroup 绘制参考文章

概要

Activity

界面的根布局其实是一个名为

DecorView

FrameLayout

, 在创建

DecorView

的时候,会相应的创建一个

ViewRootImpl

类来控制 UI 的绘制,其实也就是控制

DecorView

。绘制的过程分为了

measure

,

layout

,

draw

三个过程。 以下分别对

View

ViewGroup

三个过程进行说明。

View 绘制

measure

public final void measure(int widthMeasureSpec, int heightMeasureSpec)
           

两个参数

widthMeasureSpec

heightMeasureSpec

parent

提供过来的,

parent

用这两个参数告诉

View

,我希望你的宽高是多大。这两个 参数可以由

View

中的一个静态类

MeasureSpec

来解析,

MeasureSpec

是由

size

mode

组成,可由下面方法获得。

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

size

很好理解 ,就是

parent

Child View

的大小。 而

mode

呢,有三种形式。

  1. UNSPECIFIED

    parent

    Child View

    没有任何限制,

    Child View

    想多大就多大。
  2. EXACTLY

    parent

    child view

    说,我只希望是这个固定的大小,也就是上面代码解析出来的

    specSize

  3. AT_MOST

    parent

    告诉

    child view

    ,你最大绘制范围就是

    specSize

那么

View.onMeasure()

就是利用

paren

t 传过来的关于宽高限制的参数来决定自己的宽度 ,然而可以看到这个方法是

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

如果解析出来的

mode

UNSPECIFIED

,也就说

parent

Child View

没有限制,那就用

getSuggestedMinimumXXX()

的值。

如果解析出来的

mode

AT_MOST

EXACTLY

, 就用

parent

提供过来的大小。 那也就是说,如果调用系统的方法

super.onMeasure()

进行测试,默认就是填充整个父布局。 我们自定义

View

ViewGroup

的时候,有时候并不想这样,因此需要自己测量,最后调用

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)

, 因为系统帮我们绘制了

background

scrollBar

,而且最重要的是,如果是

ViewGroup

的话,会调用

dispatchDraw()

来让

Child View

进行绘制。这也正是

@CallSuper

注解的意思。

从代码中的

step 3

可以看出,

onDraw()

才是绘制自己的内容区域。而

View

onDraw()

方法是一个空方法

/**
     * 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) != ) {
        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);
    }
}
           

View

layout()

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

View

measure()

方法为

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(, specSize - padding);

        int resultSize = ;
        int resultMode = ;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= ) {
                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 >= ) {
                // 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 >= ) {
                // 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 ?  : 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 ?  : 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 = ; 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()

这个方法就比较全面,首先考虑了

padding

margin

,其次它需要传入两个参数,分为代表已经使用过的宽度和高度,这样我们就很方便计算并排的

Child View

的宽高。

FrameLayout.onMeasure()

说了这么多很空泛,举个例子。 我们知道,根布局其实就是一个名为

DecorView

FrameLayout

,它的

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 = ;
        int maxWidth = ;
        int childState = ;

        for (int i = ; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, , heightMeasureSpec, );
                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,代表永远没有使用过的宽度来高度,只考虑

padding

margin

即可。

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

ViewGroup

layout()

方法其实也是调用

View

layout()

完成布局的。 然而我们注意到

ViewGroup

onLayout

是一个抽象方法,子类必须实现,这也是提示我们,自定义

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

继续阅读