Android事件分发逻辑--针对事件分发相关函数的讲解

何为事件分发?

声明:这是个人的学习笔记,刚学事件分发几天对事件分发的理解没有这么深请见谅 由于这篇文字是在飞书上写的所以一些颜色不对应,请见谅

事件 指的是屏幕触发事件 ------即Android中的TouchEvent/MotionEvent。每一次我们触摸屏幕,都会产生一连串的触摸事件,这些一连串的触摸事件合起来就是一个触摸事件序列。

触摸事件在Android官方API中由类MotionEvent来描述,不同的触摸事件对应不同的事件类型。事件类型分别有ACTION_DOWN、ACTION_UP、ACTION_MOVE、ACTION_CANCEL。(这里暂时不讨论多指触控)

那什么叫分发 呢?我们都知道Android是由View树进行渲染的。假设屏幕坐标为(11,11)的区域既属于一个LinearLayout,又属于LinearLayout下的一个Button,那我这次触碰所产生的触摸事件,是该给LinearLayout还是Button呢?当然,我们很确定这次触摸事件最终会被Button所处理。那触摸事件是怎么给到Button的呢?需要经过LinearLayout吗?怎样能让Button不处理呢?这就需要我们了解触摸事件(后文统称为事件)在View树上传递与消费的过程,这就是事件的分发。

以下是 Android 中 onTouchEvent 常见的事件类型及其特性的汇总表格,以及针对各事件的深度解析。

Android 触摸事件(MotionEvent)核心概览表

事件类型 (Action) 触发条件 (Trigger Condition) 触发频率 (Frequency) 对事件流的影响 核心用途
ACTION_DOWN 手指初次接触屏幕 仅 1 次 生死关口:若返回 false,后续事件不再传给此 View 记录起点坐标、重置状态、申明消费意向
ACTION_MOVE 手指在屏幕上滑动 多次 (高频) 持续更新,反映手指轨迹 计算位移、实现拖拽、判断是否触发滑动阈值
ACTION_UP 手指离开屏幕 仅 1 次 正常终点:标志整个手势序列圆满结束 触发点击事件 (onClick)、执行回弹动画
ACTION_CANCEL 事件流被父布局拦截 最多 1 次 非正常终点:强制终止当前 View 的事件处理 状态重置(如取消按钮高亮),防止逻辑出错

事件详细解析

ACTION_DOWN:事件流的"敲门砖"

  • 触发条件:这是所有触摸事件的源头。每当一根手指接触屏幕时触发。

  • 消费机制:这是 View 唯一一次能决定"要不要处理这个序列"的机会。

    • 如果你在 ACTION_DOWN 时返回了 false ,系统会认为你对这个任务不感兴趣,接下来的 MOVE UP 统统不会再发给你
    • 比喻:就像公司的派活,如果你拒绝了开头,那后续所有的进度汇报和结尾都没你的事了。

ACTION_MOVE:高频的"过程量"

  • 触发条件:只要手指按下后在屏幕上移动,甚至是微小的抖动。

  • 多次触发:它的触发频率极高(通常 16ms 或 8ms 一次,取决于屏幕刷新率)[每刷新一次屏幕就需要计算两次的位移差]。

  • 注意事项

    • 性能 :不要在 MOVE 里面写大量的计算逻辑、创建大量对象或进行数据库操作,否则会导致 UI 卡顿。
    • 阈值 (TouchSlop) :通常我们会判断滑动距离是否超过系统定义的 mTouchSlop(通常是 8dp 左右),来决定这是不是一次有效的"滑动",还是只是手指的轻微震颤。

ACTION_UP:完美的"收尾"

  • 触发条件:最后一根手指离开屏幕。

  • 逻辑处理

    • Android 的 onClick(点击监听)就是在 UP 里面判断的。如果手指按下到抬起的位移很小,且时长符合要求,系统就会在内部调用 performClick()
    • 它是释放资源、结束动画的最佳时机。

