大白话讲解 Android 的触摸事件

98 阅读11分钟

我们把 Android 的触摸事件流程想象成一个快递配送系统,力求通俗易懂地拆解它!

核心目标:  把用户的手指触摸(点击、滑动、长按等)这个“包裹”,准确无误地送到能“签收”(处理)它的“收件人”(View)手中。

主要角色:

  1. Activity:  整个应用的“总部”。它最先知道有快递(触摸事件)来了。
  2. Window:  Activity 的“前台窗口”。负责把事件初步转交给合适的“区域”。
  3. DecorView:  窗口的“根视图容器”(通常是 FrameLayout),包含了标题栏和 setContentView() 设置的整个布局。它是视图树的“顶端配送中心”。
  4. ViewGroup:  像 LinearLayoutRelativeLayoutFrameLayout 等可以包含其他 View 或 ViewGroup 的“区域配送中心”或“配送站”。负责把事件分发给自己的“下属”(子 View)。
  5. View:  像 ButtonTextViewImageView 等不能再包含其他 View 的“最终收件人”。它们决定是否签收(消费)这个包裹(事件)。

触摸事件包裹:

  • 每个包裹都贴有“事件类型”标签:ACTION_DOWN(手指按下)、ACTION_MOVE(手指移动)、ACTION_UP(手指抬起)、ACTION_CANCEL(事件被取消)等。
  • 一个完整的“手势”(比如一次点击)通常包含一个 ACTION_DOWN,零个或多个 ACTION_MOVE,最后以一个 ACTION_UP 或 ACTION_CANCEL 结束。ACTION_DOWN 是关键起点,它决定了后续事件会送给谁处理。

核心配送流程(分发逻辑):

