Android View 绘制与事件分发

① View 的绘制流程从哪里开始?整体流程是什么?

**答案:View 的绘制从 ViewRootImpl 的 performTraversals() 方法开始,依次执行 performMeasure → performLayout → performDraw 三个流程,对应 measure → layout → draw 三步。整个流程的触发链路是:VSYNC 信号 → Choreographer → ViewRootImpl.doTraversal() → performTraversals()。 **

完整链路

scss 复制代码
VSYNC 信号到达

    ↓

Choreographer.doFrame()

    ↓

ViewRootImpl.doTraversal()

    ↓

performTraversals()

    ├── performMeasure()  →  measure()  →  onMeasure()   测量大小

    ├── performLayout()   →  layout()   →  onLayout()    确定位置

    └── performDraw()     →  draw()     →  onDraw()      绘制内容

performTraversals 核心逻辑

java 复制代码
// ViewRootImpl.performTraversals() 简化

private void performTraversals() {

    // ... 根据 dirty 标记决定是否执行各流程 ...

  


    if (mMeasureIsNeeded) {

        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

    }

  


    if (mLayoutRequested) {

        performLayout(lp, mWidth, mHeight);

    }

  


    if (!mStopped && !mReportNextDraw) {

        performDraw();

    }

}

重要细节

scss 复制代码
performTraversals 不是每次都三步全走:

  - 只有尺寸变化时才 measure

  - 只有位置变化时才 layout

  - 只有内容变化时才 draw

  


判断依据:

  - requestLayout()  → 标记需要 measure + layout

  - invalidate()     → 标记需要 draw

从 setContentView 到绘制的完整流程

scss 复制代码
Activity.setContentView()

  → PhoneWindow.setContentView()

    → Window.installDecor()

      → 创建 DecorView

  


Activity.onResume() 之后

  → WindowManager.addView(decorView, lp)

    → WindowManagerGlobal.addView()

      → new ViewRootImpl()

        → ViewRootImpl.setView(decorView)

          → requestLayout()

            → scheduleTraversals()

              → Choreographer.postSyncBarrier + postCallback

                → VSYNC 到来时 doFrame()

                  → performTraversals()

② MeasureSpec 是什么?measure 过程怎么理解?

**答案:MeasureSpec 是一个 32 位 int 值,高 2 位是模式(SpecMode),低 30 位是大小(SpecSize),用于父 View 向子 View 传递测量约束。三种模式:EXACTLY(精确值/match_parent)、AT_MOST(wrap_content)、UNSPECIFIED(无限制,系统内部用)。子 View 的 MeasureSpec 由父 View 的 MeasureSpec 和子 View 的 LayoutParams 共同决定。 **

MeasureSpec 结构

sql 复制代码
32 位 int 值:

┌──────────┬─────────────────────────────────┐

│  高 2 位  │          低 30 位                │

│ SpecMode │         SpecSize                 │

└──────────┴─────────────────────────────────┘

  


三种模式:

  EXACTLY  (00) → 父 View 给了精确大小,子 View 就是这个值

  AT_MOST  (01) → 子 View 不超过 SpecSize,具体多大看自己

  UNSPECIFIED (10) → 子 View 想多大就多大,不受约束

MeasureSpec 的生成规则

java 复制代码
// ViewRootImpl 中根 View 的 MeasureSpec

// 窗口大小确定后,根 View 的模式总是 EXACTLY,大小就是窗口大小

  


// 子 View 的 MeasureSpec 由 getChildMeasureSpec() 决定:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

    int specMode = MeasureSpec.getMode(spec);

    int specSize = MeasureSpec.getSize(spec);

  


    // 根据 父specMode + 子LayoutParams 生成 子spec

    switch (specMode) {

        case EXACTLY:

            if (childDimension >= 0)        → EXACTLY  (childDimension)

            else if (childDimension == LP_MATCH_PARENT → EXACTLY  (父大小 - padding)

            else if (childDimension == LP_WRAP_CONTENT → AT_MOST  (父大小 - padding)

            break;

        case AT_MOST:

            if (childDimension >= 0)        → EXACTLY  (childDimension)

            else if (childDimension == LP_MATCH_PARENT → AT_MOST  (父大小 - padding)

            else if (childDimension == LP_WRAP_CONTENT → AT_MOST  (父大小 - padding)

            break;

        case UNSPECIFIED:

            if (childDimension >= 0)        → EXACTLY  (childDimension)

            else if (childDimension == LP_MATCH_PARENT → UNSPECIFIED (0)

            else if (childDimension == LP_WRAP_CONTENT → UNSPECIFIED (0)

            break;

    }

}

完整对照表

| 父 View 模式 \ 子 LayoutParams | 精确值 (100dp) | match_parent | wrap_content |

|---|---|---|---|

| EXACTLY | EXACTLY(100dp) | EXACTLY(父大小) | AT_MOST(父大小) |

| AT_MOST | EXACTLY(100dp) | AT_MOST(父大小) | AT_MOST(父大小) |

| UNSPECIFIED | EXACTLY(100dp) | UNSPECIFIED(0) | UNSPECIFIED(0) |

measure 流程

java 复制代码
// View.measure() 是 final,不可重写

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

    // ... 缓存判断 ...

    onMeasure(widthMeasureSpec, heightMeasureSpec);  // 调用 onMeasure

}

  


// View 的默认 onMeasure

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    setMeasuredDimension(

        getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),

        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)

    );

}

  


// getDefaultSize:AT_MOST 和 EXACTLY 都是 SpecSize(这就是 wrap_content 不生效的原因!)

public static int getDefaultSize(int size, int measureSpec) {

    int result = size;

    int specMode = MeasureSpec.getMode(measureSpec);

    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {

        case UNSPECIFIED: result = size; break;

        case EXACTLY:

        case AT_MOST:     result = specSize; break;  // ← AT_MOST 也取 specSize

    }

    return result;

}

ViewGroup 的 measure

java 复制代码
// ViewGroup 是抽象类,没有重写 onMeasure

// 因为不同布局的测量方式不同(LinearLayout vs FrameLayout 等)

// 每个 ViewGroup 子类自己实现 onMeasure

  


// ViewGroup 提供 measureChildren() 辅助方法

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {

    for (int i = 0; i < count; i++) {

        View child = getChildAt(i);

        if (child.getVisibility() != GONE) {

            measureChild(child, widthMeasureSpec, heightMeasureSpec);

        }

    }

}

  


protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {

    LayoutParams lp = child.getLayoutParams();

    int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, padding, lp.width);

    int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, padding, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

}

