【Android】View的绘制原理

一、View绘制总入口

ActivityThread中,首先创建Activity,然后通过attach方法初始化对应的mWindow,然后将顶级视图DecorView添加到Windows中,并创建ViewRootImpl对象,这个对象就是沟通WindowManager和DecorView之间的桥梁,也是View绘制的开始。

View的绘制流程首先开始于ViewRootImpl的performTraversals()方法。依次经过三大过程,measure、layout、draw,performTraversals会依次调用performMeasure、performLayout、performDraw方法。其中measure用来对View进行测量,layout来确定子元素在父元素中的位置即真实宽高以及四个顶点位置,draw负责将View绘制出来。

说的接地气一点,measure就是给个建议值,View多大合适;layout就是去放View的外框,放在哪里,具体多大;draw就是去画View里面的内容。

measure过程得到的测量宽高可以通过getMeasureWidth和getMeasureHeight得到,其值不一定就是实际宽高,实际宽高是layout之后,可以通过getWidth和getHeight获得。

二、measure

1、MeasureSpec

测量过程需要提到一个类,叫MeasureSpec,“测量规格”。MeasureSpec是View定义的一个内部类。MeasureSpec代表一个32位的int,高两位代表SpecMode,测量模式,第30位代表SpecSize,在某种测量模式下的规格大小。

MeasureSpec提供打包和解包的方法,可以将一组SpecMode和SpecSize通过makeMeasureSpec方法打包成MeasureSpec,也可以将一个MeasureSpec通过getMode和getSize进行解包获得对应的值。

SpecMode测量模式包含三种,含义如下:

SpecMode值含义
UNSPECIFIED父容器没有对View有任何限制,要多大给多大
EXACTLY父容器能得到View的精确的值,这时候View的测量大小就是SpecSize的值,对应于View的LayoutParams中为match_parent或具体的值的情况。
AT_MOST父容器指定了一个可用大小SpecSize,View的大小不定,但是不能大于这个值,这个对应于View的LayoutParams的wrap_content。

系统需要通过MeasureSpec对View进行测量。View的MeasureSpec需要由父容器的MeasureSpec和View的View的layoutParams一起决定,然后根据View的MeasureSpec确定View的宽和高。

由于DecorView是顶级视图,所以它的测量方法比较特殊,具体下面一一看下DecorView和普通View的MeasureSpec的计算。

(1)DecorView的MeasureSpec值计算,DecorView是最先被测量的,可以从ViewRootImpl的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);
    }
}

这里的mView就是DecorView。往前搜传入的两个MeasureSpec的值。

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

可以看见调用了getRootMeasureSpec方法,传入了两个值,第一个值是屏幕尺寸,第二个值lp是LayoutParams 长宽的参数。所以,DecorView的MeasureSpec的值是由屏幕尺寸和它的LayoutParams 决定的。接下来看具体的关系getRootMeasureSpec。

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

可以看见如果是MATCH_PARENT的话,就是精确的模式,大小就是窗口的大小,如果是WRAP_CONTENT,就是AT_MOST模式,即大小不定,但是最大为窗口大小。
(2)普通View的MeasureSpec值计算。
前面说到DecorView首先被测量,调用了mView.measure(childWidthMeasureSpec, childHeightMeasureSpec),由于DecorView继承自FrameLayout继承自ViewGroup继承自View,可以看见View里面的measure方法是一个final方法,即不可被重写。

public final void measure(int widthMeasureSpec, int heightMeasureSpec)

这个方法其实是去调用了onMeasure方法,这个方法是各个子类可以重写的。

跟踪这个方法,可以看见在View的measure是由ViewGroup传递过来的,具体是在ViewGroup里面的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);
}

可以看见,首先获取View的LayoutParams,然后调用getChildMeasureSpec方法获取View的MeasureSpec,再调用View的measure进行测量,测量后面说,先说View的MeasureSpec的计算。看getChildMeasureSpec方法。

先看传入的三个参数:

  • 第一个参数parentWidthMeasureSpec即父容器的MeasureSpec
  • 第二个参数mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed,这些都是啥呢?这些分别是父容器设置的padding和View设置的margin和父容器已经被占用的大小。所以这些值加起来就是父容器中不能被View使用的长度。可以看见源码中函数参数名字就是padding,虽然不很确切,但是也可以说明就是不能被View使用的部分。
    第三个参数是View的LayoutParams。确切地说就是XML中layout_width和layout_height。

看下具体的getChildMeasureSpec方法。

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

一句句看下来,首先,获取父容器的specMode和specSize,然后计算size = Math.max(0, specSize – padding),这里size就是说是父容器留给View的最大长度,如果specSize < padding,说明没有空间给View了,所以是0,否则能给View的剩余空间就是specSize – padding,这个比较好理解。

然后判断父容器specMode。根据父容器的specMode和View的LayoutParams值(match_parent、具体值、wrap_content)来决定。具体的逻辑非常简单,代码容易看懂。

总结成如下表:

View的LayoutParams\父容器的测量模式specModeEXACTLYAT_MOSTUNSPECIFIED
具体的值EXACTLY childsizeEXACTLY childsizeEXACTLY childsize
match_parentEXACTLY parentsizeAT_MOST parentsizeUNSPECIFIED 0
wrap_contentAT_MOST parentsizeAT_MOST parentsizeUNSPECIFIED 0

