Android View 事件体系笔记(二):View 事件分发机制

Android View 事件体系笔记(二).png

声明:本文内容依据《Android开发艺术探索》的思路,基于 API 26 进行总结

一、View 事件分发机制概览

1.1 点击事件传递规则

定义:所谓点击事件的事件分发,其实就是对 MotionEvent 事件的分发过程。即当一个 MotionEvent 产生之后,系统需要把这个事件传递给一个具体的 View。
关于MotionEvent对象的含义及参数可以参考上一篇文章。

点击事件的分发由三个重要的方法共同完成:

  • public boolean dispatchTouchEvent(MotionEvent ev) (dispatch:处理)
    用来进行事件的分发。如果事件能够传递给当前 View ,一定会调用此方法,返回结果受当前 View 的 onTouchEvent 和下级 View 的 dispatchTouchEvent 方法的影响,表示是否消费当前事件。
  • public boolean onInterceptTouchEvent(MotionEvent event) (Intercept:拦截)
    用来判断是否拦截某事件,如果当前 View 拦截了某事件,那么在同一事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
  • public boolean onTouchEvent(MotionEvent event)
    在 dispatchTouchEvent 方法中调用,用来处理点击事件,返回结果表示是否消费当前事件,如果不消费,则在同一个事件序列中,当前 View 无法再次接收到事件。

伪代码表明三个方法的关系:

// 事件传递其实是MotionEvent对象的传递
public boolean dispatchTouchEvent(MotionEvent event){
    // 标记是否消费事件
    boolean consume = false;
    // 判断当前 View 是否拦截此事件,拦截则进一步处理
    if(onInterceptTouchEvent(event)){
        consume = onTouchEvent(event);
    }else {
        // 不拦截则传递到子 View 接着判定
        consume = child.dispatchTouchEvent(event);
    }
    return consume;
}
三个重要方法.png

View 设置 OnTouchListener,回调 onTouch 方法,如果 onTouch 返回 false,则当前 View 的 onTouchEvent 方法会被调用,如果 true,则被拦截。说明 OnTouchListener 优先级高于 onTouchEvent 。
在 onTouchEvent 方法中,如果当前设置的有 OnClickListener,那么它的 onClick 方法会被调用。OnClickListener 优先级最低,处于事件传递的尾端。

1.2 点击事件传递顺序

顺序: Activity --> ViewGroup --> View
这里借用一张图来说明:

事件分发流程图.png

简单解释一下这张图:

  • 三个重要角色 Activity、ViewGroup、View
  • 三个角色包含两个重要函数:dispatchTouchEvent() 和 onTouchEvent()ViewGroup 多了一个 onInterceptTouchEvent() 用来判断是否拦截当前事件
  • dispatchTouchEvent() 用来传递点击事件,onInterceptTouchEvent() 用来判断是否拦截当前事件,onTouchEvent() 用来处理是否消费当前事件
  • 如果 dispatchTouchEvent() 返回 false 则传递给上级 View 或 Activity,如果 true 则传递事件
  • 如果某 View 的 onTouchEvent 返回 false,则它的父容器的 onTouchEvent 会被调用,依此类推。如果都返回 false,交给 Activity 处理。

二、基于 Android 8.0 源码分析View 事件分发机制

2.1 事件最先传递给 Activity,由其 dispatchTouchEvent 进行派发

也就是上面流程图的第一部分


ActivityToViewGroup.png

android.app.Activity.java

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            // 空方法
            onUserInteraction();
        }
        // 事件交给 Activity 所属的 Window 进行分发,返回 true 事件结束
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        // 如果没有 View 处理事件,调用 Activity 的 onTouchEvent
        return onTouchEvent(ev);
    }
2.2 Window 会将事件传递给 decor view,一般是当前页面的底层容器(setContentView 所设置的 View 的父容器)通过 Activity.getWindow.getDecorView() 可以获得。

(1)Window 是一个抽象类,其 superDispatchTouchEvent 也是一个抽象方法。所以要找到其实现类。

Window 类源码描述(API 26).png