想象一个树状结构(视图树),根是 DecorView,下面是层层嵌套的 ViewGroup,叶子节点是 View

  1. 总部接收 (Activity -> Window -> DecorView):

    • 当用户触摸屏幕,系统内核生成一个触摸事件。
    • 事件首先到达 Activity 的 dispatchTouchEvent() 方法。Activity 说:“哦,有包裹来了!”
    • Activity 把包裹交给它的 Window (mWindow.dispatchTouchEvent())。
    • Window 再把包裹交给 DecorView (mDecor.superDispatchTouchEvent())。
    • 至此,包裹正式进入“视图配送系统”。
  2. 自上而下分发 (递归进入 ViewGroup):

    • 包裹现在在 DecorView(根 ViewGroup)手上。

    • DecorView 的 dispatchTouchEvent() 方法被调用。

    • 关键决策点:  ViewGroup 在 dispatchTouchEvent() 中做两件重要事情:

      • a) 检查拦截 (onInterceptTouchEvent):

        • ViewGroup 问自己:“这个包裹是给我的,还是要转发给下属的?”(默认返回 false 表示不拦截)。
        • 如果 onInterceptTouchEvent() 返回 true,表示:“这个包裹我截下了!后续事件(MOVEUP)都直接找我,不再问下属了!”(拦截)。
        • 拦截通常发生在用户开始滑动 (MOVE 事件) 时,比如 ScrollView 发现用户意图是滑动而不是点击内部按钮。
      • b) 向下分发给子 View (如果没拦截):

        • 如果不拦截(或还没到拦截的时机),ViewGroup 需要找出“哪个子 View 在触摸点下面并且愿意接收包裹”。

        • ViewGroup 按照子 View 绘制的倒序(后绘制的在上面)遍历所有子 View:

          • 检查触摸点坐标是否在子 View 的区域内 (frame.contains(x, y))。
          • 检查子 View 是否可见 (VISIBLE) 且没有动画正在执行。
          • 如果满足条件,调用该子 View 的 dispatchTouchEvent() ,把包裹交给它。
        • 如果这个子 View 也是一个 ViewGroup,那么它重复步骤 2:检查自己是否拦截,再向下分发。

        • 如果这个子 View 是一个普通的 View,那么进入步骤 3。

        • 如果找到了一个愿意接收 ACTION_DOWN 的子 View,ViewGroup 会记住它(存储在一个 TouchTarget 链表中),后续事件 (MOVEUP) 会直接尝试分发给它,不再遍历所有子 View(除非事件被取消或新的 ACTION_DOWN 发生),这提高了效率。

        • 如果没有任何子 View 愿意消费 ACTION_DOWN 事件,那么 ViewGroup 会自己尝试处理(进入步骤 3)。

  3. 最终签收处理 (View):

    • 包裹终于到达一个叶子节点 View(如 Button)的 dispatchTouchEvent() 方法。

    • 该 View 会按顺序尝试“签收”包裹:

      • a) 优先给 OnTouchListener:  如果给这个 View 设置了 setOnTouchListener(),并且 onTouch() 方法返回了 true(表示“我签收了,不用再处理了”),那么流程到此结束,View 自己的 onTouchEvent() 不会被调用。

      • b) 自己处理 (onTouchEvent):  如果没有 OnTouchListener 或 onTouch() 返回了 false(表示“我没签收,或者签收了但还想让内部再处理一下”),那么就会调用 View 自己的 onTouchEvent() 方法。

        • 在 onTouchEvent() 里,View 根据事件类型做具体响应:

          • 对于 ButtonACTION_DOWN 改变背景色,ACTION_UP 触发点击事件 (OnClickListener.onClick())。
          • 对于 TextView:可能只是改变按下状态或不做响应。
        • 如果 onTouchEvent() 返回 true,表示“这个包裹我签收并处理了!”。

        • 如果返回 false,表示“这个包裹我不要,退回去!”。

  4. 自下而上回溯 (消费与否):

    • 不管是在 ViewGroup 的 dispatchTouchEvent() 中(自己尝试处理或拦截后处理),还是在 View 的 dispatchTouchEvent() / onTouchEvent() 中,只要有一个环节返回 true,就表示“包裹已签收”,事件分发流程就此结束,结果返回 true

    • 如果某个 ViewGroup 分发给所有子 View 后,没有任何子 View 消费事件(所有子 View 的 dispatchTouchEvent() 都返回 false),并且 ViewGroup 自己的 onTouchEvent() 也返回 false(表示它自己也不要),那么这个 ViewGroup 的 dispatchTouchEvent() 会返回 false

    • 这个 false 会沿着视图树向上传递(回溯):

      • 父 ViewGroup 收到 false,知道下属没人要,就会尝试用自己的 onTouchEvent() 处理。
      • 如果父 ViewGroup 的 onTouchEvent() 也返回 false,它也会向上返回 false
    • 最终,如果回溯到 DecorView 甚至 Activity,它们自己的 onTouchEvent() 都没有消费事件(返回 false),那么 Activity 的 dispatchTouchEvent() 最终返回 false,系统就知道这次触摸事件没有被任何应用内的 View 消费。

核心方法回调总结:

方法名所属类调用时机/作用返回值意义
dispatchTouchEvent()Activity最先接收事件,交给 Window。最终表示事件是否被消费。
dispatchTouchEvent()ViewGroup核心枢纽!  1. 调用 onInterceptTouchEvent() 决定是否拦截。2. 若不拦截,遍历子 View 调用其 dispatchTouchEvent()。3. 若子 View 都不消费或自己拦截了,调用自身 onTouchEvent()返回 true:事件被消费(自己或子 View)。返回 false:事件未被消费。
onInterceptTouchEvent()ViewGroup仅 ViewGroup 有!  在 dispatchTouchEvent() 内部调用,询问是否要拦截事件,停止向下分发,后续事件自己处理。返回 true:拦截,事件流后续事件直接给此 ViewGroup 的 onTouchEvent()。返回 false:不拦截(默认)。
dispatchTouchEvent()View1. 若有 OnTouchListener 且其 onTouch() 返回 true,则消费。2. 否则调用自身 onTouchEvent()返回 true:事件被消费(Listener 或 onTouchEvent)。返回 false:事件未被消费。
onTouchEvent()View/ViewGroup最终处理者!  被 dispatchTouchEvent() 调用(当没有 Listener 消费或 ViewGroup 拦截/自己处理时)。实现具体的触摸响应逻辑(如点击、滑动)。返回 true:事件在此被消费。返回 false:事件未被消费,向上回溯。
onTouch()OnTouchListener通过 View.setOnTouchListener() 设置。在 View 的 dispatchTouchEvent() 中优先于 onTouchEvent() 被调用。返回 true:消费事件,阻止 onTouchEvent() 被调用。返回 false:不消费,继续执行 onTouchEvent()
onClick()OnClickListener通过 View.setOnClickListener() 设置。在 onTouchEvent() 处理 ACTION_UP 且满足点击条件时触发。N/A

