03_View的事件分发机制

496 阅读13分钟

View事件分发机制介绍

基本概念

在Android系统中,当用户与触摸屏进行交互时,如点击、触摸、滑动等,这些操作会产生一系列的事件,这些事件被封装在MotionEvent类中。MotionEvent是Android中处理触摸事件的核心类,它包含了触摸事件的所有相关信息,如事件类型、触摸位置、触摸时间等。

事件类型

  • ACTION_DOWN:表示用户的手指刚接触屏幕。这是触摸事件序列的开始。
  • ACTION_MOVE:表示用户的手指在屏幕上移动。这个事件会在用户手指在屏幕上移动时连续产生。
  • ACTION_UP:表示用户的手指从屏幕上松开。这是触摸事件序列的结束。

常用方法

以下是MotionEvent的一些常用方法和属性的Markdown语法格式:

  • getAction(): 获取事件的动作类型。
  • getX(): 获取事件在当前视图中的X坐标。
  • getY(): 获取事件在当前视图中的Y坐标。
  • getRawX(): 获取事件在屏幕中的X坐标。
  • getRawY(): 获取事件在屏幕中的Y坐标。

事件传递过程

事件传递是指当用户在屏幕上产生一个事件时,系统如何将这个事件分发到合适的View进行处理。Android事件传递主要通过以下三个方法来实现:

1. dispatchTouchEvent()

这是事件传递的入口方法。当一个触摸事件发生时,首先会调用dispatchTouchEvent()方法。这个方法存在于View、ViewGroup和Activity中,用于分发事件。

2. onInterceptTouchEvent()

此方法仅在ViewGroup中存在。ViewGroup可以通过onInterceptTouchEvent()方法拦截事件,决定是否将事件传递给其子View。如果返回true,则表示拦截事件,事件不会传递给子View,而是由当前ViewGroup处理。

3. onTouchEvent()

这是事件处理的最终方法。View或者ViewGroup接收到事件后,会调用onTouchEvent()方法来处理具体的事件。可以通过重写这个方法来实现自定义的事件处理逻辑。

事件分发机制的工作流程

  1. 当用户触摸屏幕时,事件首先传递到Activity的dispatchTouchEvent()方法。
  2. Activity的dispatchTouchEvent()方法会将事件传递给当前窗口的根View(通常是一个ViewGroup)。
  3. ViewGroup的dispatchTouchEvent()方法首先调用onInterceptTouchEvent()方法,判断是否拦截事件。
    • 如果onInterceptTouchEvent()返回false,事件将继续传递给子View的dispatchTouchEvent()方法。
    • 如果返回true,事件将由当前ViewGroup处理,调用其onTouchEvent()方法。
  4. 子View接收到事件后,依次调用其dispatchTouchEvent()onTouchEvent()方法进行处理。
  5. 如果子View不处理事件(onTouchEvent()返回false),事件会回传给父View进行处理,直到根View或者Activity。

常见的事件处理方法

1. 重写onTouchEvent()

通过重写onTouchEvent()方法,可以自定义View的事件处理逻辑。例如,实现一个自定义的按钮:

public class MyButton extends View {
    public MyButton(Context context) {
        super(context);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 按下事件
                setBackgroundColor(Color.RED);
                return true;
            case MotionEvent.ACTION_UP:
                // 抬起事件
                setBackgroundColor(Color.GREEN);
                return true;
        }
        return super.onTouchEvent(event);
    }
}

2. 使用OnTouchListener

除了重写onTouchEvent()方法,还可以通过设置OnTouchListener来处理触摸事件:

myButton.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 按下事件
                v.setBackgroundColor(Color.RED);
                return true;
            case MotionEvent.ACTION_UP:
                // 抬起事件
                v.setBackgroundColor(Color.GREEN);
                return true;
        }
        return false;
    }
});

需要注意的是,如果OnTouchListeneronTouch()方法返回true,表示事件已被处理,不会再传递给onTouchEvent()方法。


Android View 事件分发机制源码解析

我们这里将 View 事件分发机制源码分为三部分介绍:

  • 从 Activity 到 ViewGroup 的事件分发过程、
  • ViewGroup 对事件的分发
  • View 对事件的处理

1. Activity 到 ViewGroup 的事件分发过程

在 Android 中,触摸事件首先传递给当前处于活动状态的 Activity,它作为顶层容器负责将事件传递给内部的 Window 对象,进而再传递给实际的视图层次结构。

