Android学习笔记之View的绘制流程(一)——measure过程

一句话总结:View的绘制流程是从ViewRoot (ViewRootImpl)的 performTraversals方法开始,经过measure,layout,draw 三个过程才能最终将一个View绘制出来。其中measure用来测量View的宽高,layout用来确定View在父容器中的放置位置,draw用来将View绘制到屏幕上。

再说细点,当界面强制重绘调用了View的requestLayout这个方法时,如下:

public void requestLayout() {
    ......
    if (mParent != null && !mParent.isLayoutRequested()) {
        mParent.requestLayout();
    }
    ......
}

可以看到它会一层一层的向上调用 parent 的 requestLayout 方法,直到最顶层的ViewRootImpl.requestLayout()。然后在该方法中post了一个Runnable,其中调用了performTraversals方法。

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

接着,performTraversals会依次调用performMeasure,performLayout 和 performDraw 三个方法,这三个方法分别完成了顶级View 的measure,layout,draw 这三大流程。以performMeasure为例,performMeasure→measure→onMeasure,在onMeasure方法中则会对所有的子元素进行measure过程,这样 measure 流程就从父容器传递到了子元素中,然后子元素会重复父容器的 measure 过程,如此反复直到完成整个View树的遍历。同理,performLayout 和 performDraw 的传递流程和 performMeasure 是类似的,只是 performDraw 的传递过程是在draw 方法中通过dispatchDraw来实现的,不过这本质上没有区别。

View的measure过程

因为ViewGroup也是View,所以View的measure过程可以细分为普通View的measure过程以及ViewGroup的measure过程。

1.普通View的measure过程

view的measure过程由measure方法来完成,而measure方法是一个final类型的方法,这意味着子类不能重写此方法。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) 

而measure方法中会调用View的onMeasure方法,该方法可以被重写

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

可以看到,它通过setMeasuredDimension方法设置了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;
}

可以看出,getDefaultSize方法的逻辑分为两种情况,如果MeasureSpec的模式SpecMode为UNSPECIFIED,那么返回的尺寸为第一个参数,即getSuggestedMinimumWidth()的返回值;如果MeasureSpec的模式SpecMode不是UNSPECIFIED,那么返回的尺寸为SpecSize,即View测量后的大小。至于MeasureSpec是怎么得来的,SpecSize又是怎么测量出来的,可以参考我的上一篇博文《MeasureSpec》

至于 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight,它们的实现原理是一样的,只是一个是宽一个是高,这里仅以宽为例,看下它的源码

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

不难看出它的逻辑是,如果View没有设置背景,那么返回mMinWidth,这个值对应于android:minWidth这个属性所指定的值,可以为0;如果View设置了背景,则返回mMinWidth和背景最小宽度中的较大值。

从getDefaultSize方法的实现来看,View的宽/高由SpecSize决定,结合上一篇文章《MeasureSpec》的SpecSize测量值的由来,因此我们需要注意的是:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的默认尺寸大小,否则在布局文件中使用wrap_content和使用match_parent效果一样,尺寸均为父容器剩余的空间大小。具体来说如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
    if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
        setMeasuredDimension(mWidth,mHeight);
    } else if (widthSpecMode == MeasureSpec.AT_MOST){
        setMeasuredDimension(mWidth,heightMeasureSpec);
    } else if (heightSpecMode == MeasureSpec.AT_MOST){
        setMeasuredDimension(widthMeasureSpec,mHeight);
    }
}

2.ViewGroup的measure过程

对于ViewGroup来说,除了完成自己的measure过程之外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,因此它没有重写View的onMeasure方法,而是提供了一个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);
        }
    }
}
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);
}

不难看出,measureChild方法的思想就是取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure方法来进行测量。至于getChildMeasureSpec方法,在上一篇博文《MeasureSpec》的结尾分析过,它是将子元素的布局参数LayoutParams根据父容器所施加的约束规则转换成对应的MeasureSpec。

讲到这,ViewGroup完成了子元素的遍历以及measure过程的传递,那么它自身的测量过程呢?之前提到,ViewGroup没有重写onMeasure方法,这是因为不同的ViewGroup子类有着不同的布局特性,导致它们的测量细节各不相同,因此需要各个子类去具体实现onMeasure方法。

最后,我们可以总结一下measure过程中方法大致调用顺序为:从ViewGroup开始,measure→onMeasure(需要具体实现,结合布局特性以及所有子元素的测试宽高,计算出自己的测试宽高)→measureChild→child.measure→子元素的onMeasure(如果仍然是ViewGroup,则重复上述步骤)

猜你喜欢

转载自blog.csdn.net/Ein3614/article/details/82945809