这里的parentsize就是父容器可用剩余空间。

2、View的measure过程

首先,View的measure过程是由measure方法完成,看下View的measure方法。可以看见是个final方法,即不可被重写。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) 

measure方法会调用onMeasure方法,看onMeasure方法的实现。

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

setMeasuredDimension即设置View的测量宽高的值。计算方法是调用getDefaultSize,传入两个参数,先看这两个参数。
第二个参数是MeasureSpec,即前面说的,通过父MeasureSpec和View的LayoutParams以及父容器已经占用的空间进行计算得到。
看下第一个参数getSuggestedMinimumWidth()。

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

思路比较简单,如果没有背景,则直接返回mMinWidth,mMinWidth即View的android:minWidth的设置值,默认为0;如果有背景,则返回max(mMinWidth, mBackground.getMinimumWidth(),mBackground.getMinimumWidth()为背景的尺寸,默认也是0。
现在看下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;
}

思路也是比较简单的,如果测量模式是UNSPECIFIED,返回第一个参数,如果是AT_MOST和EXACTLY,直接返回View的MeasureSpec的规格大小。

从上面的分析可以发现,直接继承View的时候,需要重写onMeasure方法,并设置wrap_content时候的大小,否则会和match_parent的时候效果一直。比如当View设置为wrap_content的时候,此时View的MeasureSpec为AT_MOST,parentsize。当View设置为match_parent的时候,其测量值最后的结果也为parentsize。

3、ViewGroup的measure过程

ViewGroup除了完成自己的measure过程外,还会去遍历所有的子元素的measure方法,各个子元素再去递归这个过程。ViewGroup是个抽象类,其onMeasure方法是个抽象方法,需要子类去实现它,因为不同的ViewGroup有不一样的布局特性,所以导致他们的测量细节不一样。

ViewGroup提供一个遍历的方法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) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

遍历所有的View,如果可见的话调用measureChild进行测量。getChildMeasureSpec的实现前面已经讲过了。得到之后调用子元素的measure方法。

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

4、measure结果的获取

有的业务需要,需要获取测量结果,但是有个问题,measure的测量和Activity的各个生命周期没有关系,所以在什么生命周期里面都有可能得到的measure测量值为0,即还没测量结束。从源码可以看见performTraversals是另开一个线程执行的。

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

如果保证能获取到呢?有几个方法:
(1)为View添加onWindowFocusChanged监听,这个监听会在每次Activity的窗口获得和失去焦点的时候被调用,而View绘制成功的时候也是一次获得焦点的过程,所以也会调用。

public void onWindowFocusChanged(boolean hasWindowFocus) {
    super.onWindowFocusChanged(hasWindowFocus);
    if (hasWindowFocus) {
        int height = view.getMeasureHeight();
        int width = view.getMeasureWidth();
    }
}

(2)view.post
通过post将一个runnable投递到消息队列尾部,然后调用的时候,说明View已经初始化好了。

view.post(new Runnable() {
    @Override
    public void run() {
        int height = view.getMeasureHeight();
        int width = view.getMeasureWidth();
    }
});

(3)view.measure(int widthMeasureSpec, int heightMeasureSpec)
这个是最直接的方法,直接触发计算。这个一般是用在具体的值上,因为parentsize不知道。

比如宽高都是100px的。

int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);

二、layout

layout的作用是ViewGroup用来确定子元素的位置。首先调用setFrame初始化View的四个顶点,接着调用onLayout方法。

public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }

    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    ...
}

onLayout由于不同的子类实现细节都不一样,所以onLayout在View和ViewGroup中都没有具体实现,都在子类中实现。以LinearLayout为例。

@Override
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关键代码如下。

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();
        
        int gravity = lp.gravity;
        if (gravity < 0) {
            gravity = minorGravity;
        }
        final int layoutDirection = getLayoutDirection();
        final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
        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;
        }

        childTop += lp.topMargin;
        setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                childWidth, childHeight);
        childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

        i += getChildrenSkipCount(child, i);
    }
}

可以看见是一个逐渐往下的过程。在父容器完成定位后,调用setChildFrame调用子元素的layout。

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

这里的width和height就是测量宽高。

final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();

看下最后真正的宽高的计算方法如下:

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

显然这个地方得到的值就是width和height。所以说在View的默认实现中,View的measure的结果测量宽高和layout的结果最终宽高是相等的。除非重新View,使得两者不一致。比如下面这样,但是这个没有什么意义。

public void layout(int l, int t, int r, int b) {
    super.layout(l, t, r + 100, b + 100);
}

三、draw

draw的过程就是将View绘制在屏幕上面。从Android源码的注释也可以看见绘制分为四个步骤,在View的draw方法中。
(1)绘制背景

drawBackground(canvas)

(2)绘制自己的内容

onDraw(canvas)

(3)绘制children

dispatchDraw(canvas);

(4)绘制装饰

onDrawForeground(canvas);

其中View的绘制通过dispatchDraw来遍历子元素并调用其draw方法,将draw事件一层层传递下去。

    原文作者:黑暗终将过去
    原文地址: https://www.jianshu.com/p/3654ab59b908
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