ACTION_CANCEL:无奈的"被截胡"

  • 触发条件 :这个事件比较特殊,它不是由用户直接触发的,而是由父布局产生的。

    • 典型场景 :你在一个 RecyclerView(列表)里按住了一个按钮,刚开始触发了 DOWN。但接着你向上滑动,父容器 RecyclerView 觉得你要滚动列表,于是它强行拦截了事件(onInterceptTouchEvent 返回 true)。
    • 此时,按钮会收到一个 ACTION_CANCEL,告诉它:"活儿被老板接管了,你洗洗睡吧。"
  • 重要性 :View 收到此事件时必须重置状态 。比如按钮本来是按压变色的,收到 CANCEL 后必须变回原色,否则按钮会一直卡在"按下"的状态。

也就是说事件从触摸开始,经历滑动,最后以手指抬起结果。也就是说DOWN事件是其他事件的开始没有DOWN事件就没有其他事件的发生

事件分发的关键函数

scss 复制代码
dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()

dispatchTouchEvent

负责分发事件,View和ViewGroup上有不同的表现

ViewGroup的dispatchTouchEvent会向子View分发事件,如果子View不处理再交给父View处理,此时也会调用ViewGroup父View的disptachTouchEvent方法(ViewGroup继承于View,此时调用的是父类的方法)

ViewGroup的dispatchTouchEvent

dispatchTouchEvent是一个返回 布尔值 的函数,通过返回临时变量handled值,默认赋值为false

对于DOWN,MOVE,UP事件的会进入不同的分发链

了解分发链前我们需要重点关注的几个变量

intercepted->判断是否需要拦截

ini 复制代码
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
        //这个标黄的标志位,是后面内部拦截法requestDisallowInterceptTouchEvent() 的关键
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
    //这里调用onInterceptTouchEvent方法
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); // restore action in case it was changed
    } else {
        intercepted = false;
    }
} else {

    intercepted = true;
}

mFirstTouchTarget->消费DOWN事件子View的位置

newTouchTarget->新手指的触摸位置

DOWN事件在dispatchTouchEvent主要流程

ini 复制代码
TouchTarget newTouchTarget = null;//每次循环前清空
boolean alreadyDispatchedToNewTouchTarget = false;//注意这个参数
final boolean canceled = resetCancelNextUpFlag(this)
        || actionMasked == MotionEvent.ACTION_CANCEL;
if (!canceled && !intercepted) {

    View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
            ? findChildWithAccessibilityFocus() : null;

    if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        final int actionIndex = ev.getActionIndex(); // always 0 for down
        final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                : TouchTarget.ALL_POINTER_IDS;

        removePointersFromTouchTargets(idBitsToAssign);

        final int childrenCount = mChildrenCount;
        if (newTouchTarget == null && childrenCount != 0) {
            final float x = ev.getXDispatchLocation(actionIndex);
            final float y = ev.getYDispatchLocation(actionIndex);
         
            final ArrayList<View> preorderedList = buildTouchDispatchChildList();
            final boolean customOrder = preorderedList == null
                    && isChildrenDrawingOrderEnabled();
            final View[] children = mChildren;
            //从右往左遍历
            for (int i = childrenCount - 1; i >= 0; i--) {
                final int childIndex = getAndVerifyPreorderedIndex(
                        childrenCount, i, customOrder);
                final View child = getAndVerifyPreorderedView(
                        preorderedList, children, childIndex);

              
                if (childWithAccessibilityFocus != null) {
                    if (childWithAccessibilityFocus != child) {
                        continue;
                    }
                    childWithAccessibilityFocus = null;
                    i = childrenCount;
                }

                if (!child.canReceivePointerEvents()
                        || !isTransformedTouchPointInView(x, y, child, null)) {
                    ev.setTargetAccessibilityFocus(false);
                    continue;
                }
//如果这个手指位置已经存在会直接跳出循环
                newTouchTarget = getTouchTarget(child);
                if (newTouchTarget != null) {
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                    break;
                }

//最关键代码
                resetCancelNextUpFlag(child);
                //这个方法会调用子View的dispatchTouchevent来判断是否消费事件
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                    // Child wants to receive touch within its bounds.
                    mLastTouchDownTime = ev.getDownTime();
                    if (preorderedList != null) {
                        for (int j = 0; j < childrenCount; j++) {
                            if (children[childIndex] == mChildren[j]) {
                                mLastTouchDownIndex = j;
                                break;
                            }
                        }
                    } else {
                        mLastTouchDownIndex = childIndex;
                    }
                    mLastTouchDownX = x;
                    mLastTouchDownY = y;
                    //关键调用函数
                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                    //开始提到的参数
                    alreadyDispatchedToNewTouchTarget = true;
                    break;
                }
                
                ev.setTargetAccessibilityFocus(false);
            }
            if (preorderedList != null) preorderedList.clear();
        }
