Android View测量过程

Measure


目录

简介

onMeasure方法

  1. onMeasure测量视图和内容,以确定自己的宽高。该方法由measure(public final修饰)方法调用,子类需要重写来提供准确、高效的内容测量
  2. 重写此方法时,必须调用setMeasureDimension方法来存储该view的测量宽高。
  3. 默认的实现是背景的大小,除非指定了更大的MeasureSpec
  4. 重写时需要提供更好的内容测量方式
  5. 如果重写了该方法,应确保测量的宽高满足View的最小宽高getSuggestedMinimumHeightgetSuggestedMinimumWidth
MeasureSpec
  1. MeasureSpec封装了从父视图到子视图传递的布局需求。
  2. 每个MeasureSpec表示对宽度或高度的要求。
  3. MeasureSpec包含尺寸(size)和模式(mode)。

    模式说明
    UNSPECIFIED父视图不会对子view加任何约束,想要多大就多大
    EXACTLY父视图决定子视图的具体尺寸。子view想要的大小必须在这个范围内
    AT_MOST子视图想要多大就是多大
  4. MeasureSpec使用int实现以减少对象使用。并提供将sizemode包装和拆装的方法

内部int结构与意义

首先介绍下内部定义的一些常量值,以十六进制进行说明

名称说明
MODE_SHIFT30
MODE_MASK0x3 << MODE_SHIFT,即为二进制 11000000000000000000000000000000(32位)
UNSPECIFIED0 << MODE_SHIFT,仍未0,二进制 00000000000000000000000000000000(32位)对应wrap_content
EXACTLY1 << MODE_SHIFT,即为 01000000000000000000000000000000(32位)对应match_parent和xxdp
AT_MOST2 << MODE_SHIFT,即为 10000000000000000000000000000000(32位)

我们看其内部方法makeMeasureSpec(size,mode)的实现

return (size & ~MODE_MASK) | (mode & MODE_MASK)

由常量值和方法实现,可以得出:

共由32位组成,高2位表示尺寸模式,后面表示具体尺寸

常用静态方法

方法名参数参数说明说明
makeMeasureSpec(int size,int mode)size:尺寸,0 - 0x40000000之间
mode: UNSPECIFIED,EXACTLY,AT_MOST其中之一
组装尺寸和模式
makeSafeMeasureSpec(int size,int mode)同makeMeasureSpec内部调用makeMeasureSpec,对UNSPECIFIED直接返回0
getMode(int measureSpec)拆装出模式measureSpec & MODE_MASK
getSize(int measureSpec)拆装出尺寸measureSpec & ~MODE_MASK
相关方法
  1. measureChildWithMargins

    该方法为ViewGroup的protected方法。使用自己的尺寸和已使用尺寸,对子view进行测量

    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的padding和子view的margin,然后进入ViewGroup的静态方法getChildMeasureSpec()中,这里就不展示源码了,通过表格的方式讨论下不同情况时如何确定大小的,其中:

    1. childDimension为子view中LayoutParams对应的尺寸
    2. specMode为父View的尺寸模式
    3. specSize为父View的尺寸大小
    4. size为0和(specSize - padding)中的大值
    父容器测量模式子view期望尺寸 结果
    EXACTLY childDimension >= 0EXACTLY + childDimension
    childDimension == LayoutParams.MATCH_PARENTEXACTLY + size
    childDimension == LayoutParams.WRAP_CONTENTAT_MOST + size
    AT_MOST childDimension >= 0EXACTLY + childDimension
    childDimension == LayoutParams.MATCH_PARENTAT_MOST + size
    childDimension == LayoutParams.WRAP_CONTENTAT_MOST + size
    UNSPECIFIED childDimension >= 0EXACTLY + childDimension
    childDimension == LayoutParams.MATCH_PARENTUNSPECIFIED + (View.sUseZeroUnspecifiedMeasureSpec ? 0 : size)
    childDimension == LayoutParams.WRAP_CONTENTUNSPECIFIED + (View.sUseZeroUnspecifiedMeasureSpec ? 0 : size)

    由上表可得出以下结论(未接触过UNSPECIFIED,暂时不提)

    1. 子view的onMeasure方法的参数由自己的模式+父类的模式共同决定
    2. 当子view为具体尺寸时,一定会拿到EXACTLY模式的尺寸参数
    3. 当子view和父View都是具体尺寸或者MATCH_PARENT时,子view会拿到EXACTLY模式的尺寸参数
    4. 如果父View是AT_MOST,除去2中的情况,子view也同样会拿到AT_MOST
  2. setMeasuredDimension()方法

    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;
    
            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

    会在该方法内设置mMeasuredWidth,mMeasuredHeight两个属性的值,并将标记为设置为已设置测量尺寸mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET,这是个必须调用的方法,如未调用,会通过mPrivateFlags抛出异常

    此方法过后,即可通过getMeasureXxx()或者测量宽高

  3. resolveSizeAndState()方法

    TODO

    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);
    }
  4. setFrame()方法

    为当前view的大小和位置赋值,这个方法在layout时调用;这个方法的参数的值都是相对父View的

    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;
    
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;
    
            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;
    
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
    
            // Invalidate our old position
            invalidate(sizeChanged);
    
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
    
            mPrivateFlags |= PFLAG_HAS_BOUNDS;
    
    
            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }
    
            if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
                mPrivateFlags |= PFLAG_DRAWN;
                invalidate(sizeChanged);
                invalidateParentCaches();
            }
    
            // Reset drawn bit to original value (invalidate turns it off)
            mPrivateFlags |= drawn;
    
            mBackgroundSizeChanged = true;
            mDefaultFocusHighlightSizeChanged = true;
            if (mForegroundInfo != null) {
                mForegroundInfo.mBoundsChanged = true;
            }
    
            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
        return changed;
    }

    TODO