1. 事件传递至 Activity

触摸事件发生时,系统会调用 ActivitydispatchTouchEvent 方法来处理事件。

Activity 的 dispatchTouchEvent 方法

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}
  • onUserInteraction():当事件是ACTION_DOWN类型时,这个方法会被调用,用于通知Activity用户与其发生了交互。
  • getWindow().superDispatchTouchEvent(ev):这是事件传递的关键步骤。getWindow()方法返回当前ActivityWindow对象(通常是PhoneWindow的实例)。superDispatchTouchEvent方法用于将事件传递给Window对象进行进一步处理。

2. Window 对事件的分发

在 Android 中,Window 是一个抽象类,PhoneWindow 是其具体实现类,用于处理与窗口相关的具体逻辑。

PhoneWindow 的 superDispatchTouchEvent 方法

public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
  • PhoneWindowsuperDispatchTouchEvent 方法将事件传递给 mDecor 对象,这是 Activity 的根视图 DecorView 的实例。

3. DecorView 对事件的分发

DecorViewActivity 界面的顶层视图,继承自 FrameLayout。在 DecorView 中,superDispatchTouchEvent 方法调用了父类 ViewGroupdispatchTouchEvent 方法。

DecorView 的 superDispatchTouchEvent 方法

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}
  • super.dispatchTouchEvent(event) 将事件传递给 ViewGroupdispatchTouchEvent 方法,标志着事件进入视图层次结构进行分发。

2. ViewGroup 对事件的分发

在 Android 中,ViewGroup 对事件的分发主要在其 dispatchTouchEvent 方法中实现。该方法的核心逻辑是确定是否拦截触摸事件,以及将事件分发给子元素处理。

1. 确定是否拦截触摸事件

以下是简化版的 dispatchTouchEvent 方法,展示了确定是否拦截触摸事件的相关逻辑:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 获取事件类型
    final int actionMasked = ev.getActionMasked();

    // 如果是ACTION_DOWN事件,重置触摸状态并清除之前的触摸目标
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }

    final boolean intercepted;

    // 判断是否有子元素正在处理事件
    if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
        // 检查是否设置了禁止拦截标记位
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

        if (!disallowIntercept) {
            // 调用自身的onInterceptTouchEvent方法判断是否拦截事件
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(actionMasked); // 恢复事件类型,防止在onInterceptTouchEvent中被改变
        } else {
            // 如果子View请求不拦截,则不拦截
            intercepted = false;
        }
    } else {
        // 如果没有子元素处理事件且事件不是ACTION_DOWN,则默认拦截。
        // 执行这里的逻辑时,onInterceptTouchEvent不会被调用。
        intercepted = true;
    }

    // ... 省略后续处理代码

    return true; // 表示事件已被处理
}

1. 重置触摸状态

if (actionMasked == MotionEvent.ACTION_DOWN) {
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

当事件是 ACTION_DOWN(即触摸开始)时,会取消并清除之前的触摸目标,并重置触摸状态。这是为了确保新一轮的触摸事件处理能够从一个干净的状态开始。

注:resetTouchState 方法中会重置 FLAG_DISALLOW_INTERCEPT 标记位。

2. 判断是否拦截事件

final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(actionMasked);
    } else {
        intercepted = false;
    }
} else {
    intercepted = true;
}

分析这段代码之前需要先了解几个关键变量:

  • mFirstTouchTarget:当事件由子元素成功处理时,mFirstTouchTarget 会被赋值并指向子元素;mFirstTouchTarget != null 即表示有子元素成功处理了事件。
  • FLAG_DISALLOW_INTERCEPT:一般是子 View 调用 requestDisallowInterceptTouchEvent(true) 方法来设置的,表示子 View 不希望父 View 拦截事件。