//多指触控逻辑 :既然没找到新的 View 接收这根手指,就把这根手指分配给"最老"的那个负责人
        if (newTouchTarget == null && mFirstTouchTarget != null) {
            newTouchTarget = mFirstTouchTarget;
            while (newTouchTarget.next != null) {
                newTouchTarget = newTouchTarget.next;
            }
            newTouchTarget.pointerIdBits |= idBitsToAssign;
        }
    }
}
//如果DOWN事件没有被子View消费mFirstTouchTarget就为空此时由父View来处理
    if (mFirstTouchTarget == null) {
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
    //如果DOWN事件被子View消费
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            final TouchTarget next = target.next;
            //DOWN事件并且DOWN被子View消费情况下这里的alreadyDispatchedToNewTouchTarget就为true,返回handled值,防止重复分发
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                handled = true;
            } else {
               ......
    }

   
}
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    //给mFirstTouchTarget赋值 后面会对mFirstTouchTarget的值进行判断这个参数很重要
    mFirstTouchTarget = target;
    return target;
}

MOVE事件在dispatchTouchEvent主要流程

intercepted->判断是否需要拦截

mFirstTouchTarget->消费DOWN事件子View的位置

newTouchTarget->新手指的触摸位置

ini 复制代码
//如果前面的DOWN事件没有子View消费,这里会直接进入else分支
//即intercepted = true if (!canceled && !intercepted) {},巧妙的跳过了这里的遍历流程

final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
        //这个标黄的标志位,是后面内部拦截法requestDisallowInterceptTouchEvent() 的关键
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); 
    } else {
        intercepted = false;
    }
} else {

    intercepted = true;
}
ini 复制代码
//------前置参数  这是一个200行的大方法,不要忘了必要的参数
final boolean intercepted;
final boolean canceled = resetCancelNextUpFlag(this)
        || actionMasked == MotionEvent.ACTION_CANCEL;
 boolean alreadyDispatchedToNewTouchTarget = false;//注意这个参数
//------正式代码
//MOVE分发下:如果前面的DOWN事件没有子View消费,这里会调用ViewGroup的dispatchTouchEvent(也就是decorView的onTouchEvent方法最后调用activity的onTouchEvent方法)
    if (mFirstTouchTarget == null) {
    //这里不仅可以是DOWN的结果也可以是MOVE的结果(结合前文)
       1. handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
//这里MOVE分为两种情况,正常MOVE和MOVE事件被拦截,这里使用两种颜色的字体表示 灰色正常 绿色MOVE拦截    由于这篇文字是在飞书上写的所以颜色不对,请见谅.
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        //这里的循环是为了做多指触控,本来mFirstTouchTarget就是ViewGruop指向一个View的单向链表,由于有多指的情况,这样的链表有几对
        while (target != null) {
            final TouchTarget next = target.next;
            //只有在DOWN事件的情况下才有可能为true,所以MOVE事件不会到这里面去
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            2.    handled = true;
            } 
//DOWN事件分发所有可能的结果只能到这里 要么是1.  要么是2.           
            else {
            //这里有个误区,这里是不需要考虑前面DOWN事件没有被子View消费的情况的,因为已经被这段代码开头的if判断清除了,所以这里的intercepted是通过 intercepted = onInterceptTouchEvent(ev);这里得到的
            
            //resetCancelNextUpFlag这里用来避免子View出现的意外情况,一旦子View发生意外,不让子VieW消费事件 比如下面这些情况
            //当一个 View 正在被按下时,它突然被从父容器中移除了(detach)
            //或者 View 的状态发生了剧烈变化,导致系统认为它不应该再产生点击效果
                final boolean cancelChild = resetCancelNextUpFlag(target.child)
                        || intercepted;
                 //根据前面的target定向寻找子View,判断是否消费事件,这个就是我们之前一直说的mFirstTouchTarget
                 //如果这里cancelChild为true,会子View分发CANCEL终止分发
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
                //如果cancelChild为true置空链表
                if (cancelChild) {
                    if (predecessor == null) {
                        mFirstTouchTarget = next;
                    } else {
                        predecessor.next = next;
                    }
                    target.recycle();
                    target = next;
                    continue;
                }
            }
            //循环遍历操作
            predecessor = target;
            target = next;
        }
    }

}

