① 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合成