UI 绘制 / 事件分发 / RecyclerView — 知识详解
更新: 5/15/2026 字数: 0 字 时长: 0 分钟
1. View 绘制流程
1.1 整体流程
绘制从 ViewRootImpl.performTraversals() 开始,依次执行三大流程:
performTraversals()
├── performMeasure() → View.measure() → onMeasure()
├── performLayout() → View.layout() → onLayout()
└── performDraw() → View.draw() → onDraw()触发时机:requestLayout()(触发 measure + layout)、invalidate()(触发 draw)。
1.2 MeasureSpec
MeasureSpec 是一个 32 位 int 值,高 2 位是模式,低 30 位是大小:
// 三种模式
EXACTLY // 精确值:match_parent 或具体 dp 值
AT_MOST // 最大值:wrap_content,不能超过父容器剩余空间
UNSPECIFIED // 无限制:ScrollView 中的子 View父 SpecMode + 子 LayoutParams → 子 MeasureSpec 决策表:
| 父 Mode | 子 LP = 具体值 | 子 LP = match_parent | 子 LP = wrap_content |
|---|---|---|---|
| EXACTLY | EXACTLY + 子值 | EXACTLY + 父大小 | AT_MOST + 父大小 |
| AT_MOST | EXACTLY + 子值 | AT_MOST + 父大小 | AT_MOST + 父大小 |
| UNSPECIFIED | EXACTLY + 子值 | UNSPECIFIED + 0 | UNSPECIFIED + 0 |
源码在 ViewGroup.getChildMeasureSpec():
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) { // 具体值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// ... AT_MOST 和 UNSPECIFIED 类似
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}1.3 自定义 View 的 onMeasure
wrap_content 不处理时为什么等同于 match_parent?
从决策表可以看到,当父是 EXACTLY 时,wrap_content 得到的是 AT_MOST + 父大小。如果自定义 View 的 onMeasure 不处理 AT_MOST 模式,直接用 specSize,那就等于父容器大小,和 match_parent 效果一样。
正确处理:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int width;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize; // 精确值,直接用
} else {
int desiredWidth = calculateContentWidth() + getPaddingLeft() + getPaddingRight();
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(desiredWidth, widthSize); // wrap_content,取较小值
} else {
width = desiredWidth; // UNSPECIFIED,用期望值
}
}
setMeasuredDimension(width, height);
}1.4 onLayout
ViewGroup 必须重写 onLayout,确定每个子 View 的位置:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childTop = getPaddingTop();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
child.layout(getPaddingLeft(), childTop,
getPaddingLeft() + childWidth, childTop + childHeight);
childTop += childHeight;
}
}
}1.5 onDraw
绘制顺序:
drawBackground():绘制背景onDraw():绘制自身内容dispatchDraw():绘制子 ViewonDrawForeground():绘制前景(滚动条等)
@Override
protected void onDraw(Canvas canvas) {
// 使用 Canvas API 绘制
canvas.drawCircle(centerX, centerY, radius, paint);
canvas.drawText(text, x, y, textPaint);
canvas.drawBitmap(bitmap, srcRect, dstRect, paint);
}1.6 requestLayout vs invalidate
requestLayout():标记 PFLAG_FORCE_LAYOUT,向上传递到 ViewRootImpl,强制触发 measure + layout。如果布局发生变化,View.layout()内部会调用invalidate()标记脏区域,连带触发 drawinvalidate():标记脏区域,只触发 drawpostInvalidateOnAnimation():在下一帧绘制时 invalidate,避免同一帧多次绘制
2. 事件分发机制
2.1 三个核心方法
// 分发事件
public boolean dispatchTouchEvent(MotionEvent ev)
// 拦截事件(只有 ViewGroup 有)
public boolean onInterceptTouchEvent(MotionEvent ev)
// 消费事件
public boolean onTouchEvent(MotionEvent ev)2.2 分发流程
Activity.dispatchTouchEvent
→ PhoneWindow.superDispatchTouchEvent
→ DecorView.dispatchTouchEvent
→ ViewGroup.dispatchTouchEvent
→ ViewGroup.onInterceptTouchEvent // 是否拦截?
│
├── 不拦截 → 遍历子 View
│ → child.dispatchTouchEvent
│ → child.onTouchEvent // 子 View 消费?
│ ├── true → 事件被消费,结束
│ └── false → 回传给父 ViewGroup.onTouchEvent
│
└── 拦截 → ViewGroup.onTouchEvent // 自己处理2.3 关键规则
DOWN 事件决定后续事件的接收者:如果某个 View 在 DOWN 时返回 true(消费),后续的 MOVE/UP 都会直接发给它
一旦拦截,后续事件不再询问 onInterceptTouchEvent:ViewGroup 拦截后,后续事件直接交给自己的 onTouchEvent
子 View 可以请求父 ViewGroup 不拦截:
parent.requestDisallowInterceptTouchEvent(true);
// 设置 FLAG_DISALLOW_INTERCEPT,父 ViewGroup 的 onInterceptTouchEvent 不会被调用
// 但 DOWN 事件会重置这个标志- onTouchListener 优先于 onTouchEvent:
// dispatchTouchEvent 中的逻辑
if (mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) {
return true; // OnTouchListener 消费了,不调用 onTouchEvent
}
return onTouchEvent(event); // 否则调用 onTouchEvent- onClick 在 onTouchEvent 的 ACTION_UP 中触发
2.4 源码关键逻辑(ViewGroup.dispatchTouchEvent)
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
// DOWN 事件重置状态
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState(); // 清除 FLAG_DISALLOW_INTERCEPT
}
// 判断是否拦截
final boolean intercepted;
if (actionMasked == ACTION_DOWN || mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = false;
}
} else {
// 没有子 View 消费 DOWN,后续事件直接拦截
intercepted = true;
}
// 不拦截则遍历子 View
if (!intercepted) {
// 按 Z 序从上到下遍历子 View
for (int i = childrenCount - 1; i >= 0; i--) {
// 判断触摸点是否在子 View 范围内
if (dispatchTransformedTouchEvent(ev, child)) {
mFirstTouchTarget = addTouchTarget(child);
break;
}
}
}
// 分发给 target 或自己处理
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, null); // 自己的 onTouchEvent
} else {
// 分发给 mFirstTouchTarget 链表中的子 View
}
return handled;
}2.5 滑动冲突解决
外部拦截法(推荐):在父 ViewGroup 的 onInterceptTouchEvent 中判断:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
return false; // DOWN 不拦截,否则子 View 收不到事件
case MotionEvent.ACTION_MOVE:
if (needIntercept(ev)) return true; // 根据滑动方向判断
break;
case MotionEvent.ACTION_UP:
return false;
}
return false;
}内部拦截法:子 View 通过 requestDisallowInterceptTouchEvent 控制:
// 子 View
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (parentNeedEvent(ev)) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
return super.dispatchTouchEvent(ev);
}3. RecyclerView
RecyclerView 内容较多,已拆分为独立文件:
- recyclerview-cache.md — 缓存机制(四级缓存详解、查找流程源码、回收流程、Scrap 工作时机)
- recyclerview-layout-scroll.md — 布局与滑动(LayoutManager 原理、滑动机制、SnapHelper、ItemDecoration、ItemAnimator)
- recyclerview-diffutil-optimize.md — DiffUtil 与性能优化(Myers 算法、Payload 局部更新、Prefetch 预取、嵌套优化、ConcatAdapter、ItemTouchHelper)
4. Window / WindowManager / DecorView
4.1 层级关系
Activity
└── PhoneWindow(Window 的唯一实现)
└── DecorView(FrameLayout 子类,Window 的根 View)
├── TitleBar / ActionBar
└── ContentView(android.R.id.content)
└── 你的布局(setContentView 设置的)setContentView 实际流程:
- Activity.setContentView → PhoneWindow.setContentView
- PhoneWindow 创建 DecorView(如果还没有)
- 将你的布局 inflate 到 DecorView 的 ContentView 中
4.2 WindowManager
WindowManager 管理 Window 的添加、更新和删除:
// 添加悬浮窗
val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, // Android 8.0+ 需要此类型
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
)
windowManager.addView(floatingView, params)Window 类型(z-order 从低到高):
- 应用窗口(1-99):Activity
- 子窗口(1000-1999):Dialog、PopupWindow
- 系统窗口(2000-2999):Toast、状态栏、导航栏、悬浮窗
4.3 ViewRootImpl
ViewRootImpl 是 View 树的管理者:
- 连接 WindowManager 和 DecorView
- 触发 View 的三大流程(measure/layout/draw)
- 接收输入事件并分发
- 通过 Choreographer 与 VSYNC 同步
5. SurfaceView vs TextureView
| 特性 | SurfaceView | TextureView |
|---|---|---|
| 渲染线程 | 独立 Surface,可在子线程绑制 | 共享 Window 的 Surface |
| 动画/变换 | 不支持(独立 Surface 不在 View 层级中) | 支持(是普通 View) |
| 内存 | 较少 | 较多(额外的 SurfaceTexture) |
| 性能 | 更好(独立渲染) | 略差 |
| 适用场景 | 视频播放、相机预览、游戏 | 需要动画/变换的视频场景 |
SurfaceView 的"挖洞"原理:SurfaceView 在 Window 上"挖"一个透明区域,其独立 Surface 在 Window 下方显示。所以 SurfaceView 不能做半透明效果。
6. 属性动画原理
6.1 核心类
// ValueAnimator:值动画,只产生值的变化
ValueAnimator.ofFloat(0f, 1f).apply {
duration = 300
addUpdateListener { animator ->
view.alpha = animator.animatedValue as Float
}
start()
}
// ObjectAnimator:属性动画,直接修改对象属性
ObjectAnimator.ofFloat(view, "translationX", 0f, 100f).apply {
duration = 300
start()
}6.2 工作原理
start()注册到 Choreographer 的 VSYNC 回调- 每帧 VSYNC 到来时,根据已过时间和 Interpolator 计算进度
- 通过 TypeEvaluator 计算当前值
- ObjectAnimator 通过反射调用
setXxx()方法更新属性 - 属性变化触发
invalidate()重绘
6.3 Interpolator 与 TypeEvaluator
- Interpolator:控制动画速度曲线(线性、加速、减速、弹性等)
- TypeEvaluator:计算属性值(IntEvaluator、FloatEvaluator、ArgbEvaluator)
// 自定义 Interpolator
class BounceInterpolator : TimeInterpolator {
override fun getInterpolation(input: Float): Float {
// input: 0.0 → 1.0(时间进度)
// return: 属性进度(可以超过 1.0 实现弹性效果)
}
}6.4 View 动画 vs 属性动画
- View 动画(补间动画):只改变绘制位置,不改变实际属性。点击事件仍在原位置
- 属性动画:真正改变对象属性。点击事件跟随移动
7. ConstraintLayout 性能优势
ConstraintLayout 只需要一次 measure + layout 就能完成复杂布局,而嵌套的 LinearLayout/RelativeLayout 需要多次。
原理:ConstraintLayout 使用 Cassowary 线性约束求解算法,将所有约束转化为线性方程组一次性求解,避免了多层嵌套导致的指数级 measure 调用。
嵌套 LinearLayout(3层):measure 调用 2^3 = 8 次
ConstraintLayout(扁平):measure 调用 2 次(水平+垂直各一次)