Android 自定义控件 measure
Read The Fucking Source Code
引言
Android自定义控件涉及View的绘制分发流程
源码版本(Android Q — API 29)
本文涉及Android绘制流程
1. 顶层视角预览 measure
2. MeasureSpec
2.1 MeasureSpec简介
- 测量规格(MeasureSpec) = 测量模式(mode) + 测量大小(size)。
- MeasureSpec中的值是一个整型(32位),高2位是mode(为什么只用2位?因为只有3个状态,足矣),低30位是size。这么设计主要是为了内存优化。
2.2 MeasureSpec的三种模式
- UPSPECIFIED(未指定模式) : 父容器对于子容器没有任何限制,子容器想要多大就多大
- EXACTLY(精准模式): 父容器已经为子容器设置了尺寸,子容器应当服从这些边界,不论子容器想要多大的空间。
- AT_MOST(最大模式):子容器可以是声明大小内的任意大小
2.3 MeasureSpec从何而来?
2.3.1 最顶层(DecorView)分发的MeasureSpec
最顶层分发的Measure的只有两种模式:EXACTLY / AT_MOST。(为什么呢?因为UPSPECIFIED模式在xml中不存在映射关系,只能在代码中设置,而DecorView只是从xml加载布局,后面会专门针对UPSPECIFIED进行说明)
我们来看ViewRootImpl中的getRootMeasureSpec()方法
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
//xml中的宽/高设置为MATCH_PARENT
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
//xml中的宽/高设置为WRAP_CONTENT
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
//xml中的宽/高设置为具体值 dp/px
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
2.3.2 父View对子View的MeasureSpec计算
子View的MeasureSpec值根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的。
我们来看ViewGroup中的getChildMeasureSpec()方法,看不懂不着急,下面有汇总。
//计算子View的MeasureSpec
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//父view的测量模式
int specMode = MeasureSpec.getMode(spec);
//父view的大小
int specSize = MeasureSpec.getSize(spec);
//父view出去padding能给到子View的最大值
int size = Math.max(0, specSize - padding);
//子view想要的实际大小和模式
int resultSize = 0;
int resultMode = 0;
//父View的策略模式
switch (specMode) {
// Parent has imposed an exact size on us
//父View的是精确模式
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
//父View的是最大模式
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
//父View的是未指定模式
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him 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);
}
子View的策略模式汇总
2.3.3 子View的MeasureSpec计算
我们来看View中的getDefaultSize()方法
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
//策略模式为UNSPECIFIED时,用自己入参的大小(入参取值简单,不做说明),一般默认为0
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//策略模式为AT_MOST/EXACTLY时,用父视图的大小
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
由上面的子View的MeasureSpec计算,可以引出一个问题:
自定义 View 中 如果 onMeasure 方法没有对 wrap_content 做处理,会发生什么?为什么?怎么解决?
- 如果View布局参数设置为wrap_content,而父视图为AT_MOST/EXACTLY时,对应的View的mode为AT_MOST。
- 此时View的宽高都返回从MeasureSpec中获取到的size值(也就是父视图的size)。
- 那么View的wrap_content效果和match_parent是一样的。
- 解决方案就是重写onMeasure,对AT_MOST进行特殊处理,比如给定默认宽高等。
2.3.4 UNSPECIFIED模式的单独说明
前面讲了,UNSPECIFIED模式在xml的布局声明中,是没有映射关系的,只能从代码层去设置。具体应用场景有哪些呢?举例:ListView / ScrollView。
在ScrollView中重写了ViewGroup的measureChildWithMargins()方法。
@Override
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 usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
heightUsed;
//主动设置子View的策略模式为UNSPECIFIED
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
如果大家以后想自定义控件,比如用DragHelper来实现类似SystemUI的下拉StatusBar的话,就可以使用UNSPECIFIED模式,可以完美达到预期效果。
2.3.5 MeasureSpec小结
View的measure分发,说白了就是不停的计算最终的MeasureSpec,确定最终的视图尺寸。所以会多次进行measure计算。这个大家在开发过程中想必也会从log中有所体会。
3. measure
3.1 View和ViewGroup的区别
View:测量自己尺寸,然后保存。
ViewGroup:递归遍历所有子View,测量子View尺寸,然后保存;根据所有子View的尺寸计算保存自己的尺寸。
3.2 View和ViewGroup的汇总
3.2.1 View的 measure 过程
3.2.2 ViewGroup的 measure 过程
3.3 问题思考
View 的 measure 方法和 onMeasure 方法有什么区别和关系?
- measure 方法使用了 final 来修饰,说明是不可修改的,onMeasure 方法则是可以让子类按需重写。
- measure 方法用于检测缓存数据,对比是否要重新测量。
- onMeasure 方法用于读取父布局的测量规则并按需求定制自己的测量规则,调用 setMeasuredDimension 确定自己的尺寸。
ViewGroup 里面有重写 onMeasure 方法吗?为什么?
- ViewGroup 默认是没有重写 onMeasure 的,重写 onMeasure 方法这个任务是交给 ViewGroup 的子类的。
- 不同的 ViewGroup 子类(LinearLayout、FrameLayout 等),它们的布局要求往往都不一样,那 onMeasure 方法就交给他们自己重写好了。
为什么ViewGroup的measure过程不像单一View的measure过程那样对onMeasure做统一的实现?
- onMeasure()的作用 = 测量View的宽/高值。
- 因为不同的ViewGroup子类(LinearLayout、RelativeLayout / 自定义ViewGroup子类等)具备不同的布局特性,这导致他们子View的测量方法各有不同。
- 在单一View measure过程中,getDefaultSize()只是简单的测量了宽高值,在实际使用时有时需更精细的测量。所以有时候也需重写onMeasure()。
- 在自定义ViewGroup中,关键在于:根据需求复写onMeasure()从而实现你的子View测量逻辑。
View 的测试方法为什么会给多次调用? View 在什么情况下 getMeasuredWidht/Height() 和 getWidht/Height(),结果是不一致?
- View 的测量方法会多次调用是因为前后的测量结果可能并不一致,比分说 LinearLayout 的权重,View 的第一次测量结果和最终的测量结果肯定是不一样的。
- View 的 getMeasuredWidht/Height() 和 getWidht/Height() ,它们大多数情况下是一致的,但是在一种情况下是例外的,那就是在 layout 方法中重新设置 View 的位置大小,从而改变 View 的宽高,因为最终确定 View 的实际尺寸和位置信息的就是 setFrame 方法。
- getMeasuredWidth方法获得的值是setMeasuredDimension方法设置的值,它的值在measure方法运行后就会确定。
- getWidth方法获得是layout方法中传递的四个参数中的mRight-mLeft,它的值是在layout方法运行后确定的。
- 一般情况下在onLayout方法中使用getMeasuredWidth方法,而在除onLayout方法之外的地方用getWidth方法。
小编的扩展链接
优秀博客推荐
Android开发之自定义控件(一)---onMeasure详解
Android开发之getMeasuredWidth和getWidth区别从源码分析
自定义View Measure过程 - 最易懂的自定义View原理系列(2)
Android应用层View绘制流程与源码分析