通过 Window 源码描述可知其实现类为 PhoneWindow,描述大意为:Window 类可以控制顶级 View 的外观和行为策略,它的唯一实现位于 android.view.PhoneWindow 中,当你要实例化这个 Window 类的时候,你并不知道它的细节,因为这个类会被重构,只有一个工厂方法可以使用。
(2)PhoneWindow 类是 Window 唯一的实现类,前者直接将事件传递给了 DecorView

PhoneWindow 类 package com.android.internal.policy;

@Override
public boolean superDispatchKeyEvent(KeyEvent event) {
    return mDecor.superDispatchKeyEvent(event);
}

PhoneWindow 类的 mDecor 对象

    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;
    ...
    @Override
    public final View getDecorView() {
        if (mDecor == null || mForceDecorInstall) {
            installDecor();
        }
        return mDecor;
    }

通过 ((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0); 可以获取 Activity 所设置的 View,这个 mDecor 就是 getWindow().getDecorView() 返回的 View,通过 setContentView 设置的 View 是它的一个子 View。目前事件由 mDecor.superDispatchKeyEvent(event) 传递到了 DecorView 这里。

(3)DecorView 是整个Window界面的最顶层View,包含通知栏,标题栏,内容显示栏(就是 Activity setContentView 设置的布局 View)三块区域

public class DecorView extends FrameLayout

    public boolean superDispatchKeyEvent(KeyEvent event) {
        ...
        // 传递给父类的 dispatchKeyEvent
        return super.dispatchKeyEvent(event);
    }

DecorView 的父类为 FrameLayout 且是父 View,事件会一层层传递最终会传递给 View。到这时,事件已经传递到顶级 View 了,也就是在 Activity 中通过 setContentView 设置的 View。顶级 View 也叫根 View,一般来说都是 ViewGroup。

2.3 顶级 View 对点击事件的分发过程

点击事件达到顶级 View (一般是一个 ViewGroup)以后,会调用 ViewGroup 的 dispatchTouchEvent 方法,对应流程图为:

ViewGroup Start.png

之后的逻辑是这样的:
如果顶级 ViewGroup 拦截事件即 onInterceptTouchEvent 返回 true,则事件由 ViewGroup 处理,这时如果 ViewGroup 的 mOnTouchListener 被设置,则 onTouch 会被调用,否则 onTouchEvent 会被调用。
在 onTouchEvent 中,如果设置了 mOnClickListener,则 onClick 会被调用。
如果顶级 View 不拦截事件,则事件会传递给子 View 并调用其 dispatchTouchEvent,接下来会一直处理到结束。
ViewGroup 分发过程.png

package android.view; ViewGroup
public boolean dispatchTouchEvent(MotionEvent ev) {...} 方法源码

(1)当前 ViewGroup 是否拦截点击事件

// 检查拦截。
final boolean intercepted;
// Step 1 判断按下或者mFirstTouchTarget:
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    // Step 2 判断 mGroupFlags 和 FLAG_DISALLOW_INTERCEPT
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); // 动作改变时重置事件状态
    } else {
        intercepted = false;
    }
} else {
    // 没有触摸目标且不是最初的按下事件,所以
    // 该 ViewGroup 继续处理事件
    intercepted = true;
}

Step 1 判断按下或者mFirstTouchTarget:if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null): 判断当前动作是否为 MotionEvent.ACTION_DOWN 按下,或者 mFirstTouchTarget != null。mFirstTouchTarget 通过查看后面逻辑可以看出指的是如果事件由 ViewGroup 的子元素成功处理,则 mFirstTouchTarget 会被赋值并指向子元素。
也就是说,当 ViewGroup 不拦截事件并将事件交给子 View 去成功处理,ViewGroup 不会再处理除 MotionEvent.ACTION_DOWN 以外的事件。因为此时 mFirstTouchTarget 的值不为 null,同时 MotionEvent.ACTION_MOVE 和 MotionEvent.ACTION_UP 不会再经历 ViewGroup 的 onInterceptTouchEvent(ev) 方法。
Step 2 判断 mGroupFlags 和 FLAG_DISALLOW_INTERCEPT: mGroupFlags 参数不用理会。FLAG_DISALLOW_INTERCEPT 标记一旦被设置,ViewGroup 将无法拦截除 ACTION_DOWN 以外的事件。这个标记的设置一般是子 View 通过 ViewGroup 的 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {...} 方法来设置的。每次按下以后都会清除和重置标记:

// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Throw away all previous state when starting a new touch gesture.
    // The framework may have dropped the up or cancel event for the previous gesture
    // due to an app switch, ANR, or some other state change.
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

这个方法是在第(1)步之前的,由于每次重新按下都会清除标记,所以 ViewGroup 总是能调用onInterceptTouchEvent来判断是否拦截事件。如果事件不为 ACTION_DOWN 且其子 View 设置了标记,ViewGroup 就不会再拦截事件而是直接交给子 View 去处理。
结论:

  • 当 ViewGroup 决定拦截事件,则后续不会再调用 onInterceptTouchEvent 方法,因为 Step 1中条件 actionMasked != MotionEvent.ACTION_DOWN 并且 mFirstTouchTarget == null 故而不会再跑里面的方法。
  • FLAG_DISALLOW_INTERCEPT 标记的作用是让 ViewGroup 不再拦截事件,当然前提是 ViewGroup 不拦截 ACTION_DOWN 事件。可以利用这个方法去解决滑动冲突。

(2)ViewGroup 不拦截事件向下分发的过程

if (newTouchTarget == null && childrenCount != 0) {
    // Step 1:获取点击坐标位置
    final float x = ev.getX(actionIndex);
    final float y = ev.getY(actionIndex);
    // Find a child that can receive the event.
    // Scan children from front to back.
    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
    final boolean customOrder = preorderedList == null
            && isChildrenDrawingOrderEnabled();
    final View[] children = mChildren;
     // Step 2:遍历 ViewGroup 所有子元素
    for (int i = childrenCount - 1; i >= 0; i--) {
        final int childIndex = getAndVerifyPreorderedIndex(
                childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(
                preorderedList, children, childIndex);

        // 如果有一个子 View 可以被点击并且我们需要它获得点击事件,
        // 会让它首先获得点击事件,如果该 View 不处理则会执行正常的调度。
        if (childWithAccessibilityFocus != null) {
            if (childWithAccessibilityFocus != child) {
                continue;
            }
            childWithAccessibilityFocus = null;
            i = childrenCount - 1;
        }
        // Step 3:判断是否可以接收点击事件(判断方法为是否在执行动画),
        // 或者坐标是否坐落在子元素的区域内
        if (!canViewReceivePointerEvents(child)
                || !isTransformedTouchPointInView(x, y, child, null)) {
            ev.setTargetAccessibilityFocus(false);
            continue;
        }

        newTouchTarget = getTouchTarget(child);
        if (newTouchTarget != null) {
            // Child is already receiving touch within its bounds.
            // Give it the new pointer in addition to the ones it is handling.
            newTouchTarget.pointerIdBits |= idBitsToAssign;
            break;
        }

        resetCancelNextUpFlag(child);
         // Step 4:进行判断,如果 child 不为 null,则 child 进行 dispatchTouchEvent。反之,则调用父 View 的 dispatchTouchEvent。
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            // Child wants to receive touch within its bounds.
            mLastTouchDownTime = ev.getDownTime();
            if (preorderedList != null) {
                // childIndex points into presorted list, find original index
                for (int j = 0; j < childrenCount; j++) {
                    if (children[childIndex] == mChildren[j]) {
                        mLastTouchDownIndex = j;
                        break;
                    }
                }
            } else {
                mLastTouchDownIndex = childIndex;
            }
            mLastTouchDownX = ev.getX();
            mLastTouchDownY = ev.getY();
            // Step 5:结合上方 if 返回 true 表示子元素的 dispatchTouchEvent 处理成功,
            // mFirstTouchTarget 就会被赋值同时跳出 for 循环 
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }

        // The accessibility focus didn't handle the event, so clear
        // the flag and do a normal dispatch to all children.
        ev.setTargetAccessibilityFocus(false);
    }
    if (preorderedList != null) preorderedList.clear();
}

Step 1: 只是记录点击的坐标。
Step 2: 遍历 ViewGroup 所有子元素。
Step 3: canViewReceivePointerEvents 该子 View 是否正在播放动画。

    private static boolean canViewReceivePointerEvents(@NonNull View child) {
        return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                || child.getAnimation() != null;
    }