③ Layout 过程做了什么?与 measure 的区别?

**答案:Layout 过程根据 measure 阶段测量的宽高,确定每个子 View 在父容器中的四个位置(left、top、right、bottom)。与 measure 的区别是:measure 测量"多大",layout 确定"放哪"。layout 过程中父 View 调用子 View 的 layout() 方法传入具体坐标,子 View 在 onLayout() 中对自己的子 View 做同样的事。 **

Layout 流程

java 复制代码
// View.layout() --- 确定自身位置

public void layout(int l, int t, int r, int b) {

    // 1. 保存旧位置

    int oldL = mLeft, oldT = mTop, oldR = mRight, oldB = mBottom;

  


    // 2. setFrame() 设置四个顶点(这是真正赋值的地方)

    boolean changed = isLayoutModeOptical(mParent) ?

            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

  


    // 3. 如果位置变化或需要重新 layout

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {

        onLayout(changed, l, t, r, b);  // 让子类摆放子 View

    }

}

ViewGroup.onLayout()

java 复制代码
// ViewGroup.onLayout() 是抽象方法,子类必须实现

// 因为每种布局摆放子 View 的方式不同

  


// LinearLayout.onLayout() --- 竖直/水平排列

// FrameLayout.onLayout() --- 左上角叠加

// RelativeLayout.onLayout() --- 根据规则定位

measure vs layout

| | measure | layout |

|---|---|---|

| 目的 | 测量 View 的大小 | 确定 View 的位置 |

| 核心方法 | onMeasure() | onLayout() |

| 输入 | MeasureSpec(约束) | 四个坐标 (l, t, r, b) |

| 输出 | getMeasuredWidth/Height() | getWidth/Height() |

| 存储 | mMeasuredWidth/Height | mLeft/mTop/mRight/mBottom |

| 是否必须重写 | View 需要自定义大小时 | ViewGroup 必须实现 |

getMeasuredWidth vs getWidth

java 复制代码
// measure 之后就有值

view.getMeasuredWidth();   // onMeasure 中 setMeasuredDimension 设置的

  


// layout 之后才有值

view.getWidth();           // layout 之后 = mRight - mLeft

  


// 绝大多数情况两者相等,但在 layout 中可以修改位置使它们不同

④ Draw 过程的顺序?为什么是这个顺序?

**答案:Draw 过程的顺序是:绘制背景 → 绘制自身内容(onDraw) → 绘制子 View(dispatchDraw) → 绘制前景(装饰、滚动条等)。这个顺序的原因是:背景在最底层,内容在背景之上,子 View 在内容之上,装饰在最上层 ------ 本质是画家算法(从远到近),后绘制的覆盖先绘制的。 **

Draw 源码

java 复制代码
// View.draw() 简化

public void draw(Canvas canvas) {

    // 1. 绘制背景

    drawBackground(canvas);

  


    // 2. 绘制自身内容(可重写)

    if (!dirtyOpaque) {

        onDraw(canvas);

    }

  


    // 3. 绘制子 View

    dispatchDraw(canvas);

  


    // 4. 绘制前景装饰(滚动条、前景等)

    onDrawForeground(canvas);

}

绘制层次图

sql 复制代码
从下到上的绘制顺序(Z 轴):

  


  ┌─────────────────────────┐

  │  4. onDrawForeground    │  ← 前景(滚动条等)

  ├─────────────────────────┤

  │  3. dispatchDraw        │  ← 子 View

  ├─────────────────────────┤

  │  2. onDraw              │  ← 自身内容

  ├─────────────────────────┤

  │  1. drawBackground      │  ← 背景

  └─────────────────────────┘

特殊情况

java 复制代码
// 如果设置了 WillNotDraw 标记,系统会跳过 draw 流程

// ViewGroup 默认 setWillNotDraw(true),因为大多数 ViewGroup 不需要绘制自身内容

// 只有自定义绘制内容的 ViewGroup 才需要 setWillNotDraw(false)

  


// 带硬件加速的绘制流程略有不同

// 但最终都是按上述顺序提交绘制命令

onDraw 注意事项

java 复制代码
// ❌ 不要在 onDraw 中创建对象

@Override

protected void onDraw(Canvas canvas) {

    Paint paint = new Paint();  // 每次绘制都创建,导致 GC

    canvas.drawText("hello", 0, 0, paint);

}

  


// ✅ 对象应该在构造器中创建

private Paint mPaint;

public MyView(Context context) {

    mPaint = new Paint();

}

  


@Override

protected void onDraw(Canvas canvas) {

    canvas.drawText("hello", 0, 0, mPaint);  // 复用 Paint

}

⑤ requestLayout / invalidate / postInvalidate 的区别?

**答案:requestLayout 会触发重新 measure + layout + draw(从 performTraversals 开始);invalidate 只触发当前 View 的重新 draw(不会 measure 和 layout),且只能在 UI 线程调用;postInvalidate 和 invalidate 作用一样,但可以在非 UI 线程调用,内部通过 Handler 切到 UI 线程再调 invalidate。 **

