【Android View】事件分发机制

参考文献

  • https://juejin.cn/post/6844904041487532045
  • https://www.jianshu.com/p/dea72779a6b7
  • https://yanfukun.com/read/anroid-dev1/event#68qq20

一. 基本概念

1. Activity的构成

在这里插入图片描述

2. 触摸事件的类型

触摸事件对应的是MotionEvent类,事件的类型主要有如下:

  • ACTION_DOWN:手指按下屏幕
  • ACTION_MOVE:手指在屏幕上移动
  • ACTION_UP:手指离开屏幕
  • ACTION_CANCEL:事件被取消(如被父View拦截)

3. 事件传递的三个阶段

分发(Dispatch):事件从顶层视图向下传递到目标视图的过程
拦截(Intercept):父视图决定是否拦截事件,不再向子视图传递
处理(Handle):最终接收事件的视图处理触摸事件

4. 视图层次与事件传递方向

Android界面是由视图树(View Tree)组成的,从Activity的根视图(通常是DecorView)开始,经过各级ViewGroup,最终到达叶子节点View。事件传递的基本方向是:

  • 分发阶段:自顶向下(从父到子)
  • 处理阶段:自底向上(从子到父)

二.事件传递核心机制理解

1.核心方法的作用及返回true/false/super的含义

在这里插入图片描述

2. 三个方法之间的关系(伪代码)

/**
 * 点击事件产生后
  */ 
  // 步骤1:调用dispatchTouchEvent()
  public boolean dispatchTouchEvent(MotionEvent ev) {
 
    boolean consume = false; //代表 是否会消费事件
 
    // 步骤2:判断是否拦截事件
    if (onInterceptTouchEvent(ev)) {
      // a. 若拦截,则将该事件交给当前View进行处理
      // 即调用onTouchEvent ()方法去处理点击事件
        consume = onTouchEvent (ev) ;
 
    } else {
 
      // b. 若不拦截,则将该事件传递到下层
      // 即 下层元素的dispatchTouchEvent()就会被调用,重复上述过程
      // 直到点击事件被最终处理为止
      consume = child.dispatchTouchEvent (ev) ;
    }
 
    // 步骤3:最终返回通知 该事件是否被消费(接收 & 处理)
    return consume;
   }

3. Activity/ViewGroup/View的差异

Activity:拥有分发和消费两个方法。
ViewGroup:拥有分发、拦截和消费三个方法。
View:拥有分发、消费两个方法。

4.事件传递的优先级

OnTouchListener > onTouchEvent > OnClickListener
如果OnTouchListener返回true,onTouchEvent和OnClickListener不会执行
onClickListener 是基于 onTouchEvent 实现的高级封装

5. 事件序列的特殊处理

对于一个完整的事件序列(从DOWN到UP),有几个重要的特性:

5.1 DOWN事件决定后续事件走向

  • 如果ViewGroup在DOWN事件中返回true拦截,则整个事件序列都会交由它处理
  • 子View将收到一个ACTION_CANCEL事件
  • 为什么这么设计?
    这是为了保证事件序列的一致性。如果 DOWN 事件被某个父容器处理,那么它应该拥有处理整个手势(如滑动、抬起)的权利。
    如果父容器不拦截 DOWN 事件,后续的 MOVE 和 UP 事件可能会被子 View 处理,这样手势逻辑就可能混乱。

5.2 MOVE事件的动态拦截

  • ViewGroup可以在MOVE事件中开始拦截(即使之前没拦截DOWN)
  • 此时子View会收到ACTION_CANCEL,后续事件交给ViewGroup
  • 应用场景举例:横向滑动的ViewPager与内部纵向滑动的RecyclerView嵌套。当用户开始横向滑动时,ViewPager会拦截事件并处理,此时内部的RecyclerView会收到ACTION_CANCEL;如果用户只轻微纵向滑动,则RecyclerView继续处理事件

5.3 事件一旦被消费,后续同序列事件优先交给消费者

  • 如果View消费了DOWN事件,后续MOVE和UP事件会优先交给它处理
  • 不再重新执行完整的分发流程
  • 这种设计提升了性能,也简化了事件处理逻辑.但 ViewGroup 仍保留中途拦截的权利(通过onInterceptTouchEvent)

6. 常见属性对事件传递的影响

6.1 clickable和focusable属性

当这些属性设置为true时:

  • View的onTouchEvent()方法会返回true(消费事件)
  • 即使没有设置OnClickListener,View也会消费触摸事件
  • 这会阻止事件向上传递给父View

