Skip to content

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 位是大小:

java
// 三种模式
EXACTLY    // 精确值:match_parent 或具体 dp 值
AT_MOST    // 最大值:wrap_content,不能超过父容器剩余空间
UNSPECIFIED // 无限制:ScrollView 中的子 View

父 SpecMode + 子 LayoutParams → 子 MeasureSpec 决策表

父 Mode子 LP = 具体值子 LP = match_parent子 LP = wrap_content
EXACTLYEXACTLY + 子值EXACTLY + 父大小AT_MOST + 父大小
AT_MOSTEXACTLY + 子值AT_MOST + 父大小AT_MOST + 父大小
UNSPECIFIEDEXACTLY + 子值UNSPECIFIED + 0UNSPECIFIED + 0

源码在 ViewGroup.getChildMeasureSpec()

java
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 效果一样。

正确处理:

java
@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 的位置:

java
@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

绘制顺序:

  1. drawBackground():绘制背景
  2. onDraw():绘制自身内容
  3. dispatchDraw():绘制子 View
  4. onDrawForeground():绘制前景(滚动条等)
java
@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() 标记脏区域,连带触发 draw
  • invalidate():标记脏区域,只触发 draw
  • postInvalidateOnAnimation():在下一帧绘制时 invalidate,避免同一帧多次绘制

2. 事件分发机制

2.1 三个核心方法

java
// 分发事件
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 关键规则

  1. DOWN 事件决定后续事件的接收者:如果某个 View 在 DOWN 时返回 true(消费),后续的 MOVE/UP 都会直接发给它

  2. 一旦拦截,后续事件不再询问 onInterceptTouchEvent:ViewGroup 拦截后,后续事件直接交给自己的 onTouchEvent

  3. 子 View 可以请求父 ViewGroup 不拦截

java
parent.requestDisallowInterceptTouchEvent(true);
// 设置 FLAG_DISALLOW_INTERCEPT,父 ViewGroup 的 onInterceptTouchEvent 不会被调用
// 但 DOWN 事件会重置这个标志
  1. onTouchListener 优先于 onTouchEvent
java
// dispatchTouchEvent 中的逻辑
if (mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) {
    return true; // OnTouchListener 消费了,不调用 onTouchEvent
}
return onTouchEvent(event); // 否则调用 onTouchEvent
  1. onClick 在 onTouchEvent 的 ACTION_UP 中触发

2.4 源码关键逻辑(ViewGroup.dispatchTouchEvent)

java
@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 中判断:

java
@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 控制:

java
// 子 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 实际流程:

  1. Activity.setContentView → PhoneWindow.setContentView
  2. PhoneWindow 创建 DecorView(如果还没有)
  3. 将你的布局 inflate 到 DecorView 的 ContentView 中

4.2 WindowManager

WindowManager 管理 Window 的添加、更新和删除:

kotlin
// 添加悬浮窗
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

特性SurfaceViewTextureView
渲染线程独立 Surface,可在子线程绑制共享 Window 的 Surface
动画/变换不支持(独立 Surface 不在 View 层级中)支持(是普通 View)
内存较少较多(额外的 SurfaceTexture)
性能更好(独立渲染)略差
适用场景视频播放、相机预览、游戏需要动画/变换的视频场景

SurfaceView 的"挖洞"原理:SurfaceView 在 Window 上"挖"一个透明区域,其独立 Surface 在 Window 下方显示。所以 SurfaceView 不能做半透明效果。


6. 属性动画原理

6.1 核心类

kotlin
// 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 工作原理

  1. start() 注册到 Choreographer 的 VSYNC 回调
  2. 每帧 VSYNC 到来时,根据已过时间和 Interpolator 计算进度
  3. 通过 TypeEvaluator 计算当前值
  4. ObjectAnimator 通过反射调用 setXxx() 方法更新属性
  5. 属性变化触发 invalidate() 重绘

6.3 Interpolator 与 TypeEvaluator

  • Interpolator:控制动画速度曲线(线性、加速、减速、弹性等)
  • TypeEvaluator:计算属性值(IntEvaluator、FloatEvaluator、ArgbEvaluator)
kotlin
// 自定义 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 次(水平+垂直各一次)