View 工作原理(二)| 艺术探索笔记

View 工作流程

View 的工作流程包括 Measure、Layout、Draw 三个过程。其中 Measure 测量 View 的宽高,Layout 确定 View 最终宽高和四个顶点的位置,Draw 将 View 绘制到屏幕上。

Measure

Measure 过程分为两种情况:原始 View 和 ViewGroup。如果是原始 View,则在 measure 方法中完成测量过程。如果是 ViewGroup,除了完成了自己的测量过程,还会遍历并调用所有子元素的 measure 方法,然后子元素中再递归执行该流程。

View 的 Measure 过程

View 的 Measure 由 measure 方法完成,measure 方法中会调用 onMeasure 方法,来看 onMeasure 方法

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

setMeasureDimension 方法会设置 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;
}

在 AT_MOST 和 EXACTLY 两种情况中,getDefaultSize 方法返回的就是 MeasureSpec 中的 SpecSize,而 SpecSize 是 View 测量后的大小。这里说的测量后的大小,指的是 Measure 过程中确定的大小,而最终的大小在 Layout 中确定,但大部分情况中,View 的测量大小与最终大小相等。

UNSPECIFIED 的情况适用于系统内部测量过程。这种情况 getDefaultSize 返回的是传入的第一个参数,即为 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 两个方法的返回值

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

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight,
        mBackground.getMinimumHeight());
}

从 getSuggestedMinimumWidth 中可以看到,当 View 没有设置背景,那么宽度为 mMinWidth,mMinWidth 对应于 android:minWidth 这个属性值,如果这个属性不指定,则默认为 0。当 View 指定了背景,则宽度为 max(mMinWidth, mBackground.getMinimumWidth()),于是转到 Drawable 中的 getMinimumWidth 方法

public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

getMinimumWidth 返回的是 Drawable 的原始宽度(待补充),如果没有就返回 0。getSuggestedMinimumHeight 原理是一样的,这里不重复。

从以上可知,当 View 没有设置背景,那么 getSuggestedMinimumWidth 返回 android:Width 的属性值,如果值没有指定,返回 0。当 View 设置了背景,那么返回 android:minWidth 和背景最小宽度这两者中的最大值。getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 的返回值就是 UNSPECIFIED 情况下的测量宽高。

在 getDefaultSize 方法中可以看到,View 的宽高由 SpecSize 指定,所以直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小,否则使用 wrap_content 就等于 match_parent。原因是,当布局使用 wrap_content,SpecMode 为 AT_MOST 模式,根据 View 工作原理(一)中普通 View 的 MeasureSpec 创建规则可知,这种情况下 View 的 SpecSize 为 parentSize,而 parentSize 就是当前父容器的剩余大小,即 View 的宽高等于父容器的剩余大小,这就导致了使用 wrap_content 相当于 match_parent。

普通 View 的 MeasureSpec 创建规则

解决方法是,当使用 wrap_content 时设置一个默认内部宽高,非 wrap_content 则使用系统的测量宽高。

ViewGroup 的 Measure 过程

在 ViewGruop 中,除了完成自己的 Measure 过程,还会遍历调用子元素的 measure 方法,所有子元素再递归执行这个过程。由于 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);
        }
    }
}

可以看到,它会对每一个子元素进行 measure,再看 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);
}

measureChild 取出子元素的 LayoutParams,再通过 getChildMeasureSpec 来创建子元素的 MeasureSpec,最后将 MeasureSpec 传给 View 的 measure 方法测量。getChildMeasureSpec 方法在 View 工作原理(一)中有分析。

由于 ViewGroup 是抽象类,所以测量过程的 onMeasure 方法需要子类去实现,下面以 LinearLayout 的 onMeasure 方法为例,来继续分析 ViewGroup 的 Measure 过程。

先看 LinearLayout 的 onMeasure 方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

接着看 measureVertical 方法,由于太长,省略了部分代码

// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
    final View child = getVirtualChildAt(i);

    ...
    
    measureChildBeforeLayout(
        child, i, widthMeasureSpec, 0, heightMeasureSpec,
        totalWeight == 0 ? mTotalLength : 0);

    if (oldHeight != Integer.MIN_VALUE) {
        lp.height = oldHeight;
    }

    final int childHeight = child.getMeasuredHeight();
    final int totalLength = mTotalLength;
    mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
        lp.bottomMargin + getNextLocationOffset(child));
}

代码中遍历了子元素并对每个都执行了 measureChildBeforeLayout 方法,该方法内部会调用子元素的 measure 方法,并且还会通过 mTotalLength 变量来存储 LinearLayout 在竖直方向的初步高度,其中主要是子元素的高度和子元素在竖直方向上的 margin 值。当子元素测量完毕,LinearLayout 会测量自己的大小。

// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;

int heightSize = mTotalLength;

// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;

...

setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
    heightSizeAndState);

对竖直方向的 LinearLayout 来说,水平方向的测量与 View 测量过程相同,竖直方向上,当布局中使用的是 match_parent 时,它与 View 测量过程一致,即高度为 specSize。如果是 wrap_content,那么高度是不超过父容器剩余空间下,所有子元素所占用的高度加上其他竖直方向的 padding。resolveSizeAndState 方法如下

public static int resolveSizeAndState(int size, int measureSpec,
        int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

经过以上的过程,View 的 Measure 过程就完成了。这时可以通过 getMeasuredWidth 和 getMeasuredHeight 方法得到 View 的测量宽高。

你可能感兴趣的:(View 工作原理(二)| 艺术探索笔记)