6.2 enabled属性

enabled = false

  • OnTouchListener不会被调用
  • 但onTouchEvent()仍会被调用,只是不会有视觉反馈

6.3 visibility属性

  • 不可见的View(INVISIBLE)不会接收触摸事件
  • 完全隐藏的View(GONE)不会接收触摸事件,且不占用空间

6.4 FLAG_DISALLOW_INTERCEPT标志

  • 子View可以通过调用父ViewGroup的requestDisallowInterceptTouchEvent()方法,要求父View不要拦截事件:
  • 这个标志只对ACTION_DOWN之后的事件有效,因为ViewGroup在收到ACTION_DOWN时会重置这个标志。

7. 事件传递的核心原则

7.1 责任链模式

Android事件系统本质上是一个责任链模式的实现,事件沿着预定路径传递,直到被处理或到达终点

7.2 事件的完整性

触摸事件是成系列的,从DOWN开始,到UP结束。一旦某个View决定处理这个序列中的一个事件,后续事件会优先交给它。

7.3 返回值的意义

返回true:表示事件已被消费,不再传递
返回false:表示事件未被消费,继续传递

7.4 拦截的时机

通常在DOWN事件中不拦截,而在MOVE事件中根据需要动态拦截,这样可以保证事件的连贯性。

7.5 一些结论的补充

添加链接描述

三. 事件传递流程图

1. 基本流程图

┌─────────────────────────────────────┐
│           Activity                  │
│     boolean dispatchTouchEvent()    │
└───────────────┬─────────────────────┘
                ↓
┌─────────────────────────────────────┐
│         PhoneWindow/DecorView       │
│     boolean dispatchTouchEvent()    │
└───────────────┬─────────────────────┘
                ↓
┌─────────────────────────────────────┐
│          根ViewGroup                │
│     boolean dispatchTouchEvent()    │
└───────────────┬─────────────────────┘
                ↓
┌─────────────────────────────────────┐
│   是否拦截? onInterceptTouchEvent() │
└───────────────┬─────────────────────┘
                ↓
       ┌────────┴─────────┐
       ↓是               ↓否
┌─────────────┐   ┌─────────────────────────┐
│自己处理事件 │   │分发给子View/ViewGroup    │
└──────┬──────┘   └─────────────┬───────────┘
       ↓                        ↓
┌─────────────┐      ┌──────────┴──────────┐
│ onTouchEvent│      │子View.dispatchTouch │
└─────────────┘      └──────────┬──────────┘
                                ↓
                     ┌──────────┴──────────┐
                     │重复以上ViewGroup流程 │
                     └──────────┬──────────┘
                                ↓
                     ┌──────────┴──────────┐
                     │最终到达目标子View    │
                     │  boolean onTouchEvent│
                     └─────────────────────┘

注意点:

  • 事件回溯:如果目标View的onTouchEvent()返回false(不消费事件),事件会向上传递给父View的onTouchEvent()处理。如此层层上传,直到有View处理或到达Activity

2. 完整流程分析

在这里插入图片描述

3. 基于源码分析的完整流程

在这里插入图片描述

四. 源码分析

4.1 View类的dispatchTouchEvent源码分析

public boolean dispatchTouchEvent(MotionEvent event) {
    // 处理辅助功能相关
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
            mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    return onTouchEvent(event);
}
  • 如果设置了OnTouchListener且View是启用状态,先调用OnTouchListener
  • 如果OnTouchListener返回true,事件被消费,方法结束
  • 否则调用自身的onTouchEvent方法处理事件

4.2 View类的onTouchEvent源码分析

