一句话说透Android里面事件在哪些对象之间进行传递

86 阅读3分钟

一句话总结:
Android事件传递就像快递配送流程

  • 发件人:屏幕触摸事件
  • 配送路径:Activity(总部) → Window(分拣中心) → ViewGroup(区域站点) → View(最终收件人)

一、事件传递完整路径

1. 起送站:Activity

手指触碰屏幕 → 系统生成触摸事件 → 第一个接收者永远是Activity  

类比:快递总部接收客户订单


2. 中转站:Window(PhoneWindow)

// 关键方法  
public boolean superDispatchTouchEvent(MotionEvent event)  

作用:将事件传递给DecorView(整个窗口的根View)
类比:总部把包裹分发给城市分拣中心


3. 区域分发站:DecorView

// 通常是FrameLayout,包含状态栏、标题栏、内容区域  
事件传递顺序:  
DecorView → 内容区域的根布局(如LinearLayout) → 子View  

比喻:分拣中心根据地址分配到不同区域的快递站点


4. 派送员:ViewGroup

// 核心方法  
public boolean dispatchTouchEvent(MotionEvent ev)  
public boolean onInterceptTouchEvent(MotionEvent ev)  

关键决策

  • 自己处理(派送员私吞包裹❌)
  • 继续派送(传给子View)
  • 拦截包裹(自己处理)

5. 收件人:View

public boolean onTouchEvent(MotionEvent event)  

最终处理

  • 返回true:签收成功(事件不再传递)
  • 返回false:拒收包裹(事件回传)

二、典型传递场景

场景1:点击按钮

Activity → Window → DecorView → LinearLayout → Button  
                                 ↓              ↓  
                              不拦截          onTouch返回true(成功处理)  

场景2:滑动ScrollView

Activity → Window → DecorView → ScrollView → TextView  
                                 ↓          ↓  
                              不拦截     onIntercept返回true(拦截事件自己处理滚动)  

场景3:事件无人处理

Activity → Window → DecorView → FrameLayout → ImageView  
                                 ↓               ↓  
                              不拦截         onTouch返回false(拒收)  
事件回传路径:ImageView → FrameLayout → DecorView → Activity  

三、核心对象职责表

对象角色关键方法类比
Activity事件入口dispatchTouchEvent()快递总部
Window事件中转superDispatchTouchEvent()分拣中心
ViewGroup调度决策者onInterceptTouchEvent()区域站长
View最终执行者onTouchEvent()收件人

四、常见问题解答

Q1:事件传递顺序能改吗?

  • 不能改默认顺序,但可通过以下方式控制:

    // 在ViewGroup中拦截  
    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {  
        return true // 中断向子View传递  
    }  
    
    // 在View中消费事件  
    override fun onTouchEvent(e: MotionEvent): Boolean {  
        return true // 阻止事件回传  
    }  
    

Q2:多个手指触摸怎么处理?

系统通过MotionEvent指针索引区分:

val actionIndex = e.actionIndex  // 当前操作的手指索引  
val pointerId = e.getPointerId(actionIndex)  // 获取唯一ID  

Q3:为什么有时候点击没反应?

三大常见原因

  1. 父ViewGroup拦截了事件(站长扣留包裹)
  2. 目标View的onTouchEvent返回false(收件人拒收)
  3. View被设置为不可点击(android:clickable="false"

五、避坑指南

坑1:事件冲突(父子抢包裹)

案例:ViewPager嵌套RecyclerView

// 解决思路:根据滑动方向决定谁处理  
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {  
    when(计算滑动方向()) {  
        水平滑动 → 自己处理(ViewPager翻页)  
        垂直滑动 → 不拦截(给RecyclerView滚动)  
    }  
}  

坑2:内存泄漏

错误示范

// 在Activity中直接持有Handler  
val handler = Handler() // 导致Activity无法回收  

正确方案

// 使用弱引用+静态内部类  
class SafeHandler(activity: WeakReference<MainActivity>) : Handler()  

坑3:事件延迟卡顿

优化技巧

// 在自定义View中减少onDraw复杂度  
override fun onTouchEvent(e: MotionEvent): Boolean {  
    when(e.action)  {  
        ACTION_MOVE → {  
            updateData()  
            postInvalidate() // 异步刷新避免阻塞事件线程  
        }  
    }  
    return true  
}  

终极口诀:
事件传递如快递,Activity是总站起
Window分拣不处理,ViewGroup派送看心情
View收件要确认,返回true才停息
冲突方向来判断,拦截消费要牢记!