对比表

| 方法 | measure | layout | draw | 线程 |

|------|---------|--------|------|------|

| requestLayout() | ✅ | ✅ | ✅ | UI 线程 |

| invalidate() | ❌ | ❌ | ✅ | UI 线程 |

| postInvalidate() | ❌ | ❌ | ✅ | 任意线程 |

源码分析

java 复制代码
// ========== requestLayout ==========

public void requestLayout() {

    if (mParent != null && !mParent.isLayoutRequested()) {

        mParent.requestLayout();  // 向上传递到 ViewRootImpl

    }

}

  


// ViewRootImpl.requestLayout()

public void requestLayout() {

    if (!mHandlingLayoutInLayoutRequest) {

        checkThread();  // 检查是否在 UI 线程

        mLayoutRequested = true;  // 标记需要 layout

        scheduleTraversals();     // 安排 performTraversals

    }

}

  


// performTraversals 内部:

// mLayoutRequested = true → 执行 measure + layout + draw

  


// ========== invalidate ==========

public void invalidate() {

    invalidate(true);

}

  


public void invalidate(boolean invalidateCache) {

    // ... 向上传递到 ViewRootImpl

    // 最终调 ViewRootImpl.scheduleTraversals()

    // 但不设置 mLayoutRequested,只标记 dirty 区域

}

  


// performTraversals 内部:

// mLayoutRequested = false → 跳过 measure 和 layout

// dirty 区域非空 → 只执行 draw

  


// ========== postInvalidate ==========

public void postInvalidate() {

    // 通过 ViewRootImpl 的 Handler 发消息到 UI 线程

    // 最终调用 invalidate()

    mAttachInfo.mHandler.post(() -> invalidate());

}

使用场景

java 复制代码
// 改变 View 大小或位置 → requestLayout

view.setLayoutParams(new LayoutParams(500, 500));

  


// 只改变外观(颜色、文字等)→ invalidate

view.setTextColor(Color.RED);

  


// 子线程改变外观 → postInvalidate

new Thread(() -> {

    // ... 耗时操作 ...

    view.postInvalidate();  // 安全

    // view.invalidate();   // ❌ 非 UI 线程崩溃

}).start();

⑥ 事件分发流程是什么?三个核心方法的关系?

**答案:事件分发由三个核心方法协作完成:dispatchTouchEvent(分发)、onInterceptTouchEvent(拦截,ViewGroup 独有)、onTouchEvent(消费)。核心规则是:事件从 Activity → Window → 顶级 ViewGroup,逐层 dispatchTouchEvent 向下传递;ViewGroup 可通过 onInterceptTouchEvent 拦截事件不再向下传递;如果所有层都不消费(onTouchEvent 返回 false),事件会沿原路回传(逆序调用 onTouchEvent)------这就是经典的"U 型"分发模型。 **

三个方法职责

| 方法 | 位置 | 作用 | 返回值含义 |

|------|------|------|-----------|

| dispatchTouchEvent | View/ViewGroup | 分发事件 | true=事件已处理;false=不处理,回传给父 View |

| onInterceptTouchEvent | ViewGroup | 拦截事件 | true=拦截,自己处理;false=不拦截,继续传给子 View |

| onTouchEvent | View/ViewGroup | 消费事件 | true=消费事件;false=不消费,回传给父 View |

U 型分发模型

scss 复制代码
ACTION_DOWN 事件分发流程:

  


Activity.dispatchTouchEvent()

    ↓

PhoneWindow.superDispatchTouchEvent()

    ↓

DecorView.dispatchTouchEvent()

    ↓

ViewGroupA.dispatchTouchEvent()

    ├── ViewGroupA.onInterceptTouchEvent() = false → 不拦截

    ↓

ViewGroupB.dispatchTouchEvent()

    ├── ViewGroupB.onInterceptTouchEvent() = false → 不拦截

    ↓

View.dispatchTouchEvent()

    ├── View.onTouch() = false → 不消费

    ├── View.onTouchEvent() = false → 不消费

    ↓ (回传)

ViewGroupB.onTouchEvent() = false → 不消费

    ↓ (回传)

ViewGroupA.onTouchEvent() = false → 不消费

    ↓ (回传)

Activity.onTouchEvent() → 最终也不消费,事件丢失

ViewGroup.dispatchTouchEvent 核心源码

java 复制代码
public boolean dispatchTouchEvent(MotionEvent ev) {

    boolean handled = false;

  


    if (onInterceptTouchEvent(ev)) {

        // 拦截了,自己处理

        handled = onTouchEvent(ev);

    } else {

        // 不拦截,分发给子 View

        for (int i = childrenCount - 1; i >= 0; i--) {

            if (isTransformedTouchPointInView(x, y, child)) {

                handled = child.dispatchTouchEvent(ev);

                if (handled) break;  // 子 View 消费了,不再传递

            }

        }

        if (!handled) {

            // 没有子 View 消费,自己处理

            handled = onTouchEvent(ev);

        }

    }

    return handled;

}

核心口诀

复制代码
分发靠 dispatch,拦截靠 intercept,消费靠 onTouchEvent

  


拦截只问一次(DOWN 时决定),消费层层上溯

DOWN 确定目标,后续事件直接送达

⑦ onTouch 和 onTouchEvent 的关系?onClickListener 在哪触发?

**答案:在 View.dispatchTouchEvent 中,优先级依次是:onTouch > onTouchEvent > onClickListener。如果设置了 OnTouchListener 且 onTouch 返回 true,onTouchEvent 不会被调用;onClickListener 在 onTouchEvent 的 ACTION_UP 中触发,所以如果 onTouch 返回 true 拦截了事件,onClick 也不会触发。 **

dispatchTouchEvent 中的优先级

java 复制代码
// View.dispatchTouchEvent() 简化