public boolean onTouchEvent(MotionEvent event) {
    final int viewFlags = mViewFlags;
    // 如果View是禁用的,但可点击,仍然消费事件,只是不响应
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        // 禁用状态下仍返回是否可点击
        return (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }
    // 如果设置了代理,交给代理处理
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
    // 检查View是否可点击(clickable、longClickable或contextClickable)
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    // 处理点击事件
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        // 移除长按回调
                        removeLongPressCallback();
                        // 只有不是长按才执行点击
                        if (!focusTaken) {
                            // 使用点击声音和震动反馈
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClickInternal();
                            }
                        }
                    }
                    // 重置状态
                    setPressed(false);
                }
                mIgnoreNextUpEvent = false;
                return true;
            case MotionEvent.ACTION_DOWN:
                // 记录按下状态
                if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                    mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                }
                mHasPerformedLongPress = false;
                if (!clickable) {
                    checkForLongClick(0, x, y);
                    return true;
                }
                if (performButtonActionOnTouchDown(event)) {
                    break;
                }
                // 处理可点击控件的按下效果
                if (isInScrollingContainer) {
                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPendingCheckForTap.x = event.getX();
                    mPendingCheckForTap.y = event.getY();
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // 不在滚动容器内,立即显示按下状态
                    setPressed(true, x, y);
                    checkForLongClick(0, x, y);
                }
                return true;
            case MotionEvent.ACTION_CANCEL:
                // 取消触摸,重置状态
                setPressed(false);
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                return true;
            case MotionEvent.ACTION_MOVE:
                // 处理移动事件,检查是否移出了View范围
                if (clickable) {
                    drawableHotspotChanged(x, y);
                }
                // 检查是否移出按下区域
                if (!pointInView(x, y, mTouchSlop)) {
                    // 移出区域,移除按下状态
                    removeTapCallback();
                    removeLongPressCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        setPressed(false);
                    }
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                }
                return true;
        }
        // 如果可点击,消费所有类型的事件
        return true;
    }
    // 不可点击,不消费事件
    return false;
}

onTouchEvent的核心逻辑:

  • 首先检查View是否启用(enabled)
  • 然后检查是否可点击(clickable/longClickable/contextClickable)
  • 根据不同的事件类型执行相应操作:
    DOWN:记录按下状态,启动长按检测
    MOVE:更新热点位置,检查是否移出范围
    UP:触发点击监听器
    CANCEL:重置所有状态
  • 关键点:如果View是可点击的,它会消费所有类型的触摸事件

4.3 ViewGroup类的dispatchTouchEvent源码分析

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();
    final float xf = ev.getX();
    final float yf = ev.getY();
    // 处理事件开始和结束的标记
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
        // 检查子View是否禁止父View拦截
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            // 调用onInterceptTouchEvent判断是否拦截
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // 恢复可能被修改的action
        } else {
            // 子View禁止拦截
            intercepted = false;
        }
    } else {
        // 已经开始处理事件序列且没有目标子View,直接拦截
        intercepted = true;
    }
    // 检查是否需要取消事件
    final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
    final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
    TouchTarget newTouchTarget = null;
    boolean alreadyDispatchedToNewTouchTarget = false;
    // 如果不取消且不拦截,尝试分发给子View
    if (!canceled && !intercepted) {
        // ACTION_DOWN时寻找可处理事件的子View
        if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)) {
            // 清除之前的触摸目标
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                mFirstTouchTarget = null;
            }
            // 寻找可以接收事件的子View
            final int childrenCount = mChildrenCount;
            if (childrenCount != 0) {
                // 从最上层的子View开始检查(绘制顺序的逆序)
                for (int i = childrenCount - 1; i >= 0; i--) {
                    final View child = getAndVerifyPreorderedView(preorderedList, children, i);
                    // 如果子View不能接收事件,跳过
                    if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
                        continue;
                    }
                    // 分发给子View
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                        // 子View处理了事件,记录为目标
                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
                        alreadyDispatchedToNewTouchTarget = true;
                        break;
                    }
                }
            }
        }
        // 如果没有找到新的触摸目标,检查已存在的触摸目标
        if (mFirstTouchTarget == null) {
            // 没有子View处理,传递给自己的onTouchEvent
            handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
        } else {
            // 根据已记录的触摸目标分发事件
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    // 分发给记录的子View
                    final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        // 需要取消子View的事件
                        if (target.pointerIdBits == oldPointerIdBits) {
                            removePointersFromTouchTargets(oldPointerIdBits);
                        }
                    }
                }
                target = next;
            }
        }
    }
    // 更新触摸状态
    if (mFirstTouchTarget == null) {
        // 没有子View处理,自己处理
        handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
    }
    return handled;
}

ViewGroup.dispatchTouchEvent的核心逻辑

  • 决定是否拦截事件(通过onInterceptTouchEvent)
  • 如果不拦截且是DOWN事件,寻找可处理事件的子View
  • 如果找到目标子View,记录下来,后续事件会直接分发给它
  • 如果没有子View处理或拦截了事件,调用自己的onTouchEvent处理

4.4 ViewGroup的onInterceptTouchEvent源码分析

