一、概念
1.1 为什么要重写 onMeasure()
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) |
自定义View | 给宽高设置 100dp 或 match_parent 都是一个具体值,设置 wrap_content 就需要计算出一个实际的大小,而 View 中的默认实现使得设置 wrap_content 效果和 match_parent 一样,想要实现该效果就需要手动重写。(参考计算子元素 MeasureSpec 的表,当子元素设置 wrap_content 时父容器 AT_MOST 和 EXACTLY 模式都返回 parentSize 大小,无法像 TextView 那样在不超过最大宽度的情况下显示实际内容宽度) |
自定义ViewGroup | ViewGroup 是一个抽象类,它没有重写父类中的 onMeasure() 方法,因为不同内容测量起来代码无法统一实现,但提供了三个测量方法方便我们测量(measureChildren、measureChild、measureChildWithMargins)。 |
1.2 测量流程
父容器递归调用子元素的 measure( ) 方法来测量子元素,传入的测量规格(widthMeasureSpec 和 heightMeasureSpec)是父容器对子元素的尺寸限制。之后调用真正测量的地方 onMeasure( ) 正是我们要重写的,在这里算出【自身需要占用的尺寸】并结合【父容器给的尺寸限制】得出【实际占用大小】,最终调用 setMeasuredDimension( ) 保存,用在后面的 onLayout() 布局阶段使用。
1.3 测量规格 MeasureSpec
onMeasure() 中的两个参数分别是该元素宽高的测量规格 MeasureSpec,这是在父容器中计算好后传过来的,这形成了一个链条。View 绘制流程开始于 ViewRootImpl 的 performTraversals() 中,链条头部 DecorView 的参数正是在这里被传入的(size是屏幕宽高,mode是EXACTLY)。
通过使用二进制,将测量模式 mode(前2位)测量大小 size(后30位)打包成一个测量规格 MeasureSpac(32位int值)并提供打包和解包的方法,好处是减少创建对象。三种测量模式的值0、1、2的二进制为00、01、10刚好用2位能表示。
自定义View | 由于 MeasureSpec 都是在父容器中计算好后传到子元素中的,自定义 View 不包含子元素不用管。 |
自定义ViewGroup | 自身作为父容器就需要遍历子元素挨个算出它们的 MeasureSpec 并传递。为什么需要父容器参与才能确定子元素,是因为 match_parent 需要知道自身多大才能匹配父容器,wrap_content 需要知道自身不能超过多少。 |
mode 测量模式 | EXACTLY 精准模式 | 父容器为子元素指定一个确切的大小。 | 子元素 xml 设置了 match_parent 或具体数值如100dp。 |
AT_MOST 最大模式 | 父容器为子元素指定一个最大尺寸,子元素最大不可超过该尺寸。 | 子元素 xml 设置了 wrap_content。 | |
UNSPECIFIED 无限制模式 | 父容器不约束子元素,即子元素可设置任意尺寸。 | 用于可滑动布局如系统的ListView、ScrollView,一般自定义控件用不到。 | |
size 测量大小 | 并不是子元素的最终尺寸,它是父容器对子元素能多大的限制,子元素计算出实际尺寸后需要综合这个限制来决定最终尺寸,即调用 resolveSize(int size, int measureSpec)。 |
getMode() | public static int getMode(int measureSpec) 获取模式(即高2位)。 |
getSize() | public static int getSize(int measureSpec) 获取大小(即低30位)。 |
makeMeasureSpec() | public static int makeMeasureSpec(int size, int mode) 传入 size 和 mode 来生成 MeasureSpec。 |
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec) //获取宽度的模式(占Int前2位)
val widthSize = MeasureSpec.getSize(widthMeasureSpec) //获取宽度的大小(占Int的后30位)
val measureSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode) //通过模式和大小生成规格
}
二、自定义控件怎么写
2.1 修改已有控件
现有的控件已经有了自己的正确尺寸算法,结果可以作为参考值根据我们的需求进行调整。
- 自定义子类继承已有的控件,重写构造。
- 重写 onMeasure(),调用 super.onMeasure() 进行一次原有的测量,通过 getMeasuredHeight()、getMeasuredWidth() 获取测量结果并修改成想要的值。
- 最终调用 setMeasuredDimension() 保存。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//先触发原有测量(计算完后已经调用过setMeasuredDimension(),所以可通过getMeasuredHeight()、getMeasuredWidth()拿到测量结果)
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//获取测量结果并修改为想要的尺寸
val needWidth = measuredWidth + 1
val needHeight = measuredHeight + 1
//保存最终值(是自己的期望尺寸,父容器用作参考,实际值父元素通过layout()传过来)
setMeasuredDimension(needWidth, needHeight)
}
2.2 自定义View
- 自定义子类继承 View,重写构造。
- 计算内部每个部分的尺寸最后加起来就是自身需要占用的尺寸。
- 综合父容器给的限制(widthMeasureSpec 、heightMeasureSpec),一并传入到 resolveSize() 中得出实际能占用的尺寸。
- 最终调用 setMeasuredDimension() 保存。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//计算出的占用尺寸
val needWidth = 0
val needHeight = 0
//得到最终期待尺寸
val finalWidth = resolveSize(needWidth, widthMeasureSpec)
val finalHeight = resolveSize(needHeight, heightMeasureSpec)
//保存
setMeasuredDimension(finalWidth, finalHeight)
}
2.3 自定义 ViewGroup
- 自定义子类继承 ViewGroup,重写构造。
- 对宽高的 MeasureSpec 进行判断,如果是确切值EXACTLY(如100dp或match_parent)就直接调用 setMeasuredDimension() 保存,否则计算实际大小。
-
- 如果是自定义ViewGroup中代码添加子元素:计算出子元素的宽高,算出子元素的 MeasureSpec 调用子元素的 measure() 测量子元素,通过 getMeasuredXXX() 获取单个子元素测量后的宽高,通过累加计算出自身需要占用的尺寸。
- 如果是xml布局包裹添加子元素:先调用 measureChildren() 测量所有子元素,再遍历子元素,通过 getMeasuredXXX() 获取单个子元素测量后的宽高,通过累加计算出自身需要占用的尺寸。
- 综合自身的 MeasureSpec 一并传入 resolveSize() 中得出实际能占用的尺寸。
- 最终调用 setMeasuredDimension() 保存。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//需要的尺寸
var needWidth = 0
var needHeight = 0
//获取自身测量模式和可用大小
val selfWidthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
val selfWidthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
val selfHeightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
val selfHeightSpecSize = MeasureSpec.getSize(heightMeasureSpec)
//自身宽高是固定值或者mutch_parent就直接设置确切值,否则计算实际大小
if (selfWidthSpecMode == MeasureSpec.EXACTLY && selfHeightSpecMode == MeasureSpec.EXACTLY) {
setMeasuredDimension(selfWidthSpecSize, selfHeightSpecSize)
} else {
//测量所有子元素
measureChildren(widthMeasureSpec, heightMeasureSpec)
//遍历子元素
for (i in 0 until childCount) {
val childView = getChildAt(i)
//获取测量后的子元素的宽高
val childMeasuredWidth = childView.measuredWidth
val childMeasuredHeight = childView.measuredHeight
//通过子元素计算自身需要的尺寸
//needWidth =
//needHeight =
}
//综合需要的尺寸和MeasureSpec的限制得出实际占用尺寸
val finalWidth = resolveSize(needWidth, widthMeasureSpec)
val finalHeight = resolveSize(needHeight, heightMeasureSpec)
//设置尺寸
setMeasuredDimension(finalWidth, finalHeight)
}
}
三、源码
以上是 View 默认实现流程。
3.1 measure()
测量的入口,由父容器调用,父容器计算出该子元素的MeasureSpec后通过此方法传递过来,该方法会调用 onMeasure()。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
if (cacheIndex < 0 || sIgnoreMeasureCache) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
...
}
3.2 onMeasure()
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
3.3 getSuggestedMinimumXXX()
若 View 未设置背景,宽度为 android:minWidth 属性所指定的值,未指定则为0。若设置了背景,宽度为该属性和背景Drawable宽度中的最大值。高同理。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
3.4 getDefaultSize()
综合 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: //正是这里使得 wrap_content 和 matchParent 效果一样
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
3.5 setMeasuredDimension()
保存测量尺寸。
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
}
3.6 resolveSize()
综合【计算出的需要占用的大小】和【自身的测量规格】得出【最终实际占用大小】,解决了默认实现中 wrap_content 直接返回 parentSize 和 match_parent 效果一样的问题。
public static int resolveSize(int size, int measureSpec) {
return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}
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) {
//对于AT_MOST,取实际值和最大值中最小的(实际值超过最大值界面会显示不完全)
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
//对于EXACTLY,直接返回确切值
case MeasureSpec.EXACTLY:
result = specSize;
break;
//对于UNSPECIFIED,直接返回实际值
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
3.7 measureChildran()
遍历并测量子元素。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
3.8 measureChild()
获取子元素的 LayoutParams,获取父容器的 Padding,综合父容器的 MeasureSpec,计算出子元素的 MeasureSpec,然后调用子元素的 measure() 传递过去。
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
3.9 获取子元素xml中设置的宽高 getLayoutParams()
LayoutParams 是 ViewGrogup 中的静态内部类,封装了 xml 中对控件 layout_width、layout_height 设置的宽高值(MARCH_PARENT=-1、WRAP_CONTENT=-2、或者具体值dp/px),通过属性调用 .width 和 .height 拿到具体宽高值。
3.10 获取父容器设置的内边距 getPaddingXXX()
计算子元素的 widthMeasureSpec 时传入 getPaddingLeft( ) + getPaddingRight( )。
计算子元素的 heightMeasureSpec 时传入 getPaddingTop( ) + getPaddingBottom( )。
3.11 计算子元素的测量规格 getChildMeasureSpec()
父容器的测量模式有3种情况(EXACTLY、AT_MOST、UNSPECIFIED),子元素xml设置的值也有3种情况(具体值、match_parent、wrap_content),一共有9种可能如下表。
算出子元素 MeasureSpec | 父容器测量模式 mode | |||
EXACTLY (只给多少) | AT_MOST (最多能给多少) | UNSPECIFIED (不限制) | ||
子元素宽高 LayoutParams | 100dp (具体值) | EXACTLY + childSize | EXACTLY + childSize | EXACTLY + childSize |
match_parent (全要) | EXACTLY + parentSize (父容器剩余空间) | AT_MOST + parentSize (大小不超过父容器剩余空间) | UNSPECIFIED + 0 | |
wrap_content (不确定要多少) | AT_MOST + parentSize (大小不超过父容器剩余空间) | AT_MOST + parentSize (大小不超过父容器剩余空间) | UNSPECIFIED + 0 |
//参数spec:父容器的MeasureSpec。
//参数padding:父容器设置的padding。
//参数childDimension:子元素在xml中设置的宽高。
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) {
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//父容器EXACTLY + 子元素固定值 = EXACTLY + childSize
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//父容器EXACTLY + 子元素much_parent = EXACTLY + parentSize
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//父容器EXACTLY + 子元素wrap_content = AT_MOST + parentSize
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
//父容器AT_MOST + 子元素固定值 = EXACTLY + childSize
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//父容器AT_MOST + 子元素much_parent = AT_MOST + parentSize
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//父容器AT_MOST + 子元素wrap_content = AT_MOST + parentSize
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
//父容器UNSPECIFIED + 子元素固定值 = EXACTLY + childSize
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//父容器UNSPECIFIED + 子元素MATCH_PARENT = UNSPECIFIED + 0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//父容器UNSPECIFIED + 子元素WRAP_CONTENT = UNSPECIFIED + 0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//生成并返回子元素MeasureSpec
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}