public boolean dispatchTouchEvent(MotionEvent event) {

    // 1. 先检查 OnTouchListener

    if (mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) {

        return true;  // onTouch 返回 true,事件到此为止

    }

  


    // 2. onTouch 没消费,走 onTouchEvent

    if (onTouchEvent(event)) {

        return true;

    }

  


    // 3. 都不消费,返回 false

    return false;

}

onTouchEvent 中的 onClick

java 复制代码
// View.onTouchEvent() 简化

public boolean onTouchEvent(MotionEvent event) {

    switch (event.getAction()) {

        case ACTION_DOWN:

            mHasPerformedLongPress = false;

            setPressed(true);

            // 检查长按

            checkForLongClick();

            break;

  


        case ACTION_MOVE:

            // 判断是否移出 View 边界

            break;

  


        case ACTION_UP:

            if (!mHasPerformedLongPress && isPressed()) {

                // ★ 没触发长按 + 仍然按下状态 → 触发 onClick

                performClick();  // 内部调用 mOnClickListener.onClick(this)

            }

            setPressed(false);

            break;

    }

    return true;  // 只要 View 是 clickable,onTouchEvent 默认返回 true

}

完整优先级链

sql 复制代码
View.dispatchTouchEvent 中的优先级:

  


  OnTouchListener.onTouch()

       │

       ├─ 返回 true → 事件消费完毕,后续都不执行

       │

       └─ 返回 false ↓

  


  View.onTouchEvent()

       │

       ├─ ACTION_DOWN → 记录状态

       ├─ ACTION_MOVE → 检查是否移出边界

       ├─ ACTION_UP →

       │     ├─ 没有长按 → performClick() → OnClickListener.onClick()

       │     └─ 长按了 → 不触发 onClick

       │

       └─ View clickable=true → 默认返回 true(消费事件)

  


总结:

  onTouch > onTouchEvent > onClick

  onTouch 返回 true → onClick 不触发

  onTouch 返回 false → onClick 才有机会触发

常见陷阱

java 复制代码
// ❌ 这样写 onClick 不触发

view.setOnTouchListener((v, event) -> true);  // 吞掉所有事件

  


// ✅ 想同时响应 touch 和 click

view.setOnTouchListener((v, event) -> {

    // 处理触摸反馈,但不消费事件

    if (event.getAction() == MotionEvent.ACTION_DOWN) {

        v.setAlpha(0.5f);

    }

    return false;  // 不消费,让 onClick 还能触发

});

⑧ 事件分发中 ACTION_DOWN 没被消费会怎样?

**答案:如果 ACTION_DOWN 没任何 View 消费(所有层 onTouchEvent 都返回 false),后续的 MOVE 和 UP 事件不会再传递给任何 View,直接由 Activity 的 onTouchEvent 处理。因为 DOWN 事件决定了事件的目标接收者,DOWN 没被消费意味着没有 View 愿意接收这个事件序列,后续事件就没有了分发对象。 **

事件序列的规则

scss 复制代码
一个完整的手势 = 1 个 DOWN + 0~N 个 MOVE + 1 个 UP

  


规则1:DOWN 决定目标

  DOWN 被谁消费,后续 MOVE/UP 都直接发给谁

  (不再经过 onInterceptTouchEvent,除非主动请求拦截)

  


规则2:DOWN 无人消费

  后续 MOVE/UP 不会分发给任何子 View

  直接交给 Activity.onTouchEvent()

  


规则3:DOWN 被消费后

  后续事件直接传给消费 DOWN 的 View

  不再遍历子 View 寻找目标

源码关键逻辑

java 复制代码
// ViewGroup.dispatchTouchEvent() 核心逻辑

  


// ACTION_DOWN 时:重置所有状态,寻找目标

if (actionMasked == MotionEvent.ACTION_DOWN) {

    cancelAndClearTouchTargets(ev);  // 清除之前的触摸目标

    resetTouchState();                // 重置拦截标记

}

  


// 寻找接收事件的子 View(只在 DOWN 时执行)

if (!canceled && !intercepted) {

    if (actionMasked == MotionEvent.ACTION_DOWN

        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)) {

        // 遍历子 View,寻找能消费 DOWN 的目标

        for (child : children) {

            if (child.dispatchTouchEvent(ev)) {

                mFirstTouchTarget = new TouchTarget(child);  // 记录目标

                break;

            }

        }

    }

}

  


// 后续事件(MOVE/UP)直接发给 mFirstTouchTarget

if (mFirstTouchTarget == null) {

    // DOWN 没被消费,后续事件自己处理

    handled = dispatchTransformedTouchEvent(ev, null);  // 传给自身

} else {

    // DOWN 被消费了,后续事件直接发给目标

    TouchTarget target = mFirstTouchTarget;

    while (target != null) {

        dispatchTransformedTouchEvent(ev, target.child);

        target = target.next;

    }

}

图示

scss 复制代码
DOWN 没被消费:

  DOWN → ViewGroup → 子View1(不消费) → 子View2(不消费) → 回传 → 没人消费

  MOVE → 不再分发给子View → Activity.onTouchEvent()

  UP   → 不再分发给子View → Activity.onTouchEvent()

  


DOWN 被子View2消费:

  DOWN → ViewGroup → 子View1(不消费) → 子View2(消费✅) → 记录 mFirstTouchTarget = 子View2

  MOVE → 直接发给子View2 (不遍历子View)

  UP   → 直接发给子View2 → 清除 mFirstTouchTarget

⑨ 事件冲突怎么解决?内部拦截法和外部拦截法的区别?

**答案:事件冲突的解决方案分为外部拦截法和内部拦截法。外部拦截法:在父 ViewGroup 的 onInterceptTouchEvent 中根据条件决定是否拦截,推荐使用。内部拦截法:在子 View 的 dispatchTouchEvent 中通过 requestDisallowInterceptTouchEvent 控制父 View 是否拦截,子 View 主动权更大但更复杂。 **