isTransformedTouchPointInView 判断点击是否在该子 View 的区域内。
如果子元素满足这两个条件,那么事件就会传递给它来进行处理。
Step 4: dispatchTransformedTouchEvent 方法包含下列语句,也就是根据 child view 是否为 null 来决定是否将事件传递给父 View。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
            ...
    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }
    event.setAction(oldAction);
    return handled;
}

Step 5: addTouchTarget 方法主要是为 mFirstTouchTarget 赋值,表面该事件已经交给子 View 进行处理。如果 mFirstTouchTarget 为 null,ViewGroup 就会默认拦截接下来同一序列的所有点击事件。

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

如果所有的子元素都没有处理事件,包含两种情况:第一是 ViewGroup 没有子元素,第二是子元素处理了点击事件,但是在 dispatchTouchEvent 中返回了 false。在这样的情况下 ViewGroup 会接着处理:

if (mFirstTouchTarget == null) {
    // No touch targets so treat(对待) this as an ordinary(普通) view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
} else {
          ...
}

如果 mFirstTouchTarget 为 null,dispatchTransformedTouchEvent 传递的第三个参数为 null,也就是说会调用 ViewGroup 父类的 dispatchTouchEvent。

2.4 View 对点击事件的处理过程
View 开始处理

package android.view; --> View 源码片段

(1)dispatchTouchEvent 方法

public boolean dispatchTouchEvent(MotionEvent event) {
      ...
      boolean result = false;
      ...
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        // Step 1:判断有没有设置 OnTouchListener
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        // Step 2:上方 onTouch 返回 true,result 为 true 则不会调用 onTouchEvent
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    ...
    return result;
}

Step 1:首先判断有没有设置 OnTouchListener,如果设置了并且 onTouch 返回 true,则标记 result 为 true,说明 onTouch 消费了事件。
Step 2: result 为 true 则不会调用 onTouchEvent,如果标记 result 为 false 并且 onTouchEvent 返回 true,说明该 View 消费了点击事件,最后标记设置为 true 并返回告知父 View 事件已经被消费了。

(2)onTouchEvent 方法
首先获取当前 View 的状态 clickable(是否可点击),然后再进行判定如果该 View 的状态为 DISABLED(不可用)最后再返回 clickable。有一句重要的注释:一个可点击的不可用的 View 依旧会消费点击事件,它只是不作响应而已。说明不可用的 View 依旧消费点击事件。

final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

if ((viewFlags & ENABLED_MASK) == DISABLED) {
    if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
        setPressed(false);
    }
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    // A disabled view that is clickable still consumes the touch
    // events, it just doesn't respond to them.
    return clickable;
}

接下来判定是否设置了代理,这里的 onTouchEvent 的工作机制看起来和 OnTouchListener 类似,不深入研究。

if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
        return true;
    }
}

接下来看 onTouchEvent 对点击事件的具体处理

// 如果 View 可点击或者可显示悬浮或长按的工具提示
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    switch (action) {
        case MotionEvent.ACTION_UP:
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // 如果是悬浮工具窗的操作就另行处理
            if ((viewFlags & TOOLTIP) == TOOLTIP) {
                handleTooltipUp();
            }
            // 如果 View不可点击移除各种点击回调并跳出
            if (!clickable) {
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                break;
            }
            boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
            if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                // take focus if we don't have it already and we should in
                // touch mode.
                boolean focusTaken = false;
                if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                    focusTaken = requestFocus();
                }

                if (prepressed) {
                    // 按钮在释放之前我们确实显示了它的点击效果。
                    // 使该按钮显示按下的状态以确保用户能够看到。
                    setPressed(true, x, y);
                }

                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    // 通过mHasPerformedLongPress得知这不是长按事件
                    // 说明这是一个单击事件,所以移除长按检测
                    removeLongPressCallback();

                    // 如果处于按下状态只执行点击操作
                    if (!focusTaken) {
                        // 使用一个 Runnable 的 post 方式来调用 performClick 好过直接调用。
                        // 这样不影响该 View 除了点击外的其他的状态的更新
                        if (mPerformClick == null) {
                            mPerformClick = new PerformClick();
                        }
                        if (!post(mPerformClick)) {
                            // *重要方法,用来判定是否设置了 OnClickListener 再进行一系列处理
                            performClick();
                        }
                    }
                }

                if (mUnsetPressedState == null) {
                    mUnsetPressedState = new UnsetPressedState();
                }

                if (prepressed) {
                    postDelayed(mUnsetPressedState,
                            ViewConfiguration.getPressedStateDuration());
                } else if (!post(mUnsetPressedState)) {
                    // If the post failed, unpress right now
                    mUnsetPressedState.run();
                }
                // 处理触摸回调
                removeTapCallback();
            }
            mIgnoreNextUpEvent = false;
            break;

        case MotionEvent.ACTION_DOWN:
            ...
            break;

        case MotionEvent.ACTION_CANCEL:
            ...
            break;

        case MotionEvent.ACTION_MOVE:
            ...
            break;
    }

    return true;
}

