安卓事件分发与自定义View绘制

161 阅读4分钟

好的同学们!今天我们上一堂「安卓事件分发与自定义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;

​随堂测验​

  1. 当父ViewGroup的onInterceptTouchEvent()返回true后,后续事件去了哪?
    ​答案:​​ 直接交给该ViewGroup的onTouchEvent()处理!
  2. 自定义View时,忘记处理MeasureSpec.AT_MOST会导致什么问题?
    ​答案:​​ 在wrap_content时尺寸会和match_parent效果一样!
  3. 只更新View显示内容但不改变尺寸位置,应该调用什么方法?
    ​答案:​invalidate() → 只触发重绘(onDraw)

掌握这两大机制,你就能搞定90%的UI交互和自定义View需求!下节课我们讲「性能优化陷阱」,记得预习ViewTreeObserverSystrace工具的使用!下课~ 👨‍🏫