外部拦截法(推荐)

java 复制代码
// 父 ViewGroup 中

@Override

public boolean onInterceptTouchEvent(MotionEvent ev) {

    boolean intercepted = false;

    switch (ev.getAction()) {

        case MotionEvent.ACTION_DOWN:

            intercepted = false;  // DOWN 必须不拦截,否则子 View 收不到任何事件

            break;

        case MotionEvent.ACTION_MOVE:

            if (父容器需要此事件) {

                intercepted = true;   // 拦截,自己处理

            } else {

                intercepted = false;  // 不拦截,给子 View

            }

            break;

        case MotionEvent.ACTION_UP:

            intercepted = false;  // UP 也不拦截,让子 View 收到 UP 触发 onClick

            break;

    }

    return intercepted;

}

内部拦截法

java 复制代码
// 子 View 中

@Override

public boolean dispatchTouchEvent(MotionEvent ev) {

    switch (ev.getAction()) {

        case MotionEvent.ACTION_DOWN:

            // DOWN 时请求父 View 不拦截

            getParent().requestDisallowInterceptTouchEvent(true);

            break;

        case MotionEvent.ACTION_MOVE:

            if (子View不需要此事件) {

                // 让父 View 拦截

                getParent().requestDisallowInterceptTouchEvent(false);

            }

            break;

        case MotionEvent.ACTION_UP:

            break;

    }

    return super.dispatchTouchEvent(ev);

}

  


// 父 ViewGroup 中必须重写 onInterceptTouchEvent

@Override

public boolean onInterceptTouchEvent(MotionEvent ev) {

    if (ev.getAction() == MotionEvent.ACTION_DOWN) {

        return false;  // DOWN 不拦截

    }

    return true;  // 其他事件默认拦截(除非子 View 请求不拦截)

}

对比

| | 外部拦截法 | 内部拦截法 |

|---|---|---|

| 控制方 | 父 ViewGroup | 子 View |

| 实现位置 | 父的 onInterceptTouchEvent | 子的 dispatchTouchEvent + 父的 onInterceptTouchEvent |

| 代码复杂度 | 简单 | 较复杂 |

| 推荐程度 | ✅ 推荐 | 较少用 |

| 适用场景 | 父容器能判断何时需要拦截 | 子 View 更清楚自己何时需要事件 |

requestDisallowInterceptTouchEvent 原理

java 复制代码
// View.java

public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

    if (disallowIntercept) {

        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;

    } else {

        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;

    }

    // 递归通知所有父 View

    if (mParent != null) {

        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);

    }

}

  


// ViewGroup.dispatchTouchEvent() 中检查

final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

if (!disallowIntercept) {

    intercepted = onInterceptTouchEvent(ev);

} else {

    intercepted = false;  // 子 View 请求不拦截,直接 false

}

⑩ ViewGroup 的 dispatchTouchEvent 完整流程?

**答案:ViewGroup 的 dispatchTouchEvent 流程是:① 检查是否被请求禁止拦截 → ② 调用 onInterceptTouchEvent 决定是否拦截 → ③ 如果不拦截且是 DOWN 事件,遍历子 View 找目标 → ④ 找到目标后记录到 mFirstTouchTarget → ⑤ 后续事件直接发给目标 → ⑥ 如果没找到目标,自己处理。 **

完整流程图

ini 复制代码
ViewGroup.dispatchTouchEvent(ev)

    │

    ├── actionMasked == ACTION_DOWN?

    │     └── 是 → cancelAndClearTouchTargets() + resetTouchState()

    │

    ├── 检查 disallowIntercept 标记

    │     ├── 是 → intercepted = false

    │     └── 否 → intercepted = onInterceptTouchEvent(ev)

    │

    ├── !canceled && !intercepted?

    │     └── 是 且是 DOWN → 遍历子 View(逆序,从上层到底层)

    │           ├── 子 View 可见且在触摸点内?

    │           ├── child.dispatchTouchEvent(ev) == true?

    │           │     └── 是 → mFirstTouchTarget = 该子View → break

    │           └── 所有子 View 都不消费 → mFirstTouchTarget = null

    │

    ├── mFirstTouchTarget == null?

    │     ├── 是 → 没有子 View 消费 → 自己处理(super.dispatchTouchEvent → onTouchEvent)

    │     └── 否 → 发给 mFirstTouchTarget 指向的子 View

    │

    └── return handled

关键细节

java 复制代码
// 1. 遍历子 View 是逆序的(从最上层开始)

// 因为后 add 的子 View 绘制在最上面,应该优先接收事件

for (int i = childrenCount - 1; i >= 0; i--) {

    View child = children[i];

    if (isTransformedTouchPointInView(x, y, child)) {

        if (dispatchTransformedTouchEvent(ev, false, child)) {

            mFirstTouchTarget = addTouchTarget(child);

            break;  // 找到目标就不再遍历

        }

    }

}

  


// 2. mFirstTouchTarget 是链表结构(多点触控)

// 单点触控时只有一个 TouchTarget

// 多点触控时每个手指可能对应不同的 TouchTarget

  


// 3. 后续事件不再遍历子 View

// MOVE/UP 事件直接查看 mFirstTouchTarget 发给目标

⑪ 为什么 ScrollView 嵌套 RecyclerView 会滑动冲突?怎么解决?

**答案:本质原因是两个可滑动容器对同一个 MOVE 事件都想要消费,但方向一致导致父容器先拦截了事件。ScrollView 默认在垂直方向会拦截 MOVE 事件,导致 RecyclerView 收不到垂直 MOVE,表现为 RecyclerView 无法滑动或滑动不流畅。解决方案:外部拦截法(在 ScrollView 中根据滑动方向控制拦截)或使用 NestedScrollView 替代 ScrollView(内置嵌套滑动支持)。 **

