Measure
目录
简介
onMeasure方法
- onMeasure测量视图和内容,以确定自己的宽高。该方法由measure(public final修饰)方法调用,子类需要重写来提供准确、高效的内容测量
- 重写此方法时,必须调用setMeasureDimension方法来存储该view的测量宽高。
- 默认的实现是背景的大小,除非指定了更大的MeasureSpec
- 重写时需要提供更好的内容测量方式
- 如果重写了该方法,应确保测量的宽高满足View的最小宽高
getSuggestedMinimumHeight
和getSuggestedMinimumWidth
MeasureSpec
- MeasureSpec封装了从父视图到子视图传递的布局需求。
- 每个MeasureSpec表示对宽度或高度的要求。
MeasureSpec包含尺寸(size)和模式(mode)。
模式 说明 UNSPECIFIED 父视图不会对子view加任何约束,想要多大就多大 EXACTLY 父视图决定子视图的具体尺寸。子view想要的大小必须在这个范围内 AT_MOST 子视图想要多大就是多大 MeasureSpec使用
int
实现以减少对象使用。并提供将size
、mode
包装和拆装的方法
内部int结构与意义
首先介绍下内部定义的一些常量值,以十六进制进行说明
名称 | 值 | 说明 |
---|---|---|
MODE_SHIFT | 30 | — |
MODE_MASK | 0x3 << MODE_SHIFT,即为二进制 11000000000000000000000000000000(32位) | — |
UNSPECIFIED | 0 << MODE_SHIFT,仍未0,二进制 00000000000000000000000000000000(32位) | 对应wrap_content |
EXACTLY | 1 << MODE_SHIFT,即为 01000000000000000000000000000000(32位) | 对应match_parent和xxdp |
AT_MOST | 2 << 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 |
相关方法
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()中,这里就不展示源码了,通过表格的方式讨论下不同情况时如何确定大小的,其中:
- childDimension为子view中LayoutParams对应的尺寸
- specMode为父View的尺寸模式
- specSize为父View的尺寸大小
- size为0和(specSize - padding)中的大值
父容器测量模式 子view期望尺寸 结果 EXACTLY childDimension >= 0 EXACTLY + childDimension childDimension == LayoutParams.MATCH_PARENT EXACTLY + size childDimension == LayoutParams.WRAP_CONTENT AT_MOST + size AT_MOST childDimension >= 0 EXACTLY + childDimension childDimension == LayoutParams.MATCH_PARENT AT_MOST + size childDimension == LayoutParams.WRAP_CONTENT AT_MOST + size UNSPECIFIED childDimension >= 0 EXACTLY + childDimension childDimension == LayoutParams.MATCH_PARENT UNSPECIFIED + (View.sUseZeroUnspecifiedMeasureSpec ? 0 : size) childDimension == LayoutParams.WRAP_CONTENT UNSPECIFIED + (View.sUseZeroUnspecifiedMeasureSpec ? 0 : size) 由上表可得出以下结论(未接触过UNSPECIFIED,暂时不提)
- 子view的onMeasure方法的参数由自己的模式+父类的模式共同决定
- 当子view为具体尺寸时,一定会拿到EXACTLY模式的尺寸参数
- 当子view和父View都是具体尺寸或者MATCH_PARENT时,子view会拿到EXACTLY模式的尺寸参数
- 如果父View是AT_MOST,除去2中的情况,子view也同样会拿到AT_MOST
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()或者测量宽高
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); }
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));
}
getSuggestedMinimumWidth()方法
protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }
其中:
- mMinWidth的值取决于R.styleable.View_minWidth的设置,也可以使用setMinimumWidth()方法设置
mDrawable.getMinimumWidth()方法的值
由Drawable的getIntrinsicWidth()
方法决定
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; }
setMeasuredDimension()方法 – 参考常用方法
ViewGroup
onMeasure
由于ViewGroup未重写此方法,此处以FrameLayout为例,会通过源码+日志的方式进行解析
遍历测量子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()); }
使用setMeasuredDimension方法设置自身大小
这里会涉及resolveSizeAndState()方法,参考常用方法
根据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
方法,总结下大致内容
- 拿到FrameLayout的边界值
- 通过布局的方向(RTL)、margin、gravity的值,计算出每个子view的左、上边界,再加上子view的宽高调用child.layout()方法