View系列 (三) — Measure 流程详解

一、概述

测量过程分为 View的measure过程 和 ViewGroup的measure过程。

View的类型measure 过程
单一的View (如 ImageView)只测量自身
ViewGroup遍历测量该 ViewGroup 下的所有子 View

版本: Android SDK 29
关联文章:

  1. 《View系列 (一) — Android 坐标系》
  2. 《View系列 (二) — MeasureSpec 详解》
  3. 《View系列 (三) — Measure 流程详解》
  4. 《View系列 (四) — Layout 流程详解》
  5. 《View系列 (五) — Draw 流程详解》

二、单一 View 的测量流程

1. 流程图

在这里插入图片描述

2. 源码分析

ViewGroup.measureChildWithMargins()

// ViewGroup.class
protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    // 1.子View的LayoutParams参数
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
	// 2.根据父容器的MeasureSpec和子View的LayoutParams来确认子View的MeasureSpec。
    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);
	// 3.将子View的MeasureSpec传递给子View,如果子View的单一View,方便其测量自身。
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

View.measure()

// View.class
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
	// ...省略代码...
	int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
	if (cacheIndex < 0 || sIgnoreMeasureCache) {
	    // measure ourselves, this should set the measured dimension flag back
	    // 这里执行单一View的onMeasure()方法
	    onMeasure(widthMeasureSpec, heightMeasureSpec);
	    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
	}
	// ...省略代码...
}

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

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    // ...省略代码...
    // 赋值
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
	// 将子View的大小赋值给下面两个字段
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

View.getSuggestedMinimumWidth()

// View.class
// 1.无背景,就返回mMinWidth,这个参数其实就是布局文件里的 android:minWidth 属性。
// 2.有背景,返回mMinWidth与背景宽度中的较大值。
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

// Drawable.class
public int getMinimumWidth() {
	// 如果背景有宽度,就返回宽度。
    final int intrinsicWidth = getIntrinsicWidth(); // getIntrinsicWidth()默认返回-1
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

View.getDefaultSize()

// View.class
public static int getDefaultSize(int size, int measureSpec) {
	//参数 measureSpec 是子View的MeasureSpec
    int result = size;
    // 获取子View的LauoutParams参数和父容器协调后的尺寸。
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED: //这里其实就是特殊处理一下UNSPECIFIED时,子View的大小。
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

三、ViewGroup 的测量流程

原理

  1. 遍历测量所有子View的尺寸大小。
  2. 合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值。

1. 流程图

在这里插入图片描述

2. 源码分析

// View.class
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
	// ...省略代码...
	int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
	if (cacheIndex < 0 || sIgnoreMeasureCache) {
	    // measure ourselves, this should set the measured dimension flag back
	    // 这里执行单一View的onMeasure()方法
	    onMeasure(widthMeasureSpec, heightMeasureSpec);
	    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
	}
	// ...省略代码...
}

为什么ViewGroup 中没有重写 onMeasure() 方法,而是在各个继承自 ViewGroup 的容器控件中实现?

每个容器型控件的布局特性都不一样,所以无法对 onMeasure 进行统一处理。

在 ViewGroup 中,measureChildren() 方法 可以用来遍历测量子View。

// ViewGroup.class
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    // 遍历当前ViewGroup内部的所有子View
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        // 子View显示模式不为GONE
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
        	// 测量子View
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

// 该方法与上面的 measureChildWithMargins()方法类似
protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    // 1.子View的LayoutParams参数
    final LayoutParams lp = child.getLayoutParams();
	// 2.根据父容器的MeasureSpec和子View的LayoutParams来确认子View的MeasureSpec。
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);
	// 3.将子View的MeasureSpec传递下去。
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

关于 onMeasure() 方法,我们以 LinearLayout.onMeasure() 为例进行分析。

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

// 竖直方向排列时,子View的高度相加,宽度取最宽的值。
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
	mTotalLength = 0; // 该变量是保存该ViewGroup在竖直方向的总高度
    int maxWidth = 0; //记录最大宽度
    int childState = 0;
    float totalWeight = 0;
	// 获取垂直方向上的子View个数
    final int count = getVirtualChildCount();
    // ...省略代码...

    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    boolean skippedMeasure = false;

    // See how tall everyone is. Also remember max width.
    // 遍历子View并获取其宽/高,并记录下子View中最大的宽度值
    for (int i = 0; i < count; ++i) {
    	// ...省略代码... View为null和可见性为GONE的View
        // ...省略添加分割线高度的逻辑

        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
		// 记录子View是否有weight属性设置,用于后面判断是否需要二次measure
        totalWeight += lp.weight;
		// 是否开启权重
        final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
        if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
            /* 
             * 如果LinearLayout的specMode为EXACTLY且子View设置了weight属性,在这里会跳过子View的measure过程,
             * 同时标记skippedMeasure属性为true,后面会根据该属性决定是否进行第二次measure。
             * 若LinearLayout的子View设置了weight,会进行两次measure计算,比较耗时。
             * 这就是为什么LinearLayout的子View需要使用weight属性时候,最好替换成RelativeLayout布局。
             */
            final int totalLength = mTotalLength;
            mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            skippedMeasure = true;
        } else {
            // ...省略代码...
            
            // 已用的高度值
            final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
            // 遍历当前child内的子View,并进行测量
            measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, usedHeight);
			// 获取当前子View的高度
            final int childHeight = child.getMeasuredHeight();
            // ...省略代码...
            
			// mTotalLength用于存储LinearLayout在竖直方向的高度
            final int totalLength = mTotalLength;
            // 将子View测量的高度添加到mTotalLength
            mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                   lp.bottomMargin + getNextLocationOffset(child));
            
            // ...省略代码...
        }

		// ...省略代码...

		// 获取当前ViewGroup控件中,宽度最大值(子控件左右外边距 + 子控件宽度)
        final int margin = lp.leftMargin + lp.rightMargin;
        final int measuredWidth = child.getMeasuredWidth() + margin;
        maxWidth = Math.max(maxWidth, measuredWidth);
        childState = combineMeasuredStates(childState, child.getMeasuredState());

        // ...省略代码...
    }
	// ...省略代码...

    // Add in our padding
    // 高度值还包括LinearLayout自身的上下内边距
    mTotalLength += mPaddingTop + mPaddingBottom;

    int heightSize = mTotalLength;

    // Check against our minimum height
    // 从最小高度值和当前LinearLayout高度值中取较大值。
    heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
	// Reconcile our calculated size with the heightMeasureSpec
    int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
    
    // ...省略代码-权重导致的重新测量...
    
    if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
        maxWidth = alternativeMaxWidth;
    }
	// 宽度值还包括LinearLayout自身的左右内边距
    maxWidth += mPaddingLeft + mPaddingRight;

    // Check against our minimum width
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

	// 将计算子View后得到的宽高值赋值给当前的LinearLayout
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState);

	// ...省略代码...
}

到此,View的绘制流程就分析完了。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值