一、view的结构层次
View的绘制是由ViewRoot来负责的,每个应用程序窗口的decorView都有一个与之关联的ViewRoot对象,而这种关联关系是由WindowManager来维护的。
二、View创建到呈现的三个阶段
在Android中,一个View从被创建到最终显示在屏幕上,必须经历三个核心阶段:
- Measure (测量) : 确定View的宽高(
MeasuredWidth和MeasuredHeight)。 - Layout (布局) : 确定View在父容器中的具体位置(四个顶点坐标:
Left,Top,Right,Bottom)。 - Draw (绘制) : 将View的内容渲染到Canvas上。
这三个过程遵循 “自上而下” 的递归原则。顶层ViewGroup(如DecorView)发起调用,逐层传递,最终完成整棵视图树的渲染。
第一阶段测量Measure
测量的目的是为了确定一个View的尺寸。在Android中,尺寸的确定并不是单纯由子View决定,而是由父View的约束和子View自身的需求共同决定的。
MeasureSpec的三种模式
| 模式 | 含义 | 对应场景(LayoutParams) |
|---|---|---|
| EXACTLY | 精确大小 | match_parent 或 具体数值(如100dp)。父容器已经确定了子View的精确大小。 |
| AT_MOST | 最大值限制 | wrap_content。父容器给出一个最大限制(通常是父容器剩余空间),子View不能超过这个尺寸,具体多大由子View自身内容决定。 |
| UNSPECIFIED | 未限制 | 通常出现在ScrollView或系统内部。父容器对子View没有限制,子View想要多大就给多大。 |
onMeasure的核心实现
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1. 解析父容器传入的约束
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 2. 计算最终尺寸 (根据自身内容逻辑)
int finalWidth;
int finalHeight;
// 计算宽度
if (widthMode == MeasureSpec.EXACTLY) {
// 如果父容器给了精确值,直接使用
finalWidth = widthSize;
} else {
// 计算内容需要的宽度(例如根据文字、图片计算)
int contentWidth = getContentWidth();
if (widthMode == MeasureSpec.AT_MOST) {
// wrap_content: 取 min(内容宽度, 父容器限制)
finalWidth = Math.min(contentWidth, widthSize);
} else { // UNSPECIFIED
finalWidth = contentWidth;
}
}
// 同理处理高度...
// 3. 保存测量结果
setMeasuredDimension(finalWidth, finalHeight);
}
注意事项
千万不要在 `onMeasure` 中直接使用 `getWidth()` / `getHeight()`。
此时View的尺寸尚未确定,这两个方法返回的是上一次layout结束后的值,通常为0。只有在 `onLayout` 结束之后,尺寸才是正确的。
第二阶段layout
Layout阶段决定了View具体放在哪里。对于ViewGroup来说,它需要遍历所有子View,调用子View的 layout 方法,并计算子View的具体坐标。
layout与onLayout
layout(l, t, r, b): 这是一个非私有方法(且是final的),由父View调用。它负责设置View自身的四个顶点,并内部调用onLayout。onLayout(boolean changed, int l, int t, int r, int b): 这是一个空方法,供ViewGroup重写。用于确定子View的位置。
自定义ViewGroup的Layout实现(FlowLayout)
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int currentX = getPaddingLeft();
int currentY = getPaddingTop();
int lineMaxHeight = 0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
// 获取子View测量后的宽高
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 判断是否需要换行
if (currentX + childWidth > r - l - getPaddingRight()) {
// 换行
currentX = getPaddingLeft();
currentY += lineMaxHeight;
lineMaxHeight = 0;
}
// 关键:调用子View的layout方法,设置其坐标
child.layout(currentX, currentY,
currentX + childWidth,
currentY + childHeight);
// 更新坐标
currentX += childWidth;
lineMaxHeight = Math.max(lineMaxHeight, childHeight);
}
}
}
第三个阶段绘制(Draw)
绘制阶段负责将View的内容显示在屏幕上。这个过程也是递归的,父View会调用子View的 draw 方法。
draw的执行步骤:View的draw方法内部逻辑非常清晰,按照以下顺序执行(源码分析):
- 绘制背景:
drawBackground(canvas) - 保存画布图层: 为动画或渐变做准备(非必须)
- 绘制自身内容:
onDraw(canvas)-> 子类重写 - 绘制子View:
dispatchDraw(canvas)-> ViewGroup重写 - 绘制滚动条/前景:
onDrawForeground(canvas)
性能优化建议
1、避免在 `onDraw` 中创建对象:`onDraw` 会被频繁调用,在其中`new`对象会导致内存抖动,引发GC,造成掉帧。
2、尽量减少 `onDraw` 的复杂度:复杂的计算逻辑应当放在初始化或测量阶段。
3、使用 `clipRect`:在自定义ViewGroup中,如果某个子View被遮挡,
可以在 `dispatchDraw` 前使用 `canvas.clipRect` 来避免绘制不可见的部分,提高绘制效率。
触发重绘的方式
-
requestLayout():- 作用:触发
measure和layout。 - 场景:View的尺寸发生变化(例如数据更新导致高度变化)。
- 注意:它不会强制触发
draw。如果只是尺寸变化,内容不变,系统会跳过draw阶段,或者仅重绘边界变化的区域。
- 作用:触发
-
invalidate():- 作用:触发
draw。 - 场景:View的外观发生变化(颜色、文字内容改变),但宽高不变。
- 注意:它不会触发
measure或layout,效率较高。
- 作用:触发
-
postInvalidate():- 作用:在非UI线程中请求重绘(最终依然会切回UI线程)。
ps:性能陷阱
`requestLayout` 的代价是非常高的,因为它会从当前View向上回溯到`ViewRootImpl`,
然后再向下递归执行完整的`measure`和`layout`流程。
优化原则 :如果只是改变setVisibility或者简单的位移(没有改变尺寸),尽量使用 invalidate 或 setTranslationX/Y 等属性动画,避免触发 requestLayout。
三、事件分发结构模型
-
触摸事件由Action_Down、Action_Move、Aciton_UP组成,其中一次完整的触摸事件中,Down和Up都只有一个,Move有若干个,可以为0个。
-
参与Touch事件分发的,有Activity、ViewGroup(继承于view)和View。
- ViewGroup包含:
onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent - View和Activity包含:
dispatchTouchEvent、onTouchEvent
- ViewGroup包含:
-
ViewGroup和View组成了一个树状结构,根节点为Activity内部包含的一个ViewGroup。
-
ViewGroup的
dispatchTouchEvent是真正在执行“分发”工作,而View的dispatchTouchEvent方法,并不执行分发工作,而是决定是否把touch事件交给自己处理(onTouchEvent)。 -
onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent这三个方法都是返回true表示消费了事件,返回false则表示不感兴趣,不处理。
后续事件
后续事件是指Action_Move和Action_Up,所有后续事件都会直接传给消费事件的View,不再经历中间的传播过程。
上图的事件分发顺序为①-②-⑤-⑥-⑦-③-④