UP事件在dispatchTouchEvent主要流程

MOVE事件与UP事件的流程高度重合唯一区别就是UP事件需要清空之前的状态便于迎接下一次事件的到来

scss 复制代码
// --- 这段代码处理 UP,但不处理 MOVE ---
if (canceled
        || actionMasked == MotionEvent.ACTION_UP // 【UP 走这里】
        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
    resetTouchState(); // 重置所有状态,清空链表
} 
// --- 这段处理多指抬起 ---
else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
    removePointersFromTouchTargets(idBitsToRemove);
}

CANCEL事件在dispatchTouchEvent主要流程

CANCEL事件的处理比较简单了,理解了前面MOVE的流程,这里就一目了然了

ini 复制代码
if (mFirstTouchTarget == null) {
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
} else {

    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            handled = true;
        } else {
//这里   cancelChild为true     
            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                    || intercepted;
            if (dispatchTransformedTouchEvent(ev, cancelChild,
                    target.child, target.pointerIdBits)) {
                handled = true;
            }
            //链表清除
            if (cancelChild) {
                if (predecessor == null) {
                    mFirstTouchTarget = next;
                } else {
                    predecessor.next = next;
                }
                target.recycle();
                target = next;
                continue;
            }
        }
        predecessor = target;
        target = next;
    }
}

dispatchTransformedTouchEvent

这个函数在dispatchTouchEvent里面被反复使用我们来看看长什么样

ini 复制代码
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

//如果cancel为true,分发CANCEL事件,对应之前MOVE分发代码的片段
/*
 final boolean cancelChild = resetCancelNextUpFlag(target.child)
                       || intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {
                    handled = true;}
*/                
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        //这里进行还原事件处理,防止给后面的View分发CANCEL事件
        event.setAction(oldAction);
        return handled;
    }
//-----------正常情况 可以看到都是调用dispatchTouchEvent方法的逻辑-----------------
    // Calculate the number of pointers to deliver.
    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;


    if (newPointerIdBits == 0) {
        return false;
    }

    final MotionEvent transformedEvent;
    if (newPointerIdBits == oldPointerIdBits) {
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);

                handled = child.dispatchTouchEvent(event);

                event.offsetLocation(-offsetX, -offsetY);
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event);
    } else {
        transformedEvent = event.split(newPointerIdBits);
    }

    // Perform any necessary transformations and dispatch.
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent);
    }

    // Done.
    transformedEvent.recycle();
    return handled;
}

我们发现这个方法也是通过handled变量返回布尔值

View的dispatchTouchEvent

csharp 复制代码
public boolean dispatchTouchEvent(MotionEvent event) {
    // If the event should be handled by accessibility focus first.
    if (event.isTargetAccessibilityFocus()) {
        // We don't have focus or no virtual descendant has it, do not handle the event.
        if (!isAccessibilityFocusedViewOrHost()) {
            return false;
        }
        // We have focus and got the event, then use normal event dispatch.
        event.setTargetAccessibilityFocus(false);
    }
    boolean result = false;

    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // 新手势开始,停止之前的嵌套滚动(如清理之前的滑动惯性)
        stopNestedScroll();
    }
 -----------------------------------关键代码---------------------------------------------
