文章目录
View 的整体绘制流程
View 的绘制都是由 ViewRootImpl 完成,Window 和 DecorView 通过 ViewRootImpl 关联。具体绘制流程可以详细参考文章:View绘制流程源码解析
MeasureSpec
获取测量大小和测量模式
在布局过程中,经常会使用到 MeasureSpec 根据 onMeasure(int widthMeasureSpec, int heightMeasureSpec)
提供的两个参数 widthMeasureSpec 和 heightMeasureSpec 获取测量的宽高和测量模式,或者生成 widthMeasureSpec 和 heightMeasureSpec。
其中,widthMeasureSpec 和 heightMeasureSpec 是一个 32 位的 int 值,高 2 位是测量模式 specMode,低 30 位是测量大小 specSize。
宽高和测量模式使用如下方法获取和设置:
-
MeasureSpec.getMode(measureSpec):获取测量模式 specMode
-
MeasureSpec.getSize(measureSpec):获取测量大小 specSize
-
MeasureSpec.makeMeasureSpec(int size, int mode):根据自己设置的测量大小和测量模式生成一个 widthMeasureSpec 或 heightMeasureSpec
三种测量模式
-
EXACTLY:精确值模式,对应将控件的 layout_width 属性或 layout_height 属性指定为具体数值时,比如
android:layout_width=”100dp”
,或者指定为 match_parent(View 类的onMeasure()
默认的模式,所以需要指定 wrap_content 就要重写onMeasure()
) -
AT_MOST:最大值模式,对应将控件的 layout_width 属性或 layout_height 属性指定为 wrap_content 时,此时控件的最大尺寸只要不超过父控件允许的最大尺寸即可(即能有多大就给多大)
-
UNSPECIFIED:未指定模式,它主要用于在需要多次测量的场景中作为标记的一种测量模式。平常很少用到,甚至你可以几乎当它不存在
布局过程
布局过程的含义
布局过程,就是程序在运行时利用布局文件的代码来计算出实际尺寸的过程。
布局过程的工作内容
两个阶段:测量阶段和布局阶段。
-
测量阶段:从上到下递归地调用每个 View 或者 ViewGroup 的
measure()
,测量它们的尺寸并计算它们的位置 -
布局阶段:从上到下调用每个 View 或者 ViewGroup 的
layout()
,把测得的它们的尺寸和位置赋值给它们
View 或 ViewGroup 的布局过程
-
测量阶段:
measure()
被父 View 调用,在measure()
中做一些准备和优化工作后,调用onMeasure()
来进行实际的自我测量。onMeasure()
做的事,View 和 ViewGroup 不一样:-
View:View 在
onMeasure()
中会计算出自己的尺寸然后保存 -
ViewGroup:ViewGroup 在
onMeasure()
中会调用所有子 View 的measure()
让它们进行自我测量,并根据子 View 计算出期望尺寸来计算出它们的实际尺寸和位置(实际上99%的父 View 都会使用子 View 绘制出的期望尺寸来作为实际尺寸)然后保存。同时,它也会根据子 View 的尺寸和位置来计算出自己的尺寸然后保存
-
-
布局阶段:
layout()
被父 View 调用,在layout()
中它会保存父 View 传进来的自己的位置和尺寸,并且调用onLayout()
来进行实际的内部布局。onLayout()
做的事,View 和 ViewGroup 也不一样:-
View:由于没有子 View,所以 View 的
onLayout()
什么也不做 -
ViewGroup:ViewGroup 在
onLayout()
中会调用自己的所有子 View 的layout()
,把它们的尺寸和位置传给它们,让它们完成自我的内部布局
-
所以测量和布局阶段的整体流程如下图:
布局过程自定义的方式
有三种自定义方式:
-
重写
onMeasure()
来修改已有的 View 的尺寸 -
重写
onMeasure()
来全新定制自定义 View 的尺寸 -
重写
onMeasure()
和onLayout()
来全新定制自定义 ViewGroup 的内部布局
重写 onMeasure 修改已有的 View 的尺寸
-
重写
onMeasure()
,并在里面调用super.onMeasure()
,触发原有的自我测量 -
在
super.onMeasure()
的下面用getMeasureWidth()
和getMeasureHeight()
来获取之前的测量结果,并使用自己的算法,根据测量结果计算出新的结果 -
调用
setMeasureDimension()
来保存新的结果
public class SquareImageView extends ImageView {
...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 先执行原测量算法
// super.onMeasure() 也是调用了 setMeasureDimension()
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 获取原先的测量结果
int measureWidth = getMeasureWidth();
int measureHeight = getMeasureHeight();
// 利用原先的测量结果计算出尺寸
int size = Math.min(measureWidth, measureHeight);
// 保存计算后的结果
setMeasureDimension(size, size);
}
}
如果重写 layout()
,你会发现也是可以实现相同的效果:
public class SquareImageView extends ImageView {
...
@Override
public void layout(int l, int t, int r, int b) {
int width = r - l;
int height = b - t;
int size = Math.min(width, height);
super.layout(l, t, l + size, t + size);
}
}
但是我们为什么不在这里写,要在 onMeasure()
呢?
因为在 onMeasure()
的时候保存的测量尺寸在父 View 是可知的,父 View 会根据你测量的尺寸来布局你的位置;但是在 layout()
的时候修改大小父 View 就是不可知的了,这会导致当父 View 内有多个子 View,你在 layout()
又重写了自己的大小,而父 View 仍旧按照你在 onMeasure()
时测量的尺寸来摆放你的位置,就会出现布局错误。
重写 onMeasure() 来全新定制自定义 View 的尺寸
-
重写
onMeasure()
把尺寸计算出来 -
计算出自己的尺寸
-
用
resolveSize()
或者resolveSizeAndState()
修正结果 -
使用
setMeasuredDimension()
保存结果
public class CircleView extends View {
private static final float RADIUS = Utils.dpToPx(80);
private static final float PADDING = Utils.dpToPx(30);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 自己定义的 View 宽高
int size = (int) (PADDING + RADIUS) * 2);
// 将子 View 提供的宽高大小修正,让宽高符合父 View 的要求
// 父 View 的宽高要求就在 widthMeasureSpec 和 heightMeasureSpec
int width = resolveSize(size, widthMeasureSpec);
int height = resolveSize(size, heightMeasureSpec);
setMeasureDimension(width , height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.RED);
canvas.drawCircle(PADDING + RADIUS, PADDING + RADIUS, RADIUS, paint);
}
}
onMeasure()
的 widthMeasureSpec 和 heightMeasureSpec 有特殊的含义:
从上面我们知道,子 View 的测量是父 View 发起 measure()
然后让子 View 从上到下递归自测量,而 widthMeasureSpec 和 heightMeasureSpec 就是父 View 提供给子 View 的一个测量宽高最大限度。
widthMeasureSpec 和 heightMeasureSpec 对于子 View 来说,就是处理自己在xml布局设置的 layout_width 和 layout_height。
自定义子 View 最主要的就是处理 wrap_content,也就是 MeasureSpec.AT_MOST 的情况。在 MeasureSpec.AT_MOST 情况将自己想要的大小和父 View 限定给我们的大小对比(父 View 的大小通过 onMeasure()
提供的 widthMeasureSpec 或 heightMeasureSpec 解析出来 int specSize = MeasureSpec.getSize(measureSpec)
),如果想要的大小超过父 view 的大小就使用限定大小 specSize,否则用我们自己的大小。
其实上面的说明就是 resolveSize()
做的事情,可能看了上面描述比较难理解,再对比下面的伪代码相信就能看懂:
public static int resolveSize(int size, int measureSpec) {
final int specMode = MeasureSpec.getMode(measureSpec);
// 父 View 限定的最大可用大小
final int specSize = MeasureSpec.getSize(measureSpec);
switch(specMode) {
// 父 View 提供的 specMode 为 MeasureSpec.AT_MOST,需要特殊处理
case MeasureSpec.AT_MOST:
// 如果子 View 想要的大小比父 View 提供最大可用大小要小
// 父 View 能满足,就提供给子 View 想要的大小
if (size <= specSize) {
return size;
}
// 否则使用父 View 提供的最大可用大小
else {
return specSize;
}
// 父 View 提供的 specMode 为精确值,直接返回父 View 提供的大小
case MeasureSpec.EXACTLY:
return specSize;
default:
return size;
}
}
}
问题:getWidth()
、getHeight()
和 getMeasureWidth()
、getMeasureHeight()
的区别?
getWidth()
和 getHeight()
是确定了 View 的最终宽高,形成于 layout 过程;getMeasureWidth()
和 getMeasureHeight()
是确定了 View 的测量宽高,形成于 measure 过程。
一般情况下最终宽高和测量宽高几乎相同。除非重写了 layout()
导致最终宽高与测量宽高不同。
定制 Layout 的内部布局
步骤:
-
重写
onMeasure()
-
遍历每个子 View,用
measureChildWidthMargins()
测量子 View-
重写
generateLayoutParams()
提供 MarginLayoutParams 对象 -
有些子 View 可能需要重新测量
-
测量完毕后,得出子 View 的实际位置和尺寸,并暂时保存
-
-
测量出所有子 View 的位置和尺寸后,计算出自己的尺寸,并用
setMeasureDimension()
保存
-
-
重写
onLayout()
- 遍历每个子 View,调用它们的
layout()
来将位置和尺寸传给它们
- 遍历每个子 View,调用它们的
public class TagLayout extends ViewGroup {
private List<Rect> childRectBounds = new ArrayList<>();
public TagLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthUsed = 0; // 已使用父 View 的宽度,按最大的子 View 宽度来就行了
int heightUsed = 0; // 已使用父 View 的高度,每一行最大子 View 高度的叠加和
int lineWidthUsed = 0; // 当前行的已用宽度
int lineHeight = 0; // 当前行最大子 View 的高度
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// 根据父 View 的 MeasureSpec 限定大小以及子 View 的 LayoutParams 共同决定去测量子 View 的大小
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
// 父 View 的 MeasureSpec 不能是 UNSPECIFIED 无限大,当前已用宽度+下一个测量子View的宽度 > 父View的限定宽度,换行
if (widthMode != MeasureSpec.UNSPECIFIED && ((lineWidthUsed + child.getMeasuredWidth()) > widthSize)) {
// 当前行的已用宽度置0
lineWidthUsed = 0;
// 已使用父 View 的高度增加为当前行的最大子 View 的高度
heightUsed += lineHeight;
// 重新计算
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
}
// 测量后保存子 View 的摆放位置,每一行因子 View 标签文本不同而摆放数量不同
Rect childRect;
if (childRectBounds.size() <= i) {
childRect = new Rect();
childRectBounds.add(childRect);
} else {
childRect = childRectBounds.get(i);
}
// left:添加一个子 View 后,上一个子 View 的宽度就是下一个子 View 的起始位置
// top:其实就是每一行最大的 View 高度作为下一行的起始高度
// right:left + 当前 View 的宽度
// bottom:top + 当前 View 的高度
childRect.set(lineWidthUsed, heightUsed, lineWidthUsed + child.getMeasuredWidth(),
heightUsed + child.getMeasuredHeight());
// 该行添加子 View 的宽度方便下一个子 View 的位置摆放计算
lineWidthUsed += child.getMeasuredWidth();
widthUsed = Math.max(lineWidthUsed, widthUsed);
// 该行最大子 View 高度,换行时使用
lineHeight = Math.max(lineHeight, child.getMeasuredHeight());
}
int measureWidth = widthUsed;
heightUsed += lineHeight;
int measureHeight = heightUsed;
setMeasuredDimension(measureWidth, measureHeight);
}
// 使用使用 measureChildWithMargins() 时,内部会获取到 childView 的 LayoutParams 并将它强转为 MarginLayoutParams
// 需要使用该方法转换,否则抛出 ClassCastException
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 只需要根据测量的结果循环摆放子 View 的位置即可
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
Rect rect = childRectBounds.get(i);
child.layout(rect.left, rect.top, rect.right, rect.bottom);
}
}
}
关于保存子 View 位置的两点说明:
-
不是所有的 Layout 都需要保存子 View 的位置(因为有的 Layout 可以在布局阶段实时推导出子 View 的位置,例如 LinearLayout)
-
有时候对某些子 View 需要重复测量两次或多次才能得到正确的尺寸和位置
上面的示例代码有一个方法需要特别留意:measureChildWithMargins()
,该方法是根据父 View 的 MeasureSpec 限定大小以及子 View 的 LayoutParams 共同决定测出子 View 的大小。
那具体是怎样决定的呢?(为了看起来更简洁,代码将 padding、margin 和 MeasureSpec.UNSPECIFIED 去除了,需要具体了解怎么处理的可以查看源码)
protected void measureChildWithMargins(
View child,
int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
// childDimension 就是子 View 提供的 layout_width 或 layout_height
public static int getChildMeasureSpec(int spec, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize);
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;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
measureChildWithMargins()
做的事情可以如下理解(specSize 为限定子 View 可用的最大大小):
-
父 View 的 specMode 是 MeasureSpec.EXACTLY:
-
子 View 是具体数值,返回具体数值,测量模式 MeasureSpec.EXACTLY
-
子 View 是 match_parent,返回 specSize,测量模式 MeasureSpec.EXACTLY(父 View 多大我就多大)
-
子 View 是 wrap_content,返回 specSize,测量模式 MeasureSpec.AT_MOST(只要不超过父 View 的限定大小即可)
-
-
父 View 的 specMode 是 MeasureSpec.AT_MOST:
-
子 View 是具体数值,返回具体数值,测量模式 MeasureSpec.EXACTLY
-
子 View 是 match_parent,返回 specSize,测量模式 MeasureSpec.AT_MOST(此时父 View 的大小也没固定,只要不超过父 View 的限定大小即可)
-
子 View 是 wrap_content,返回 specSize,测量模式 MeasureSpec.AT_MOST(只要不超过父 View 的限定大小即可)
-
上面虽然罗列了具体的处理,能否再简化更好理解呢?接着看:
class MyLayout(context: Context) : CustomLayout(context) {
val header = AppCompatImageView(context).apply {
...
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
header.autoMeasure()
}
}
abstract class CustomLayout(context: Context) : ViewGroup(context) {
fun View.autoMeasure() {
measure(
this.defaultWidthMeasureSpec(parentView = this@CustomLayout),
this.defaultHeightMeasureSpec(parentView = this@CustomLayout)
)
}
fun View.defaultWidthMeasureSpec(parentView: ViewGroup) : Int {
return when (layoutParams.width) {
// 父 View 多大我就多大,那子 View 的大小就是父 View 的测量大小
// 测量模式是 MeasureSpec.EXACTLY
ViewGroup.LayoutParams.MATCH_PARENT -> parentView.measuredWidth.toExactlyMeasureSpec()
// 要自适应大小,可以提供可用的最大大小
// 测量模式是 MeasureSpec.AT_MOST
ViewGroup.LayoutParams.WRAP_CONTENT -> parentView.measuredWidth.toAtMostMeasureSpec()
// 具体数值,直接返回
else -> layoutParams.width.toExactlyMeasureSpec()
}
}
fun View.defaultHeightMeasureSpec(parentView: ViewGroup) : Int {
return when (layoutParams.height) {
ViewGroup.LayoutParams.MATCH_PARENT -> parentView.measuredHeight.toExactlyMeasureSpec()
ViewGroup.LayoutParams.WRAP_CONTENT -> parentView.measuredHeight.toAtMostMeasureSpec()
else -> layoutParams.height.toExactlyMeasureSpec()
}
}
fun Int.toExactlyMeasureSpec() : Int {
return MeasureSpec.makeMeasureSpec(this, MeasureSpec.EXACTLY)
}
fun Int.toAtMostMeasureSpec() : Int {
return MeasureSpec.makeMeasureSpec(this, MeasureSpec.AT_MOST)
}
}
站在子 View 的角度理解测量会比较好理解一些了:
-
子 View 是 match_parent,返回的就是父 View 的大小,测量模式是 MeasureSpec.EXACTLY(父 View 多大我就多大)
-
子 View 是 wrap_content,测量模式是 MeasureSpec.AT_MOST
-
子 View 是具体数值,返回具体数值
或许你会有疑惑?为什么子 View 是 wrap_content 时没有说测量的大小?因为当子 View 的 LayoutParams 是 wrap_content 且测量模式是 MeasureSpec.AT_MOST 时,子 View 大多会完全忽略 MeasureSpec 提供的 size,也就是说,这个 size 数值是多少都不影响测量。
fun View.defaultWidthMeasureSpec(parentView: ViewGroup) : Int {
return when (layoutParams.width) {
// 将测量的 size 替换为 ViewGroup.LayoutParams.WRAP_CONTENT 也不影响子 View 的测量
ViewGroup.LayoutParams.WRAP_CONTENT -> ViewGroup.LayoutParams.WRAP_CONTENT.toAtMostMeasureSpec()
}
}
自定义 View 注意事项
-
让 View 支持 wrap_content:如果是直接继承 View 或 ViewGroup,且自定义 View 的宽高有 wrap_content,需要重写
onMeasure()
进行处理 -
如果有必要,让自定义 View 支持 padding:如果是直接继承 View,要在
onDraw()
中处理padding(margin 在父容器会处理),否则无法起作用;如果是直接继承 ViewGroup,要在onMeasure()
和onLayout()
中处理 padding 和 margin,否则无法起作用;getPaddingLeft()
、getPaddingTop()
、getPaddingRight()
、getPaddingBottom()
获取 padding -
尽量不要在自定义 View 中使用 Handler,没必要:View 内部提供了
post()
可以替代 Handler 的作用,除非明确需要 Handler 发送消息 -
自定义 View 中如果有线程或动画,要及时停止:如果自定义 View 中有线程或动画,在
onDetachedFromWindow()
停止线程或动画(Activity 退出或当前 View 被 remove 时调用,onAttachedToWindow()
相反);不及时处理容易造成内存泄漏 -
自定义 View 带有滑动嵌套情形时,要处理滑动冲突