源码调用流程简述 (简化版,聚焦关键点):

  1. 起点 (Activity):

    // Activity.java
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction(); // 可选回调,通知用户开始交互
        }
        if (getWindow().superDispatchTouchEvent(ev)) { // 交给 Window
            return true; // Window 及其下的视图树消费了事件
        }
        return onTouchEvent(ev); // 如果都没消费,Activity 自己尝试处理
    }
    
  2. Window 到 DecorView (PhoneWindow 是常见实现):

    // PhoneWindow.java
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event); // 转给 DecorView
    }
    
  3. DecorView 分发 (FrameLayout 子类):

    // DecorView.java
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event); // 调用 ViewGroup 的 dispatchTouchEvent
    }
    
  4. ViewGroup 核心分发逻辑 (ViewGroup.dispatchTouchEvent):

    // ViewGroup.java
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // ... (处理事件有效性、辅助功能等)
    
        boolean intercepted = false;
        // 关键点 1: 检查是否要拦截 (ACTION_DOWN 或 已有 TouchTarget)
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev); // 调用拦截方法
                ev.setAction(action); // 恢复 action (防止被拦截方法修改)
            } else {
                intercepted = false;
            }
        } else {
            // 不是 DOWN 且没有 TouchTarget (说明没人消费 DOWN),直接拦截后续事件
            intercepted = true;
        }
    
        // ... (检查取消事件等)
    
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
    
        // 关键点 2: 如果不拦截 && 是 DOWN 事件 (或预取事件) && 事件在窗口内 && 没有 TouchTarget
        if (!canceled && !intercepted && (actionMasked == MotionEvent.ACTION_DOWN || ...)) {
            // ... (处理辅助焦点)
    
            // 关键点 3: 倒序遍历子 View 寻找能接收事件的
            final View[] children = mChildren;
            for (int i = childrenCount - 1; i >= 0; i--) {
                final View child = children[i];
                // 检查 child 是否可见、坐标是否在 child 区域内
                if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) {
                    continue;
                }
                // 检查 child 是否在 TouchDelegate 内 (略)
    
                // 关键点 4: 分发给子 View!
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                    // 子 View 消费了!
                    // ... (记录这个 child 为 newTouchTarget, 添加到 mFirstTouchTarget 链表)
                    break; // 找到了一个消费的 child, 跳出循环
                }
            }
        }
    
        // 关键点 5: 处理 TouchTarget (mFirstTouchTarget)
        if (mFirstTouchTarget == null) {
            // 没有子 View 消费 || 被拦截了 -> 自己处理 (调用父类 View.dispatchTouchEvent -> onTouchEvent)
            handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
        } else {
            // 有 TouchTarget (即有子 View 消费了之前的 DOWN)
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true; // 已经发给新目标了
                } else {
                    // 关键点 6: 分发给之前消费 DOWN 的 TouchTarget (子 View)
                    if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                }
                target = next;
            }
        }
        return handled; // 返回事件是否被消费
    }
    // 辅助方法: 实际分发事件给特定 child 或自己
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
        if (child == null) {
            // 调用自己的 super.dispatchTouchEvent (即 View.dispatchTouchEvent)
            return super.dispatchTouchEvent(transformedEvent);
        } else {
            // 调用 child 的 dispatchTouchEvent
            return child.dispatchTouchEvent(transformedEvent);
        }
    }
    
  5. View 的处理 (View.dispatchTouchEvent):

    // View.java
    public boolean dispatchTouchEvent(MotionEvent event) {
        // ... (处理辅助功能等)
    
        // 关键点 1: 优先给 OnTouchListener
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true; // Listener 消费了
        }
    
        // 关键点 2: Listener 没消费 -> 调用自己的 onTouchEvent
        if (!result && onTouchEvent(event)) {
            result = true; // onTouchEvent 消费了
        }
    
        // ... (清理等)
        return result;
    }
    
    // View.onTouchEvent() - 非常长,处理各种状态 (按下、移动、抬起、长按、点击等)
    public boolean onTouchEvent(MotionEvent event) {
        // ... (处理是否可用、点击检测、长按检测、滑动检测等)
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                // 记录按下状态, 可能触发按下效果, 发送长按检测延迟消息
                break;
            case MotionEvent.ACTION_MOVE:
                // 检查是否滑出边界 (取消点击/长按), 处理滑动
                break;
            case MotionEvent.ACTION_UP:
                // 移除长按检测消息
                // 如果满足点击条件 (在区域内抬起), 触发 performClick() -> onClickListener.onClick()
                // 移除按下效果
                break;
            case MotionEvent.ACTION_CANCEL:
                // 移除按下效果, 取消长按检测
                break;
        }
        return true; // 大多数可点击的 View 在按下时都返回 true
    }
    