//onFilterTouchEventForSecurity:这是一个安全机制。如果系统检测到当前 View 上方覆盖了不安全的窗口(例如透明悬浮窗),为了保护隐私,可能会直接丢弃该事件。
    if (onFilterTouchEventForSecurity(event)) {
       
// 如果用户点在滚动条上并尝试拖拽,滚动条逻辑会先"截活",result 变为 true。       
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
/*优先级最高。
只要满足:
设置了 OnTouchListener。
View 处于 Enabled 状态(注意:禁用的 View 无法触发 onTouch)。
onTouch 返回了 true。
那么 result 为 true,后续的 onTouchEvent 就不会被执行了
*/            
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
---------------------------------------------------------------------------------------    
    
// 如果没有 View 消费事件,记录到校验器
    if (!result && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }

// 判定手势是否结束
    if (actionMasked == MotionEvent.ACTION_UP ||
            actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }

    return result;
}

我们看到View的dispatchTouchEvent方法多次调用了onTouchEvent方法再看看这个方法

View的onTouchEvent

关键代码部分:

可点击状态下的处理

只要可点击,就消费事件

swift 复制代码
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

// ... 在方法末尾 ...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    // ... switch case ...
    return true; // 只要 View 是可点击的,onTouchEvent 就会返回 true
}

这是 Android 事件分发的一个重要结论:一个可点击的 View(如 Button )在 onTouchEvent 中默认会消耗掉所有事件 。即使它是 DISABLED(禁用)状态,只要它是 CLICKABLE 的,它依然会返回 true,防止事件穿透到下层

ACTION_DOWN:
scss 复制代码
boolean isInScrollingContainer = isInScrollingContainer();
if (isInScrollingContainer) {
    mPrivateFlags |= PFLAG_PREPRESSED;
    // ... 发送一个延时 100ms 的 Tap 任务 ...
    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
    setPressed(true, x, y); // 不在滑动容器,立即显示按下状态
    checkForLongClick(...); // 开始长按计时
}
  • 如果 View 在 ScrollView 等滑动容器中,系统会延迟显示按下状态(100ms)。这是为了防止用户只是想滑动,结果手指一碰按钮就闪现按下效果。
  • 如果不在滑动容器,立即调用 setPressed(true) 变色,并开启长按定时器。
ACTION_MOVE:
scss 复制代码
if (!pointInView(x, y, touchSlop)) {
    // 只要手指移出了 View 的范围(加上系统允许的微小偏差 touchSlop)
    removeTapCallback();
    removeLongPressCallback();
    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
        setPressed(false); // 取消按下状态
    }
}

系统允许用户在点击时有轻微的晃动(touchSlop)。但一旦手指移出 View 范围,长按和点击逻辑都会被取消,按钮也会变回原来的颜色。

ACTION_UP:
scss 复制代码
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
    removeLongPressCallback(); // 移除长按计时
    // ...
    if (!post(mPerformClick)) {
        performClickInternal(); // 真正触发 OnClickListener.onClick() 的地方
    }
}
  • 优先级判定 :如果已经触发了长按(mHasPerformedLongPress 为 true),那么抬起时就不会再触发点击。
  • 异步执行 :使用 post(mPerformClick) 是为了让 View 先完成视觉上的状态切换(变回原色),然后再执行点击逻辑,避免点击任务阻塞了 UI 的状态更新。
ACTION_CANCEL
scss 复制代码
case MotionEvent.ACTION_CANCEL:
    if (clickable) {
        setPressed(false);
    }
    removeTapCallback();
    removeLongPressCallback();
    // ... 重置所有标记位 ...
    break;

当父容器(如 RecyclerView)拦截了事件时,子 View 会收到 CANCEL。此时必须清理掉所有的计时器和按下状态,否则 View 会一直显示"被按下"的颜色。

我们发现只要子View可以点击,就会返回true,这个判断下的事件状态只是执行不同的操作

为什么是这样的判定?如果是这样的话,事件会不会看起来很容易消费?

其实不然,首先我们要了解View的遍历逻辑,子View是根据根右左的顺序执行,并且是优先调用子View的onTouchEvent方法,如果遍历到的这个子View刚好是可点击的,就会消费事件。之后就不会遍历了,这符合我们对事件消费的期望。

并且在拦截的情况下也不矛盾