public boolean onInterceptTouchEvent(MotionEvent ev) {
// ev.isFromSource(InputDevice.SOURCE_MOUSE) 的意思是判断当前的输入事件是否是由鼠标产生的。
// ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY):判断是否按下了鼠标的主按钮(通常是左键)。
// isOnScrollbarThumb(ev.getX(), ev.getY()):判断点击位置是否在滚动条的滑块上
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE) &&
            ev.getAction() == MotionEvent.ACTION_DOWN &&
            ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY) &&
            isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}
  • 只有鼠标在滚动条上点击时才拦截
  • 其他情况都返回false,不拦截事件

4.5 mFirstTouchTarget分析

mFirstTouchTarget 是 ViewGroup 中用于记录当前事件序列的目标子 View 的一个链表头节点。它是一个 TouchTarget 类型的对象,本质上是一个单链表结构。

  • 一旦某个子 View 成为 mFirstTouchTarget,后续事件将直接发送给它
  • ViewGroup 仍可以在 MOVE 中拦截,此时会清空 mFirstTouchTarget 并发送 ACTION_CANCEL。
  • mFirstTouchTarget 的工作流程
    当一个 ACTION_DOWN 事件到达 ViewGroup 时:
    ViewGroup 会遍历所有子 View,寻找可以接收事件的 View(根据点击区域、是否消费事件等判断)。
    如果某个子 View 消费了 ACTION_DOWN(返回 true),则 ViewGroup 会创建一个 TouchTarget 对象,并赋值给 mFirstTouchTarget。
    后续的 ACTION_MOVE 和 ACTION_UP 事件将直接发送给这个 TouchTarget 对象所指向的 View,不再重新分发给其他子 View。
    如果 **ViewGroup 决定中途拦截事件(在 onInterceptTouchEvent() 返回 true),则会清空 mFirstTouchTarget,**并发送 ACTION_CANCEL 给原目标 View。

五. 常见问题及解决方案

5.1 滑动冲突的解决

外部拦截法

由父容器决定是否拦截事件:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercepted = false;
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // DOWN事件通常不拦截,记录初始位置
            mLastX = ev.getX();
            mLastY = ev.getY();
            intercepted = false;
            break;
        case MotionEvent.ACTION_MOVE:
            // 根据移动方向判断是否拦截
            float deltaX = Math.abs(ev.getX() - mLastX);
            float deltaY = Math.abs(ev.getY() - mLastY);
            // 例如:水平滑动时拦截
            if (deltaX > deltaY && deltaX > mTouchSlop) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            intercepted = false;
            break;
    }
    // 记录上次触摸位置
    mLastX = ev.getX();
    mLastY = ev.getY();
    return intercepted;
}

外部拦截法的特点:

  • 父容器主动判断并拦截
  • 在ACTION_MOVE中动态决定是否拦截
  • 适合处理方向性冲突(如垂直与水平滑动)

内部拦截法

由子View决定是否允许父容器拦截

// 子View的dispatchTouchEvent方法
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // 请求父容器不要拦截事件
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            // 根据自己的判断,是否允许父容器拦截
            float deltaX = Math.abs(event.getX() - mLastX);
            float deltaY = Math.abs(event.getY() - mLastY);
            // 例如:如果是垂直滑动,允许父容器拦截
            if (deltaY > deltaX && deltaY > mTouchSlop) {
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
    }
    mLastX = event.getX();
    mLastY = event.getY();
    return super.dispatchTouchEvent(event);
}

同时,父容器需要处理:

// 父容器的onInterceptTouchEvent方法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    // 默认拦截除了ACTION_DOWN以外的所有事件
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        return false;
    }
    // 其余事件的拦截权交给子View通过requestDisallowInterceptTouchEvent控制
    return true;
}

内部拦截法的特点:

  • 子View主动控制父容器是否可以拦截,requestDisallowInterceptTouchEvent方法
  • 需要父容器配合,默认拦截除DOWN以外的所有事件
  • 适合子View需要优先处理事件的场景

5.2 点击与长按冲突

setOnLongClickListener返回true

// 通过设置长按监听器和点击监听器共存
view.setOnLongClickListener(v -> {
    // 处理长按
    return true; // 消费长按事件
});
view.setOnClickListener(v -> {
    // 处理点击
});

5.3 滑动与点击冲突

记录滑动的距离,在ACTION_MOVE和ACTION_UP方法中进行分支处理

