好的同学们!今天我们上一堂「安卓事件分发与自定义View绘制」的实战课。我会用快递站取包裹和工厂流水线的比喻帮你彻底理解这两个核心机制。准备好纸笔,我们开始!
第一讲:事件分发 —— 快递配送系统
比喻: 把事件分发想象成快递站处理你的包裹(点击事件)
核心角色:Activity(总仓) →ViewGroup(分拣中心) →View(收货人)
1. 快递配送流程图解
graph TD
A[用户点击屏幕] --> B(物理驱动)
B --> C{系统内核}
C --> |生成MotionEvent| D[Activity]
D --> |dispatchTouchEvent| E[Window]
E --> |PhoneWindow| F[DecorView]
F --> |ViewGroup| G[顶级ViewGroup ex:LinearLayout]
G --> |dispatchTouchEvent| H[遍历子View]
H --> |onInterceptTouchEvent?| I{是否拦截}
I --> |是| J[自己处理 onTouchEvent]
I --> |否| K[分发到子View]
2. 关键环节拆解
场景: 你点击了屏幕上的一个按钮(Button)
(1) Activity:物流总仓
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction(); // 埋点:用户开始交互
}
return getWindow().superDispatchTouchEvent(ev);
// ↑ 把包裹交给下一站:Window
}
(2) ViewGroup:快递分拣中心
public boolean dispatchTouchEvent(MotionEvent ev) {
// 检查1:要不要拦截? (老板特权)
if (onInterceptTouchEvent(ev)) {
return onTouchEvent(ev); // 拦截!我自己处理
}
// 检查2:不拦截,找能处理的子View(扫描仓库)
for (int i = childrenCount - 1; i >= 0; i--) {
View child = getChildAt(i);
if (child.dispatchTouchEvent(ev)) return true;
// ↑ 子View签收成功!
}
// 没人认领?我自己试试
return super.onTouchEvent(ev);
}
💡 重点陷阱:
事件被孩子View处理后,后续所有事件(MOVE/UP)直接给它! 不再询问拦截(避免跳单)
这就好比:快递员知道你家孩子在代收快递,之后包裹都直接给孩子
3. View:最终收货人
public boolean dispatchTouchEvent(MotionEvent event) {
// 如果有OnTouchListener,优先执行
if (mOnTouchListener != null && onTouch(event)) {
return true; // 被监听器“代签”了
}
return onTouchEvent(event); // 自己签收
}
收货优先级:
OnTouchListener > onTouchEvent() > OnClickListener
📦 快递签收示例:
当你点击Button时,会触发:
onTouchEvent(ACTION_DOWN)→onTouchEvent(ACTION_UP)→onClick()
第二讲:自定义View绘制 —— 工厂流水线
比喻: 把View绘制想象成工厂生产零件(View)的流水线
核心工序: 测量(量尺寸) → 布局(定位置) → 绘制(喷油漆)
1. 生产流水线全流程
graph LR
A[收到VSYNC信号] --> B(Choreographer)
B --> C[ViewRootImpl#doFrame]
C --> D1[performMeasure]
C --> D2[performLayout]
C --> D3[performDraw]
D1 --> |递归| E1[自定义View#onMeasure]
D2 --> |递归| E2[自定义View#onLayout]
D3 --> |递归| E3[自定义View#onDraw]
2. 关键工序详解
(1) 测量车间:onMeasure()
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// MeasureSpec = 模式(EXACTLY/AT_MOST) + 尺寸
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
// 计算自己需要的尺寸(核心步骤)
int desiredWidth = ...; // 根据内容计算
// 处理不同模式(老板要求 vs 实际需求)
int finalWidth;
if (widthMode == MeasureSpec.EXACTLY) {
finalWidth = widthSize; // 老板说了算(match_parent/具体dp)
} else {
finalWidth = desiredWidth; // 自己决定(wrap_content)
if (widthMode == MeasureSpec.AT_MOST) {
finalWidth = Math.min(desiredWidth, widthSize); // 不能超过老板给的上限
}
}
// 确定最终尺寸(打包尺寸标签)
setMeasuredDimension(finalWidth, finalHeight);
}
⚠️ 新手坑:自定义View必须处理wrap_content!
否则尺寸会填满父容器(等价match_parent)
(2) 布局车间:onLayout()(仅ViewGroup需要)
// ViewGroup负责安排孩子的位置
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// 读取孩子自己测量的尺寸
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 计算位置(不同布局不同算法)
int childLeft = ...;
int childTop = ...;
// 通知孩子:“你的工位在这里!”
child.layout(childLeft, childTop,
childLeft + childWidth,
childTop + childHeight);
}
}
(3) 喷漆车间:onDraw()
protected void onDraw(Canvas canvas) {
super.onDraw(canvas); // 默认绘制背景
// 拿起画笔(Paint)
Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setStyle(Style.STROKE);
// 开始绘制(调用Canvas API)
canvas.drawRect(0, 0, getWidth(), getHeight(), paint); // 画边框
canvas.drawCircle(getWidth()/2, getHeight()/2, 50, paint); // 画圆心
}
🎨 高级技巧:
避免在onDraw()中创建对象(防止GC卡顿) → 在构造函数初始化Paint!
3. 工序触发条件
| 操作 | 触发工序 | 性能影响 |
|---|---|---|
setVisibility() | measure + layout + draw | 中 |
requestLayout() | measure + layout | 高 |
invalidate() | draw | 低 |
课代表总结图
graph LR
A[安卓UI两大机制] --> B[事件分发系统]
A --> C[View绘制流水线]
B --> B1[责任链模式]
B --> B2[核心方法:dispatchTouchEvent<br>onInterceptTouchEvent<br>onTouchEvent]
C --> C1[三大工序:measure→layout→draw]
C --> C2[核心方法:onMeasure<br>onLayout<br>onDraw]
B1 --> D[比喻:快递配送]
C1 --> E[比喻:工厂生产]
classDef green fill:#D5E8D4,stroke:#82B366;
classDef blue fill:#DAE8FC,stroke:#6C8EBF;
class B,C green;
class B1,B2,C1,C2 blue;
随堂测验
- 当父ViewGroup的
onInterceptTouchEvent()返回true后,后续事件去了哪?
答案: 直接交给该ViewGroup的onTouchEvent()处理! - 自定义View时,忘记处理
MeasureSpec.AT_MOST会导致什么问题?
答案: 在wrap_content时尺寸会和match_parent效果一样! - 只更新View显示内容但不改变尺寸位置,应该调用什么方法?
答案:invalidate()→ 只触发重绘(onDraw)
掌握这两大机制,你就能搞定90%的UI交互和自定义View需求!下节课我们讲「性能优化陷阱」,记得预习ViewTreeObserver和Systrace工具的使用!下课~ 👨🏫