经过了一系列的判定后来到了一个重要方法 performClick(),这个方法来处理具体的 OnClick 回调等逻辑操作。以下是 performClick 方法:

public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        // 存在OnClickListener,播放点击音效
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    notifyEnterOrExitForAutoFillIfNeeded(true);

    return result;
}

到这里点击事件分发到了 onClick 函数,接下来就是我们自己去 onClick 方法中去实现逻辑操作了。
View 的 LONG_CLICKABLE 属性默认为 falseCLICKABLE 属性依据是否可点击来确定。通过 setLongClickablesetClickable 分别可以改变两个属性的值,另外,通过 setOnClickListener 会自动将 View 的 CLICKABLE 属性设置为 true,相关源码:

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

到这里事件传递机制就基本结束了。

三、总结

事件传递机制的一些结论:

  • 同一事件序列是指手指接触屏幕到离开屏幕中产生的一系列事件。以 down 开始,中间一些 move,最后以 up 事件结束。(按下滑啊滑再松开)
  • 正常情况下一个事件序列只能被一个 View 拦截且消费。但是可以通过 onTouchEvent 强行传递给其他 View 处理。(这个事我承包了,但是我可以强行给你)
  • 某个 View 一旦决定拦截,那么这一事件序列都只能由它来处理(如果能传递给它的话),并且它的 onInterceptTouchEvent 不会再被调用。(这事我承包了,不用再问了)
  • 某个 View 一旦开始处理事件,如果不消费 ACTION_DOWN 事件(onTouchEvent 返回 false),那么同一事件序列中其他事件都不会交给他处理,并且事件重新交给它的父元素去处理,即父元素的 onTouchEvent 会被调用。(最开始的事情你都不做,后面的就不给你了,交给你上面的人去做)
  • 如果 View 不消费除 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,此时父元素的 onTouchEvent 不会被调用,并且当前 View 可以持续收到后续事件,最终消失的事件会传递给 Activity 处理。(我只答应做开头的一点事情,后面可以通知我,也不用告诉我上级,后面的事情你们自己处理)
  • ViewGroup 默认不拦截任何事件。Android 源码 ViewGroup 的 onInterceptTouchEvent 默认返回 false。(我,ViewGroup,小弟多。事情都给下面做)
  • View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递,调用 onTouchEvent 方法。(View 的具体实现是最底层小弟,不用问我做不做事,当然做)
  • View 的 onTouchEvent 默认都会消费事件(返回true),除非它是不可点击的 (clickable 和 longClickable 同时为 false)。View 的 longClickable 属性默认 false,click 看情况。Button 的 clickable 默认为 true,TextView 的 clickable 默认为 false。
  • View 的 enable 属性不影响 onTouchEvent 的默认返回值。哪怕 View 的 enable 为 false,只要它的 clickable 或者 longClickable 有一个为 true,那么它的 onTouchEvent 就返回 true。
  • onClick 发生的前提是 View 可点击,并且收到了 down 和 up 的事件。
  • 事件传递是由外向内的,先传递给父元素再由父元素分发给子 View,通过 requestDisallowInterceptTouchEvent (好长的方法名,直译为:申请不允许拦截点击事件,也就是干扰父元素消费事件)方法可以干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外

推荐阅读更多精彩内容