// 使用距离阈值区分滑动和点击
private float mLastX, mLastY;
private static final int TOUCH_SLOP = ViewConfiguration.get(context).getScaledTouchSlop();
@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mLastX = event.getX();
            mLastY = event.getY();
            return true;
        case MotionEvent.ACTION_MOVE:
            float deltaX = Math.abs(event.getX() - mLastX);
            float deltaY = Math.abs(event.getY() - mLastY);
            if (deltaX > TOUCH_SLOP || deltaY > TOUCH_SLOP) {
                // 认为是滑动
                // 处理滑动逻辑
            }
            return true;
        case MotionEvent.ACTION_UP:
            deltaX = Math.abs(event.getX() - mLastX);
            deltaY = Math.abs(event.getY() - mLastY);
            if (deltaX < TOUCH_SLOP && deltaY < TOUCH_SLOP) {
                // 认为是点击
                performClick();
            }
            return true;
    }
    return super.onTouchEvent(event);
}

5.4 嵌套滚动冲突

通过水平移动的距离,大于Y轴移动距离和最小移动距离,来拦截左右滑动的情况

// 父容器通过方向判断是否拦截
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mLastX = ev.getX();
            mLastY = ev.getY();
            return false;
        case MotionEvent.ACTION_MOVE:
            float deltaX = Math.abs(ev.getX() - mLastX);
            float deltaY = Math.abs(ev.getY() - mLastY);
            // 如果是水平方向移动,父容器拦截
            if (deltaX > deltaY && deltaX > mTouchSlop) {
                return true;
            }
            return false;
    }
    return super.onInterceptTouchEvent(ev);
}

5.5 多点触控处理

通过event.getPointerId获取每个手指

@Override
public boolean onTouchEvent(MotionEvent event) {
    final int action = event.getActionMasked();
    switch (action) {
        case MotionEvent.ACTION_DOWN:
        case MotionEvent.ACTION_POINTER_DOWN:
            // 处理手指按下
            int pointerIndex = event.getActionIndex();
            int pointerId = event.getPointerId(pointerIndex);
            // 记录该手指的初始位置
            break;
        case MotionEvent.ACTION_MOVE:
            // 处理所有手指的移动
            for (int i = 0; i < event.getPointerCount(); i++) {
                pointerId = event.getPointerId(i);
                // 处理每个手指的移动
            }
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_POINTER_UP:
        case MotionEvent.ACTION_CANCEL:
            // 处理手指抬起
            pointerIndex = event.getActionIndex();
            pointerId = event.getPointerId(pointerIndex);
            // 清理该手指的状态
            break;
    }
    return true;
}

5.6 自定义ViewGroup解决冲突

public class CustomViewGroup extends ViewGroup {
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // 完全自定义事件分发逻辑
        boolean handled = false;
        // 实现自己的分发策略
        if (shouldInterceptEvent(ev)) {
            // 自己处理
            handled = onTouchEvent(ev);
        } else {
            // 找到合适的子View处理
            View targetChild = findTargetChild(ev);
            if (targetChild != null) {
                // 分发给子View
                handled = targetChild.dispatchTouchEvent(ev);
            }
        }
        return handled;
    }
    private boolean shouldInterceptEvent(MotionEvent ev) {
        // 自定义拦截逻辑
        return false;
    }
    private View findTargetChild(MotionEvent ev) {
        // 自定义查找目标子View的逻辑
        return null;
    }
}

5.7 手势检测器GestureDetector类辅助处理

public class MyView extends View {
    private GestureDetector mGestureDetector;
    public MyView(Context context) {
        super(context);
        initGestureDetector();
    }
    private void initGestureDetector() {
        mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                // 处理单击事件
                return true;
            }
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                // 处理滑动事件
                return true;
            }
            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                // 处理快速滑动事件
                return true;
            }
            @Override
            public boolean onDoubleTap(MotionEvent e) {
                // 处理双击事件
                return true;
            }
            @Override
            public void onLongPress(MotionEvent e) {
                // 处理长按事件
            }
        });
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 将事件交给GestureDetector处理
        return mGestureDetector.onTouchEvent(event) || super.onTouchEvent(event);
    }
}

GestureDetector的优势:
简化常见手势的识别
封装了时间和距离阈值的计算
支持单击、双击、长按、滑动、快速滑动等多种手势

6. 资料

通过网盘分享的文件:事件分发机制.xmind
链接: https://pan.baidu.com/s/1vnJlyraXW6by0P7vNm-N1A 提取码: 6i6b 复制这段内容后打开百度网盘手机App,操作更方便哦
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值