这里的逻辑主要有以下几步:

  1. 判断是否有子元素正在处理事件:如果事件是 ACTION_DOWN 或者已有子 View(mFirstTouchTarget 不为空)正在处理事件,则进行进一步的拦截判断。

  2. 检查禁止拦截标记:如果没有设置 FLAG_DISALLOW_INTERCEPT 标记位,则调用 onInterceptTouchEvent 方法,询问 ViewGroup 自身是否要拦截事件。如果设置了此标记位且事件不是 ACTION_DOWN,则不拦截。

    注:因为 ACTION_DOWN 事件会重置 FLAG_DISALLOW_INTERCEPT 标记位,所以子 View 调用 requestDisallowInterceptTouchEvent 方法并不能影响 ViewGroup 对 ACTION_DOWN 事件的处理。

  3. 默认拦截:如果没有子元素处理事件且事件不是 ACTION_DOWN,则默认拦截事件。

    注:当没有子元素去处理事件时,则该事件序列中的其它事件(move、up 等事件)到来时,ViewGroup 会默认拦截事件,并且不再调用 onInterceptTouchEvent 方法。

2. 分发触摸事件给子元素

1. 事件分发逻辑

ViewGroup 不拦截事件(即 interceptedfalse)时,触摸事件会向子 View 分发,过程如下:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;

    // 如果不拦截事件
    if (!canceled && !intercepted) {
        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 (!child.canReceivePointerEvents()
                    || !isTransformedTouchPointInView(x, y, child, null)) {
                ev.setTargetAccessibilityFocus(false);
                continue;
            }

            newTouchTarget = getTouchTarget(child);

            if (newTouchTarget != null) {
                newTouchTarget.pointerIdBits |= idBitsToAssign;
                break;
            }

            resetCancelNextUpFlag(child);

            // 通过 dispatchTransformedTouchEvent 方法将触摸事件交给子元素处理
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                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 = ev.getX();
                mLastTouchDownY = ev.getY();
                // 在 addTouchTarget 方法中为 mFirstTouchTarget 赋值
                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                alreadyDispatchedToNewTouchTarget = true;
                break;
            }
        }
    }
    // 省略后续代码
}

从上面的代码可以看到,ViewGroup 向子 View 分发事件的过程主要有以下几步:

  1. 遍历子元素ViewGroup 会遍历所有子元素。
  2. 判断子元素是否能接收事件:检查子元素是否能够接收点击事件。如果不能接收事件,则跳过该子元素。

    子元素能否接收点击事件主要取决于两点:子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内。

  3. 分发事件给子元素:如果某个子元素满足条件,则调用 dispatchTransformedTouchEvent 方法将事件交给该子元素处理。
  4. 记录处理事件的子元素:如果 dispatchTransformedTouchEvent 方法返回 true,表示事件已被处理,ViewGroup 会记录下这个子元素,并调用 addTouchTarget 方法为 mFirstTouchTarget 赋值,以便后续的事件分发。

2. dispatchTransformedTouchEvent 方法

dispatchTouchEvent 中,触摸事件的分发是通过调用 dispatchTransformedTouchEvent 方法实现的。该方法会尝试将事件分发给特定的子元素。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    
    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }
    
    return handled;
}
  • 如果 child 不为 null,则调用 child.dispatchTouchEvent(event),将事件直接分发给子元素处理。
  • 返回值 handled 表示子元素是否成功处理了事件。

3. mFirstTouchTarget 赋值过程

mFirstTouchTarget 是一个用于跟踪当前处理触摸事件的子元素的成员变量。当一个子元素成功处理了触摸事件后,会将该子元素添加到 mFirstTouchTarget 中。

添加 TouchTarget 过程

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}
  • mFirstTouchTarget 是一个单链表结构。
  • TouchTarget.obtain(child, pointerIdBits) 用于创建一个新的 TouchTarget 实例,该实例包含了子元素和指针 ID 位。
  • target.next = mFirstTouchTarget 将新创建的 TouchTargetnext 指针指向当前的 mFirstTouchTarget
  • mFirstTouchTarget = targetmFirstTouchTarget 指向新创建的 TouchTarget,使其成为链表的头部。

4. 结束事件分发并返回

如果成功分发触摸事件给某个子元素处理,则会结束循环并返回。这是通过 break 语句实现的。

3. ViewGroup 自己处理事件

ViewGroup 拦截了事件(即 interceptedtrue),或者它的所有子元素都没有处理事件(即 mFirstTouchTargetnull),ViewGroup 会自己处理事件。这段代码如下:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    // ...省略无关代码

    if (mFirstTouchTarget == null) {
        // 没有触摸目标时,将其视为普通视图进行处理。
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    }
    // ...省略无关代码
}