通俗比喻总结:

  1. 快递到达 (触摸发生):  快递员(系统)把包裹(ACTION_DOWN 事件)送到公司总部(Activity)。

  2. 总部转交:  总部前台(Window)签收,转给大楼前台(DecorView)。

  3. 大楼派送:  大楼前台(DecorView,是个大部门经理 ViewGroup)拿到包裹。

    • 他先看自己要不要(onInterceptTouchEvent?默认不要)。
    • 他查大楼地图(子 View 列表),从最顶层的办公室(最后绘制的子 View)开始问:“谁在收件地址(触摸点)附近?这个包裹是你的吗?”(倒序遍历子 View)。
  4. 部门处理:  如果包裹是给某个子部门(另一个 ViewGroup)的:

    • 该子部门经理重复步骤 3:自己要不要?再问自己手下。
  5. 员工签收:  包裹最终送到一个普通员工(View)桌上。

    • 员工先问秘书(OnTouchListener):“你帮我签吗?”如果秘书签了 (onTouch 返回 true),流程结束。

    • 秘书不签,员工自己拆包处理 (onTouchEvent):

      • 如果是按下的包裹 (ACTION_DOWN),他亮起工位灯(按下状态),并设置一个 500ms 的闹钟(长按检测)。
      • 如果是移动的包裹 (ACTION_MOVE),他检查手有没有滑出工位范围(取消事件)。
      • 如果是抬起的包裹 (ACTION_UP),他关掉工位灯,如果手没滑出去且按下时间短,就大喊“我点了!”(触发 onClick),关掉闹钟。
    • 如果员工处理了包裹(onTouchEvent 返回 true),他就告诉经理“我签收了”。

  6. 向上汇报:

    • 如果员工签收了 (true),经理就告诉大楼前台“有人签了”,大楼前台告诉总部前台“有人签了”,总部告诉快递员“妥投”。
    • 如果员工不签 (false),包裹退回给经理。经理想:“手下没人要?那我看看要不要?”(调用自己的 onTouchEvent)。如果经理也不要,包裹继续退回给大楼前台... 一直退到总部。如果总部 (Activity) 也不要 (onTouchEvent 返回 false),快递员就知道“无人签收”。
  7. 后续包裹:  同一个手势的后续包裹 (ACTION_MOVEACTION_UP) 通常会直接送给最开始签收 ACTION_DOWN 的那个人(或中途拦截的经理),不再重新问遍所有办公室(效率优化)。

关键要点:

  • ACTION_DOWN 是核心:  它决定了谁接收整个事件流。后续事件默认发给消费了 ACTION_DOWN 的 View(或拦截它的 ViewGroup)。
  • 责任链模式:  事件分发是典型的“责任链”模式,从顶层到底层,再从底层回溯到顶层。
  • 拦截权 (onInterceptTouchEvent):  ViewGroup 的杀手锏,可以中途截胡事件流。
  • 优先权 (OnTouchListener):  比 onTouchEvent 优先级更高。
  • 消费标志 (return true):  任何环节返回 true 都表示“我处理了,到此为止”。
  • 回溯机制:  如果子 View 不消费,父容器有机会自己处理。

理解了这个“快递配送”流程,处理滑动冲突(多个 ViewGroup 都想拦截)、自定义触摸行为就有了坚实的基础。记住:分发是自上而下 (dispatchTouchEvent + onInterceptTouchEvent),消费是自下而上 (onTouchEvent)