欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android视图绘制流程之measure的使用详情

程序员文章站 2022-09-18 11:22:30
Android视图绘制流程之measure的使用详情 view的measure private void performMeasure(int childWidthMeas...

Android视图绘制流程之measure的使用详情

view的measure

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
        return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}
performMeasure是入口

直接看measure的onMeasure

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

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可以不看。从MeasureSpec中解包出SpecMode、SpecSize。这里的SpecSize就是View测量后的大小。(而View最终的大小是在layout阶段确定的)从代码逻辑中可以看出来,AT_MOST和EXACTLY是一种操作,所以getDefaultSize方法返回的就是SpecSize。

UNSPECIFIED情况一般用于系统内部的测量,所以getSuggestedMinimumHeight()方法获取的是UNSPECIFIED情况下的高,宽亦然。暂时不必深究。

从getDefaultSize方法的实现来看,View的宽高由SpecSize决定,所以可以得出结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小。否则在布局中使用wrap_content就相当于使用match_parent。如果View在布局中是wrap_content,那么他的SpecMode是AT_MOST,在这种模式下,他的宽高等于SpecSize。这种情况下,SpecSize是parentSize(父容器中当前可以使用的大小),这种效果和在布局中使用match_parent一致。

如何解决这个问题?

View view = new View(this){
    private int mWidth = 20;
    private int mHeight = 20;
    @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, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, mHeight);
        }
    }
};
其实这个很容易理解。比如TextView,你如果在xml中(即lp中)设置了wrap_content,一般性都是包住文本即可,但是我们不说,谁能知道?所以这个必须得我们去设置。mWidth,mHeight就是我们设置的默认值,在TextView中要进行对字体大小的计算就可以得到大小。如果没有这个设置的话,就相当于match_parent了。

view group的measure

view group是一个抽象类,没有重写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的过程就是去除子元素的lp,再通过getChildMeasureSpec来创建子元素的MeasureSpec,之后把子元素的MeasureSpec传给child进行测量。

view group没有重写onMeasure,因为onMeasure在不同的布局中测量的细节都不一样。

LinearLayout的onMeasure方法

@Override
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);
    //...
    // Determine how big this child would like to be. If this or
    // previous children have given a weight, then we allow it to
    // use all available space (and we will shrink things later
    // if needed).
    final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
    measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
            heightMeasureSpec, usedHeight);

    final int childHeight = child.getMeasuredHeight();
    if (useExcessSpace) {
        // Restore the original height and record how much space
        // we've allocated to excess-only children so that we can
        // match the behavior of EXACTLY measurement.
        lp.height = 0;
        consumedExcessSpace += childHeight;
    }

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

    if (useLargestChild) {
        largestChildHeight = Math.max(childHeight, largestChildHeight);
    }
}
会对每个孩子都执行measureChildBeforeLayout方法。这个方法内部会调用子元素的measure方法,这样各个子元素就依次进入measure过程,并且系统会通过mTotalLength这个变量来存储LinearLayout在竖直方向的初步高度。每测量一个子元素,mTotalLength就会增加。增加的部分主要包括了子元素的高度以及子元素在竖直方向上的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会根据子元素的情况来测量自己的大小。对于竖直的LinearLayout来说,他的水平方向的测量过程遵循View的测量过程,竖直方向上的测量过程就和View的不一样了。如果它的布局中高度采用的是match_parent或者具体数值,那么它的测量过程和View一致,即高度为SpecSize。如果它的布局高度采用的是wrap_content,那么它的高度是所有子元素所占用的高度总和,但是仍然不能超过它的父容器的剩余空间。它的最终高度还需要考虑其在竖直方向的padding。这个过程源码如下
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);
}