在这里,dispatchTransformedTouchEvent 方法的第三个参数 childnull。前面有介绍这个方法,如果 childnull,会调用 super.dispatchTouchEvent 方法,而 ViewGroup 的父类是 View,所以实际调用的是 ViewdispatchTouchEvent 方法。无论事件是分发给子 View 还是 ViewGroup 自己处理,都会转到 ViewdispatchTouchEvent 方法。

dispatchTransformedTouchEvent 方法

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    
    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }
    
    return handled;
}
  • 如果 childnull,则调用 super.dispatchTouchEvent(event),即 ViewdispatchTouchEvent 方法。

小结

ViewGroup 事件的分发过程主要包括三个步骤:

  1. 确定是否拦截触摸事件:通过 onInterceptTouchEvent 方法判断 ViewGroup 是否拦截事件。
  2. 分发触摸事件给子元素:如果不拦截事件,会遍历子元素,检查并分发事件给能够处理事件的子元素。
  3. ViewGroup 自己处理事件:如果事件没有被子元素处理,或者 ViewGroup 拦截了事件,ViewGroup 会自己处理触摸事件。

3. View 对事件的处理

1. View 的 dispatchTouchEvent 方法

因为 View(不包含 ViewGroup)是一个单独的元素,它没有子元素无法向下传递事件,因此它只能自己处理事件。源码如下:

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    // ...省略无关代码
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        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;
        }
    }
    // ...省略无关代码
    return result;
}

从上面的源码可以看出 View 对点击事件的处理过程,首先会判断有没有设置 OnTouchListener。如果 OnTouchListener 中的 onTouch 方法返回 true,那么 onTouchEvent 就不会被调用,可见 OnTouchListener 的优先级高于 onTouchEvent,这样做的好处是方便在外界处理点击事件。

2. View 的 onTouchEvent 方法

处理 TouchDelegate

如果 View 设置有代理,那么还会执行 TouchDelegateonTouchEvent 方法,这个 onTouchEvent 的工作机制和 OnTouchListener 类似。

public boolean onTouchEvent(MotionEvent event) {
    // ...
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
    // ...
}

具体处理点击事件

onTouchEvent 方法对点击事件的具体处理如下所示:

public boolean onTouchEvent(MotionEvent event) {
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

    if (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 (mPerformClick == null) {
                            mPerformClick = new PerformClick();
                        }

                        if (!post(mPerformClick)) {
                            performClickInternal();
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_DOWN:
                // 处理按下事件
                break;
            case MotionEvent.ACTION_CANCEL:
                // 处理取消事件
                break;
            case MotionEvent.ACTION_MOVE:
                // 处理移动事件
                break;
        }
        return true;
    }
    return false;
}

从上面的代码来看,只要 View 的 CLICKABLELONG_CLICKABLE 有一个为 true,那么它就会消耗这个事件,即 onTouchEvent 方法返回 true,不管它是不是 DISABLE 状态。然后当 ACTION_UP 事件发生时,会触发 performClick 方法,如果 View 设置了 onClickListener,那么 performClick 方法内部会调用它的 onClick 方法,如下所示:

public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result;
}

View 的 LONG_CLICKABLE 属性默认为 false,而 CLICKABLE 属性是否为 false 和具体的 View 有关。确切的来说是可点击的 View 其 CLICKABLEtrue,不可点击的 View 其 CLICKABLEfalse,比如 Button 是可点击的,TextView 是不可点击的。通过 setClickablesetLongClickable 可以分别改变 View 的 CLICKABLELONG_CLICKABLE 属性。

另外,setOnClickListener 会自动将 View 的 CLICKABLE 设为 truesetOnLongClickListener 则会自动将 View 的 LONG_CLICKABLE 设为 true,这一点从源码中可以看出来,如下所示:

public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

public void setOnLongClickListener(@Nullable OnLongClickListener l) {
    if (!isLongClickable()) {
        setLongClickable(true);
    }
    getListenerInfo().mOnLongClickListener = l;
}

总结

通过上述分析,我们可以看到,Android的事件分发机制是一个层层传递的过程,从Activity到ViewGroup再到具体的View,每个层级都可以决定是否拦截事件并进行处理。理解这一机制对于开发自定义View和处理复杂的用户交互需求至关重要。希望本文对您理解Android事件分发机制有所帮助。

View系列文章

01_View基础知识

02_View的滑动

03_View的事件分发机制

04_View的工作流程

05_自定义View

05_自定义ViewGroup

06_View滑动冲突处理