冲突原因分析

scss 复制代码
ScrollView(垂直滑动)

  └── RecyclerView(也是垂直滑动)

  


当手指垂直滑动时:

  ScrollView.onInterceptTouchEvent(MOVE) → 拦截!

  → RecyclerView 收不到 MOVE 事件

  → RecyclerView 无法滑动

  


本质:两个 View 对同一方向的 MOVE 事件都想消费

结果:父 View 拦截了,子 View 滑不动

方案一:使用 NestedScrollView(最推荐)

xml 复制代码
<!-- ❌ 有冲突 -->

<ScrollView>

    <RecyclerView />

</ScrollView>

  


<!-- ✅ 替代方案 -->

<androidx.core.widget.NestedScrollView>

    <RecyclerView />

</androidx.core.widget.NestedScrollView>

NestedScrollView 实现了 NestedScrollingParent 和 NestedScrollingChild 接口,支持嵌套滑动机制。

方案二:外部拦截法

java 复制代码
public class CustomScrollView extends ScrollView {

    private float mDownX, mDownY;

  


    @Override

    public boolean onInterceptTouchEvent(MotionEvent ev) {

        switch (ev.getAction()) {

            case MotionEvent.ACTION_DOWN:

                mDownX = ev.getX();

                mDownY = ev.getY();

                break;

            case MotionEvent.ACTION_MOVE:

                float dx = ev.getX() - mDownX;

                float dy = ev.getY() - mDownY;

                // 只有当父容器需要滑动时才拦截

                if (Math.abs(dy) > Math.abs(dx)) {

                    if (父容器还能滑动) {

                        return true;   // 父容器拦截

                    }

                }

                return false;  // 交给 RecyclerView

        }

        return super.onInterceptTouchEvent(ev);

    }

}

方案三:嵌套滑动机制(NestedScrolling)

sql 复制代码
嵌套滑动机制(Android 5.0+ 支持):

  


正常事件分发:父 View 拦截后子 View 收不到

嵌套滑动:子 View 先消费,消费不完再给父 View

  


流程:

  1. 子 View 滑动前,先问父 View 是否要先消费部分滑动

  2. 子 View 消费剩余滑动

  3. 子 View 消费不完的,再问父 View 是否消费

  4. 父 View 和子 View 都消费不了,滑动结束

  


RecyclerView 内部实现了 NestedScrollingChild

NestedScrollView 实现了 NestedScrollingParent

所以 NestedScrollView + RecyclerView 天然支持嵌套滑动

⑫ View 的 post 方法原理?和 Handler.post 的区别?

**答案:View.post() 的作用是将 Runnable 投递到主线程消息队列执行,确保在 View 的 measure/layout 完成后执行。原理是:如果 View 已 attach 到 Window,通过 AttachInfo 中的 Handler 执行;如果未 attach,将 Runnable 加入队列,在 dispatchAttachedToWindow 时批量执行。与 Handler.post 的区别是:View.post 保证 Runnable 在 View 宽高确定后执行,而 Handler.post 不保证。 **

View.post 源码

java 复制代码
public boolean post(Runnable action) {

    final AttachInfo attachInfo = mAttachInfo;

    if (attachInfo != null) {

        // View 已 attach,直接通过 Handler 投递

        return attachInfo.mHandler.post(action);

    }

  


    // View 未 attach,加入待执行队列

    // 等待 dispatchAttachedToWindow 时执行

    getRunQueue().post(action);

    return true;

}

  


// View.dispatchAttachedToWindow()

void dispatchAttachedToWindow(AttachInfo info, int visibility) {

    mAttachInfo = info;

    // ...

    // 执行之前缓存的 Runnable

    if (mRunQueue != null) {

        mRunQueue.executeActions(info.mHandler);

        mRunQueue = null;

    }

}

典型使用场景

java 复制代码
// ❌ onCreate 中获取宽高为 0

@Override

protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);

    int width = textView.getWidth();   // 0!还没有 measure

}

  


// ✅ 使用 View.post 获取宽高

@Override

protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);

    textView.post(() -> {

        int width = textView.getWidth();   // 正确值

        int height = textView.getHeight(); // 正确值

    });

}

  


// ✅ 使用 ViewTreeObserver(更早,但更复杂)

textView.getViewTreeObserver().addOnGlobalLayoutListener(

    new ViewTreeObserver.OnGlobalLayoutListener() {

        @Override

        public void onGlobalLayout() {

            int width = textView.getWidth();

            textView.getViewTreeObserver().removeOnGlobalLayoutListener(this);

        }

    });

对比

| | View.post | Handler.post |

|---|---|---|

| 执行时机 | View 的 measure/layout 完成后 | 投递到主线程 Looper 后 |

| 宽高是否确定 | ✅ 确定 | ❌ 不确定 |

| 依赖 | 需要 View 已 attach | 只要 Looper 存在 |

| 队列 | 未 attach 时缓存到 RunQueue | 直接入 MessageQueue |


⑬ Activity 的事件分发入口是什么?从屏幕触摸到 View 收到事件的完整链路?

**答案:从硬件触摸到 View 收到事件的完整链路是:屏幕硬件 → 内核驱动 → /dev/input 节点 → InputManagerService → InputDispatcher → Window(InputChannel) → InputEventReceiver → ViewRootImpl → DecorView → Activity.dispatchTouchEvent → Window → ViewGroup → View。 **

完整链路

scss 复制代码
┌─────────────────────────────────────────────────────────┐

│  1. 硬件层                                               │

│  触摸屏 → 内核驱动 → /dev/input/eventX                  │

└───────────────────┬─────────────────────────────────────┘

                    ↓

