Android-View Measure方法探究
Android-View Measure方法探究
前言
之前业务上写BubbleView三角气泡弹窗时需要动态测绘BubbleView的宽高, 并且使用TextView测量时产生了错误, 对measuredWidth等View属性理解也不到位《 因此想仔细看下View以及典型View等Measure方法
Dependency
Android Compile Version: 33
View的Measure方法
首先需要确定, View的measure方法时通用的(final). 执行具体高度测量的方法在的onMeasure方法中(protected, 开放给其他View), 每个自定义View的onMeasure方法应该各有不同
测绘自身高
需要先考虑有一些ViewGroup有自带的效果, 比如阴影、光晕等, 可能会导致实际宽高与实际不一致, 该种模式下 ViewGroup的mLayoutMode
字段为LAYOUT_MODE_OPTICAL_BOUNDS
, 需要进行特殊判断
如果父ViewGroup与该View位于同一种不是需要该特殊处理的情况, 则调用computeOpticalInsets
来拿表示该View的位置的Insets(类似一个Rect)
public static final @NonNull Insets NONE = new Insets(0, 0, 0, 0);
Insets computeOpticalInsets() {
return (mBackground == null) ? Insets.NONE : mBackground.getOpticalInsets();
}
将宽、高的measureSpec绑定在一个64位的数上, 其中width为高位, height为低位
measure过程中产生的数据会进行缓存, 上一次measure的spec会传到mOldWidthMeasureSpec
与 mOldHeightMeasureSpec
中。 缓存到LongSparseLongArray
提示
LongSparseLongArray是一个类似HashMap的工具, 但其比HashMap更高效, 因为其只存储Long to Long类型的KV数据, 优化了键冲突的情况
优化: 通过传递进的宽高measureSpec与上一次的measureSpec进行比较(针对测量情况为Exactly
)
测量流程
该步是measure的核心逻辑, 是否执行需要根据以下两个条件确定
- 是否需要强制指定了要刷新布局, 通过字段mPrivateFlags判断
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
- 是否需要布局
- MeasureSpec改变(新旧宽高的measureSpec对比)
- 以下条件任选其一:
- 是否指定了每次measure时都需要重新测量宽高 (
sAlwaysRemeasureExactly
字段) - 宽高的MeasureSpec中不存在EXACTLY的测量策略
- 当前View的mMeasuredWidth或mMeasuredHeight与传递的widthMeasureSpec或heightMeasureSpec指定的width不同
- 是否指定了每次measure时都需要重新测量宽高 (
具体执行步骤如下:
清空View的Flags标记, 表明当前View未被测量
解决RTL相关问题
看是否要求强制刷新:
- 要求刷新: 直接通过当前宽高的measureSpec执行onMeasure方法。此时会清除Flag
PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT
, 表示measure方法确实已执行, 在layout时可不需要再次调用measure方法 - 不要求: 则尝试通过当前组合的64位Spec的key, 从之前的
mMeasureCache
去拿缓存- 若无缓存或者View指定了忽略从cache拿measure数据(具体字段为
sIgnoreMeasureCache
): 则还是走要求刷新的步骤 - 若有缓存: 调用
setMeasuredDimensionRaw
方法, 直接基于缓存值设置mMeasuredWidth mMeasuredHeight, 并设置PFLAG_MEASURED_DIMENSION_SET
, 表明测量已完成。此时设置FlagPFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT
, 表示下次layout时还需要进行一次measure
- 若无缓存或者View指定了忽略从cache拿measure数据(具体字段为
异常判断, 判断Flag PFLAG_MEASURED_DIMENSION_SET
是否存在, 此时若正常执行完毕, 该Flag一定为true
由于可能进行了实际的测量, View的宽高值可能与上一次不一致了, 因此上一次layout确定的位置可能也不再正确, 需要设置Flag PFLAG_LAYOUT_REQUIRED
指示需要重新layout
收尾
为mOldHeightMeasureSpec
mMeasureCache
等缓存赋值
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL);
/**
* <p>
* This is called to find out how big a view should be. The parent
* supplies constraint information in the width and height parameters.
* </p>
*
* <p>
* The actual measurement work of a view is performed in
* {@link #onMeasure(int, int)}, called by this method. Therefore, only
* {@link #onMeasure(int, int)} can and must be overridden by subclasses.
* </p>
*
*
* @param widthMeasureSpec Horizontal space requirements as imposed by the
* parent
* @param heightMeasureSpec Vertical space requirements as imposed by the
* parent
*
* @see #onMeasure(int, int)
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 判断是否是Optical优化的的ViewGroup
boolean optical = isLayoutModeOptical(this);
// 判断下父ViewGroup是否是Optical优化的
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
// Suppress sign extension for the low bytes
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
// Optimize layout by avoiding an extra EXACTLY pass when the view is
// already measured as the correct size. In API 23 and below, this
// extra pass is required to make LinearLayout re-distribute weight.
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
if (forceLayout || needsLayout) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
View-onMeasure方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
尝试获取到View的最小宽度值(通过background以及指定的minWidth去获取)
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
根据MeasureSpec指定值, 获取默认的宽/高大小
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;
}
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);
}
View measure中的MeasureSpec如何而来?
核心: 先根据parent的ViewGroup中MeasureSpec来确定, 来进行判断
举个例子, 例如ViewGroup
方法 measureChildWithMargins
中, 实际是调用了getChildMeasureSpec
进行判断
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);
}
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let them have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}