在DOWN时拦截:根本不会走遍历的操作,就不会调用子ViewonTouchEvent方法.

在MOVE时拦截:虽然在DOWN时形成了单向链表mFirstTouchTarget,在下一次分发MOVE事件的时候,通过

ini 复制代码
  final boolean cancelChild = resetCancelNextUpFlag(target.child)
                       || intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {
                    handled = true;}

给子View分发CANCEL事件(取消该View的按下状态,计时器等等),并且清空链表,之后这个View就不会接收事件了

**对于这一帧"物理上的 MOVE 事件",它的"移动数据"确实没有被任何 View 消费,被消费的仅仅是由它转化而来的"CANCEL 信号"。**

禁用状态下的处理

kotlin 复制代码
if ((viewFlags & ENABLED_MASK) == DISABLED ...) {
    // ... 抬起时清除按下状态 ...
    return clickable; 
}

如果 View 是禁用的(Disabled),它不会响应点击逻辑,但如果它原本是可点击的,它依然返回 true这解释了为什么点击一个禁用的按钮,下方的布局不会收到点击事件。

OnTouchListener和OnClickListener

定位到View的dispatchTouchEvent方法中的这段代码:

ini 复制代码
if (li != null && li.mOnTouchListener != null
        && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnTouchListener.onTouch(this, event)) {
    result = true;
}

if (!result && onTouchEvent(event)) {
    result = true;
}

在dispatchTouchEvent中,会判断View是否设置了OnTouchListener,如果设置了OnTouchListener,就会直接拦截事件,dispatchTouchEvent方法返回true,调用OnTouchListener的onTouch方法,而不会再触发后续的onTouchEvent方法。

再定位到View的onTouchEvent方法中的这段代码:

scss 复制代码
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    switch (action) {          
        case MotionEvent.ACTION_UP:
            if (!post(mPerformClick)) {
                performClickInternal();
            }

可以发现是在onTouchEvent方法中,判断了View是否可点击。若可点击且设置了OnClickListener,那么就会调用OnClickListener的onClick方法。

按优先级排序,OnTouchListener>OnTouchEvent>OnClickListener。若设置了OnTouchListener,则不会触发后面两者。OnClickListener在ACTION_UP后触发。

ViewGroup的onInterceptTouchEvent

scss 复制代码
public boolean onInterceptTouchEvent(MotionEvent ev) {
    // 条件 1: 事件来源必须是鼠标 (MOUSE)
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            // 条件 2: 动作必须是按下 (ACTION_DOWN)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            // 条件 3: 必须是鼠标左键 (BUTTON_PRIMARY)
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            // 条件 4: 点击的位置必须在滚动条的"滑块"上 (Scrollbar Thumb)
            && isOnScrollbarThumb(ev.getXDispatchLocation(0), ev.getYDispatchLocation(0))) {
        
        // 如果上述 4 个条件同时满足,返回 true,表示拦截该事件
        return true;
    }
    
    // 默认情况(比如手指触摸、点击非滚动条区域等)返回 false
    return false;
}

好文章推荐可以和这个做补充 Android斩首行动---滑动冲突作为移动开发,我们对滑动冲突可以说是屡见不鲜。这篇文章结合事件分发机制,对常见的滑动冲突 - 掘金

相关推荐
似霰2 小时前
Android 日志系统4——logd 写日志过程分析一
android
youyoulg3 小时前
利用Android Studio编译Android上可直接执行的二进制
android·ide·android studio
闽农3 小时前
Android ANR 调用栈溯源
android·anr
似霰3 小时前
Android 日志系统7——Android 平台日志丢失问题分析
android·log
·云扬·3 小时前
MySQL Undo Log 深度解析:事务回滚与 MVCC 的底层支柱
android·数据库·mysql
fareast_mzh3 小时前
如何检测、排除手机控制屏幕
android
左手厨刀右手茼蒿3 小时前
Flutter for OpenHarmony 实战:DartX — 极致简练的开发超能力集
android·flutter·ui·华为·harmonyos
codeGoogle3 小时前
2026 年 IM 怎么选?聊聊 4 家主流即时通讯方案的差异
android·前端·后端