┌─────────────────────────────────────────────────────────┐

│  2. Native 层                                            │

│  EventHub.readEvents() → InputReader → InputDispatcher  │

│  (InputDispatcher 通过 InputChannel 发送到 App 进程)     │

└───────────────────┬─────────────────────────────────────┘

                    ↓

┌─────────────────────────────────────────────────────────┐

│  3. App 进程 Java 层                                     │

│  InputEventReceiver.onInputEvent()                       │

│    → ViewRootImpl.processInputEvent()                    │

│    → ViewRootImpl.enqueueInputEvent()                    │

│    → ViewRootImpl.doProcessInputEvents()                 │

│    → ViewRootImpl.deliverInputEvent()                    │

└───────────────────┬─────────────────────────────────────┘

                    ↓

┌─────────────────────────────────────────────────────────┐

│  4. View 层事件分发                                       │

│  ViewRootImpl.windowInputEventReceiver                   │

│    → mView.dispatchPointerEvent()  (mView = DecorView)  │

│    → DecorView.dispatchTouchEvent()                      │

│      → Window.Callback.superDispatchTouchEvent()         │

│        → Activity.dispatchTouchEvent()                    │

│          → PhoneWindow.superDispatchTouchEvent()          │

│            → DecorView.superDispatchTouchEvent()          │

│              → ViewGroup.dispatchTouchEvent()              │

│                → 子 View 逐层分发...                       │

└─────────────────────────────────────────────────────────┘

Activity 中的分发

java 复制代码
// Activity.dispatchTouchEvent() --- 事件入口

public boolean dispatchTouchEvent(MotionEvent ev) {

    if (ev.getAction() == MotionEvent.ACTION_DOWN) {

        onUserInteraction();  // 空方法,可重写

    }

    // 先给 Window 处理

    if (getWindow().superDispatchTouchEvent(ev)) {

        return true;  // Window 中的 View 消费了

    }

    // 没有 View 消费,Activity 自己处理

    return onTouchEvent(ev);

}

  


// Activity.onTouchEvent() --- 兜底处理

public boolean onTouchEvent(MotionEvent event) {

    // 点击 Window 边界外会关闭 Activity(如果设置了)

    if (mWindow.shouldCloseOnTouch(this, event)) {

        finish();

        return true;

    }

    return false;

}

⑭ 自定义 View 的注意事项?wrap_content 和 padding 不生效的原因?

**答案:自定义 View 时 wrap_content 不生效是因为 View 默认 onMeasure 中 AT_MOST 模式直接取 SpecSize(等于父容器剩余空间),导致 wrap_content 表现和 match_parent 一样;padding 不生效是因为默认 onDraw 不会考虑 padding,需要手动在绘制时缩进。解决方式:重写 onMeasure 处理 AT_MOST,重写 onDraw 考虑 padding。 **

wrap_content 不生效

java 复制代码
// 默认 View.onMeasure → getDefaultSize

public static int getDefaultSize(int size, int measureSpec) {

    int result = size;

    switch (specMode) {

        case UNSPECIFIED: result = size; break;

        case EXACTLY:     // match_parent 或精确值

        case AT_MOST:      // wrap_content ← 也取 specSize!

            result = specSize; break;

    }

    return result;

}

// 结果:wrap_content 和 match_parent 表现一样

解决:重写 onMeasure

java 复制代码
@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);

    int widthSize = MeasureSpec.getSize(widthMeasureSpec);

    int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

  


    int width, height;

  


    if (widthMode == MeasureSpec.AT_MOST) {

        // wrap_content:使用自定义的默认大小

        width = dp2px(200);  // 默认宽度

    } else {

        width = widthSize;   // match_parent 或精确值

    }

  


    if (heightMode == MeasureSpec.AT_MOST) {

        height = dp2px(100); // 默认高度

    } else {

        height = heightSize;

    }

  


    setMeasuredDimension(width, height);

}

padding 不生效

java 复制代码
// ❌ 默认 onDraw 不考虑 padding

@Override

protected void onDraw(Canvas canvas) {

    canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, mPaint);

    // 绘制区域没有减去 padding,padding 区域也会被绘制

}

  


// ✅ 手动处理 padding

@Override

protected void onDraw(Canvas canvas) {

    int paddingLeft = getPaddingLeft();

    int paddingTop = getPaddingTop();

    int paddingRight = getPaddingRight();

    int paddingBottom = getPaddingBottom();

  


    int availableWidth = getWidth() - paddingLeft - paddingRight;

    int availableHeight = getHeight() - paddingTop - paddingBottom;

  


    float cx = paddingLeft + availableWidth / 2f;

    float cy = paddingTop + availableHeight / 2f;

    float radius = Math.min(availableWidth, availableHeight) / 2f;

  


    canvas.drawCircle(cx, cy, radius, mPaint);

}

自定义 View 检查清单

markdown 复制代码
1. wrap_content → 重写 onMeasure 处理 AT_MOST

2. padding → onDraw 和 onMeasure 中都要处理

3. 避免 onDraw 中创建对象 → 构造器中创建 Paint 等

4. 硬件加速兼容 → 某些绘制 API 不支持硬件加速

5. 状态保存 → onSaveInstanceState / onRestoreInstanceState

6. attrs 自定义属性 → declare-styleable + obtainStyledAttributes

7. ViewGroup 要重写 onLayout → 否则子 View 不会显示

8. setWillNotDraw(false) → ViewGroup 默认不会调用 onDraw

⑮ Surface 和 SurfaceFlinger 在绘制中的作用?

**答案:Surface 是 View 绘制的画布,每个 Window 对应一个 Surface,View 的 draw 最终绘制到 Surface 的 Buffer 上;SurfaceFlinger 是系统级的合成服务,收集所有 Surface 的 Buffer,通过硬件合成器(HWC)将它们合成最终画面,输出到屏幕。简单说:View 负责画内容到 Surface,SurfaceFlinger 负责把所有 Surface 合成到屏幕。 **