View

View的默认实现很简单,根据LayoutParamas中的尺寸,背景尺寸,视图最小尺寸进行取值

不过在分析该方法前,先分析一下该方法的参数(int widthMeasureSpec, int heightMeasureSpec)是怎么来的,需要提前说下,这两个参数模式由该view和父视图的的测量模式和父视图能提供的大小决定,如父视图wrap_content,子视图match_parent就可能需要测量两次

尺寸封装

这里View的LayoutParams的生成有两种方式:xml设置和代码构造。但是整个过程实质上都会到了LayoutParams中的public LayoutParams(Context c,AttributeSet attrs)构造器中,同自定义属性一样,使用TypedArray的方法获取值,在该类中可以看到两种模式的取值

名称含义
MATCH_PARENT-1同父视图能提供的大小那样大,对应MeasureSpec.EXACTLY
WRAP_CONTENT-2包裹自己大小,对应MeasureSpec.AT_MOST

onMeasure源码

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

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

    其中:

    1. mMinWidth的值取决于R.styleable.View_minWidth的设置,也可以使用setMinimumWidth()方法设置
    2. mDrawable.getMinimumWidth()方法的值由Drawable的getIntrinsicWidth()方法决定
  2. 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;
    }
  3. setMeasuredDimension()方法 – 参考常用方法

ViewGroup

onMeasure

由于ViewGroup未重写此方法,此处以FrameLayout为例,会通过源码+日志的方式进行解析

  1. 遍历测量子view,获取最大宽高

    根据子view,初步获取最大宽高,由于可能具有match_parent的子view,如果此时FrameLayout是wrap_content,那么无法真正测出这些View的大小,所以先收集起来,后续继续测量

    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            maxWidth = Math.max(maxWidth,
                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight,
                    child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            childState = combineMeasuredStates(childState, child.getMeasuredState());
            if (measureMatchParentChildren) {
                if (lp.width == LayoutParams.MATCH_PARENT ||
                        lp.height == LayoutParams.MATCH_PARENT) {
                    mMatchParentChildren.add(child);
                }
            }
        }
    }

    根据前景,背景最次修正最大宽高

    // Account for padding too
    maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
    maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
    
    // Check against our minimum height and width
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
    
    // Check against our foreground's minimum height and width
    final Drawable drawable = getForeground();
    if (drawable != null) {
        maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
        maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
    }
  2. 使用setMeasuredDimension方法设置自身大小

    这里会涉及resolveSizeAndState()方法,参考常用方法

  3. 根据FrameLayout的实际大小和子view的模式,遍历之前存储的具有match_parent尺寸的view,重新调用child.measure()方法

    节选循环内部代码:

    final View child = mMatchParentChildren.get(i);
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    
    final int childWidthMeasureSpec;
    if (lp.width == LayoutParams.MATCH_PARENT) {
        final int width = Math.max(0, getMeasuredWidth()
                - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                - lp.leftMargin - lp.rightMargin);
        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                width, MeasureSpec.EXACTLY);
    } else {
        childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                lp.leftMargin + lp.rightMargin,
                lp.width);
    }
    
    final int childHeightMeasureSpec;
    if (lp.height == LayoutParams.MATCH_PARENT) {
        final int height = Math.max(0, getMeasuredHeight()
                - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                - lp.topMargin - lp.bottomMargin);
        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                height, MeasureSpec.EXACTLY);
    } else {
        childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                lp.topMargin + lp.bottomMargin,
                lp.height);
    }
    
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

    因为已知当前FrameLayout的尺寸,根据子view的模式进行测量即可

    针对FrameLayout,子view具有match_parent的可能会测量两次(此类型view个数为1时测量一次)

onLayout

此处仍以FrameLayout为例,进行onLayout方法的分析

整个方法的代码不算多,直接贴在下面

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    final int count = getChildCount();

    final int parentLeft = getPaddingLeftWithForeground();
    final int parentRight = right - left - getPaddingRightWithForeground();

    final int parentTop = getPaddingTopWithForeground();
    final int parentBottom = bottom - top - getPaddingBottomWithForeground();

    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();

            int childLeft;
            int childTop;

            int gravity = lp.gravity;
            if (gravity == -1) {
                gravity = DEFAULT_CHILD_GRAVITY;
            }

            final int layoutDirection = getLayoutDirection();
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                    lp.leftMargin - lp.rightMargin;
                    break;
                case Gravity.RIGHT:
                    if (!forceLeftGravity) {
                        childLeft = parentRight - width - lp.rightMargin;
                        break;
                    }
                case Gravity.LEFT:
                default:
                    childLeft = parentLeft + lp.leftMargin;
            }

            switch (verticalGravity) {
                case Gravity.TOP:
                    childTop = parentTop + lp.topMargin;
                    break;
                case Gravity.CENTER_VERTICAL:
                    childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                    lp.topMargin - lp.bottomMargin;
                    break;
                case Gravity.BOTTOM:
                    childTop = parentBottom - height - lp.bottomMargin;
                    break;
                default:
                    childTop = parentTop + lp.topMargin;
            }

            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}

可以直接查看layoutChildren方法,总结下大致内容

  1. 拿到FrameLayout的边界值
  2. 通过布局的方向(RTL)、margin、gravity的值,计算出每个子view的左、上边界,再加上子view的宽高调用child.layout()方法
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值