绘制架构图

scss 复制代码
┌─────────────────────────────────────────────────────────┐

│  应用进程                                                │

│                                                         │

│  View.draw(Canvas)                                      │

│       ↓                                                 │

│  Canvas → Surface.lockCanvas()                          │

│       ↓                                                 │

│  写入 Surface 的 Buffer (Bitmap)                        │

│       ↓                                                 │

│  Surface.unlockCanvasAndPost() → BufferQueue.queueBuffer│

│                                                         │

└────────────────────┬────────────────────────────────────┘

                     │ (Binder IPC)

                     ↓

┌─────────────────────────────────────────────────────────┐

│  SurfaceFlinger 进程                                     │

│                                                         │

│  接收所有 App 的 Buffer                                  │

│       ↓                                                 │

│  Layer 合成(Z-order、透明度、变换)                      │

│       ↓                                                 │

│  硬件合成器 HWC (Hardware Composer)                      │

│       ↓                                                 │

│  输出到显示屏 (Framebuffer)                              │

│                                                         │

└─────────────────────────────────────────────────────────┘

三重缓冲

css 复制代码
Surface 内部使用 BufferQueue,默认三重缓冲:

  


  App 占用 Buffer-A 绘制

  SurfaceFlinger 占用 Buffer-B 合成

  Buffer-C 空闲可被 App 获取

  


  ┌───┐ ┌───┐ ┌───┐

  │ A │ │ B │ │ C │   BufferQueue

  └─┬─┘ └─┬─┘ └─┬─┘

    │     │     │

  App   SF    空闲

  


好处:App 和 SurfaceFlinger 不同时操作同一 Buffer,避免撕裂

Surface 与 View 的关系

scss 复制代码
Window

  └── DecorView

        └── View 树

              └── ViewRootImpl

                    ├── Surface (通过 SurfaceControl 创建)

                    └── performDraw()

                          └── Canvas = Surface.lockCanvas()

                                └── View 树绘制到 Canvas

                                      └── Surface.unlockCanvasAndPost()

  


每个 Window 有一个 Surface

SurfaceView 拥有独立的 Surface(独立线程绘制)

TextureView 共享 Surface(通过 TextureLayer)

SurfaceView vs TextureView

| | SurfaceView | TextureView |

|---|---|---|

| Surface | 独立 Surface | 共享 Surface |

| 绘制线程 | 可以在子线程 | 必须在主线程 |

| 动画/变换 | 不支持 | 支持 |

| 性能 | 更好(无合成开销) | 略差(多了 GPU 合成) |

| 内存 | 更少 | 更多(TextureLayer) |

| 适用场景 | 视频、相机、游戏 | 需要动画/变换的场景 |

SurfaceFlinger 合成流程

markdown 复制代码
1. App 通过 BufferQueue 提交已绘制完的 Buffer

2. SurfaceFlinger 被 VSYNC 唤醒

3. 收集所有 Layer(每个 Surface 对应一个 Layer)

4. 根据 Z-order、透明度、裁剪区域等合成

5. 优先使用 HWC(硬件合成器)合成

6. HWC 无法处理的交由 GPU 合成

7. 合成结果写入 Framebuffer

8. 显示控制器从 Framebuffer 读取并显示

全景总结

sql 复制代码
View 绘制与事件分发 知识图谱:

  


绘制流程(5题):

  1. 入口与流程   ─── performTraversals → measure/layout/draw

  2. MeasureSpec  ─── 父约束+子LayoutParams → 子MeasureSpec

  3. Layout       ─── 确定位置,left/top/right/bottom

  4. Draw 顺序    ─── 背景 → 内容 → 子View → 前景(画家算法)

  5. 三种刷新     ─── requestLayout/invalidte/postInvalidte

  


事件分发(8题):

  6. 三个方法     ─── dispatch/intercept/touchEvent U型模型

  7. 优先级       ─── onTouch > onTouchEvent > onClick

  8. DOWN未消费   ─── 后续事件不再分发

  9. 冲突解决     ─── 外部拦截法 vs 内部拦截法

  10. ViewGroup流程 ─── 遍历子View→记录TouchTarget→后续直达

  11. 滑动冲突    ─── 嵌套滑动机制 NestedScrolling

  12. View.post   ─── 保证宽高已确定,AttachInfo Handler

  13. 完整链路    ─── 硬件→IMS→InputChannel→ViewRootImpl→View

  


进阶(2题):

  14. 自定义View   ─── wrap_content/padding/避免onDraw创建对象

  15. Surface      ─── Surface画布→BufferQueue→SurfaceFlinger合成
相关推荐
ShineWinsu1 小时前
对于Linux:进程信号的解析—下
linux·运维·服务器·面试·笔试·进程·信号
我是一颗柠檬2 小时前
【Redis】Redis面试高频考点汇总Day15(2026年)
数据库·redis·缓存·面试
IT策士2 小时前
Docker 常见面试问题
docker·容器·面试
SiYuanFeng2 小时前
大模型 / RAG / Agent 面试高频题
人工智能·面试·transformer·agent·rag
ftf拿破仑3 小时前
嵌入式面试高频问题
linux·面试
IT策士3 小时前
k8s 常见面试问题
容器·面试·kubernetes
Cosolar14 小时前
LlamaIndex 文档解析与分块策略深度解析
人工智能·面试·架构
kyriewen16 小时前
我读了一遍 Babel 编译后的 async/await,终于搞懂了它的原理(附 20 行手写实现)
前端·javascript·面试
zzz_236818 小时前
【RabbitMQ】面试系列 · 第三期:从线上故障到架构选型
面试·架构·rabbitmq