咕咕咕

基本上本文是把《Android艺术开发探索》这本书中的 View 事件分发机制 这一章节总结一下。

很久以前的 Android 开发笔记太鸡儿水了。

简单介绍

众所周知,一个事件序列是由 MotionEvent.ACTION_DOWN(按下) 开始,多个 MotionEvent.MOVE(移动) 和一个 MotionEvent.ACTION_UP(抬起) 结束。

一个事件的传递顺序:Activity - > Window -> DecorView - > RootView (你所设置的 View)

ViewGroup 是继承自 View 的,这点也是基础中的基础了。

事件的分发过程主要由以下三个方法来完成:

public boolean dispatchTouchEvent(MotionEvent ev);
// 如果事件传到此 View 那么该方法一定会被调用
public boolean onInterceptTouchEvent(MotionEvent event);
// 用来判断 View 是否拦截此事件,如果拦截那么在这事件序列中此方法不会再被调用
public boolean onTouchEvent(MotionEvent event);
// 用来处理事件,返回是否消耗当前事件,如果不消耗那么在同一事件序列不会再让此 View 接收到。

下面我们附上一段伪代码来阐述三个方法的关系:

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    // onInterceptTouchEvent判断是否拦截此事件
    if (onInterceptTouchEvent(ev)) {
        // 拦截的话,则调用 onTouchEvent 处理事件,返回是否消耗此事件
        consume = onTouchEvent(ev);
    } else {
        // 不拦截交给子类 View
        consume = child.dispatchTouchEvent(ev);
    }
    // 此方法的返回值为是否拦截此事件。
    return consume;
}

注意调用 dispatchTouchEvent 时往往第一个是 ACTION_DOWN 事件,请搞清“拦截”和“消耗”两个概念,onInterceptTouchEvent 返回的是是否拦截,onTouchEvent 返回的是是否消耗。

如果 onInterceptTouchEvent 返回 true ,但 onTouchEvent 返回 false,即代表拦截但不消耗事件,如果这个事件是 ACTION_DOWN,那么同一事件序列的其他事件将不会再交给此 View 了,这时会调用上一级的 onTouchEvent,如果还是 false ,那么就调用上一级的上一级的 onTouchEvent,如果都是 false,那么最终会交给 Activity 处理。

onTouchEvent 的返回值是取决于 View 的 clickable 和 longClickable 属性的,只要其中一个为 true,那么 onTouchEvent 就会返回 true,与 enable 属性无关。

如果 View 不消耗除 ACTION_DOWN 以外的事件,那么父 View 的 onTouchEvent 不会被调用,并且此 View 仍然可以接收到事件,不消耗的事件将直接交给 Activity 处理。

Android 源码中 ViewGroup 的 onInterceptTouchEvent 默认会返回 false,而 View 则没有这个方法,会直接调用 onTouchEvent。

如果一个 View 设置了 OnTouchListener 那么里面的 onTouch 方法则会被回调,我们都知道 onTouch 会返回一个 boolean ,onTouchEvent 方法是否被调用是取决于这个 boolean 的,如果返回 false,那么 onTouchEvent 则会被调用,我们经常设置的 OnClickListener 是在 onTouchEvent 中的。

从上面可以看出, onTouchListener 的优先级高于 onTouchEvent,而 OnClickListener 则是优先级最低的。

在网上翻到了一张图,总结得挺好:https://upload-images.jianshu.io/upload_images/2435754-a09ab44cb25be80d.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/972/format/webp

Android 源码分析

事件最开始会调用到 Activity 的 dispatchTouchEvent 方法:

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
}

事件开始交给 Activity 所属的 Window 进行分发,Window 的实现类是 PhoneWindow ,我们继续来看 PhoneWindow#superDispatchTouchEvent:

private DecorView mDecor;

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

很明显,Window 又把事件交给了 DecorView ,就是你所设置的布局的父 View。

DecorView#superDispatchTouchEvent

public boolean superDispatchTouchEvent(MotionEvent event) {
     // 这里又将事件传到了 ViewGroup, DecorView 是继承自 ViewGroup 的
     return super.dispatchTouchEvent(event);
}

我们所设置的 View 称作为根 View 或 顶级 View。

至此,事件已经传递到我们的 View 中了。

ViewGroup 源码解析

我们来看看 ViewGroup 中的 dispatchTouchEvent 方法的一小段,因为 Android 源码实在过于复杂,我们只需要专注我们需要专注的内容就好。

// 这个地方是 ViewGroup 是否拦截事件的一个逻辑
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
          || mFirstTouchTarget != null) {
   // 当 ViewGroup 满足 actionMasked == MotionEvent.ACTION_DOWN 或 mFirstTouchTarget != null 且没有设置标记位就会调用onInterceptTouchEvent
   // FLAG_DISALLOW_INTERCEPT 标记位是由 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 {
    // There are no touch targets and this action is not an initial down
    // so this view group continues to intercept touches.
    intercepted = true;
}

这个 actionMasked == MotionEvent.ACTION_DOWN 不用多说,如果事件由 ViewGroup 拦截的话,那么后面的 mFirstTouchTarget != null 中的 mFirstTouchTarget 是指向子元素的,一旦事件由 ViewGroup 拦截,那么后面的 ACTION_MOVE, ACTION_UP 经过这里时, (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) 会返回 false,所以 ViewGroup 的 onInterceptTouchEvent 不会再被调用,且同一事件序列的其他事件都交由它处理。

如果 ViewGroup 不拦截,那么 mFirstTouchTarget 会指向子元素,mFirstTouchTarget != null 为 true,则会调用 onInterceptTouchEvent ,显而易见不是吗?

我们可以仔细想想,第一个传递到这里的事件经常是 ACTION_DOWN,如果拦截那么后续的同一事件序列中的其他事件都会交给它处理,这时候 onInterceptTouchEvent 被调用。

假如后面又来了个 ACTION_MOVE,这时候因为 (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) 为 false 会直接拦截,但不会调用 onInterceptTouchEvent 了。

这就证明了:如果一个 View 一旦决定拦截,那么将不再调用 onInterceptTouchEvent 来询问是否拦截。

你们可以看到一个名为 FLAG_DISALLOW_INTERCEPT 的标记位,如果子 View 设置了这个标记位,那么

(!disallowIntercept) 表达式则为 false,父 View 则不会拦截此事件,当然 ACTION_DOWN 事件除外,因为 View 判断如果是 ACTION_DOWN 则会重置这个标记位。

标记位重置代码:

// 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();
}

我们再看 ViewGroup 不拦截的时候:

final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
    final float x = ev.getX(actionIndex);
    final float y = ev.getY(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);
        // child 
        final View child = getAndVerifyPreorderedView(
                preorderedList, children, childIndex);

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

        if (!canViewReceivePointerEvents(child)
                || !isTransformedTouchPointInView(x, y, child, null)) {
            ev.setTargetAccessibilityFocus(false);
            continue;
        }

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

        resetCancelNextUpFlag(child);
        // dispatchTransformedTouchEvent 则是调用子元素的 dispatchTouchEvent
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            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();
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }
        ev.setTargetAccessibilityFocus(false);
    }
    if (preorderedList != null) preorderedList.clear();
}

简单说一吧,dispatchTransformedTouchEvent 则是调用子元素的 dispatchTouchEvent,同来向子View 分发事件,我们来看 dispatchTransformedTouchEvent 方法内容:

final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
    event.setAction(MotionEvent.ACTION_CANCEL);
    if (child == null) {
        // 因为 ViewGroup extends View ,所以会调用 View 的 dispatchTouchEvent 方法
        handled = super.dispatchTouchEvent(event);
    } else {
        // 调用子类的 dispatchTouchEvent
        handled = child.dispatchTouchEvent(event);
    }
    event.setAction(oldAction);
    // 返回子 View 是否拦截
    return handled;
}

如果 dispatchTransformedTouchEvent 为 true,那么会调用 addTouchTarget 方法为 mFirstTouchTarget 赋值

newTouchTarget = addTouchTarget(child, idBitsToAssign);
// 具体 addTouchTarget 里的代码就不看了
alreadyDispatchedToNewTouchTarget = true;

如果最后遍历所有子元素事件却没有被合适的处理,要么是 ViewGroup 内没有合适的可传递 View,要么是子 View 拦截并处理了事件,但 onTouchEvent 返回了 false,所以 dispatchTouchEvent 也返回了 false。

这时 ViewGroup 就要自己处理事件了(震惊!孤寡老人竟无子可用!这究竟是…):

// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS)   
}

调用了 dispatchTransformedTouchEven 并在参数 child 传入了 null,回顾我们上面贴出来的 dispatchTransformedTouchEven 方法的代码,我们可以看到如果为 child == null则会调用 ViewGroup 自己的 dispatchTouchEvent。

ViewGroup 的源码解析到此也差不多了。

View 源码解析

接着看它的一段 dispatchTouchEvent 代码:

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 只处理自己的事件,不会再向下传递事件了。

我们看向中间第二个 if ,我们可以看到如果设置了 OnTouchListener,且 onTouch 方法返回了 true,那么 onTouchEvent 就不会执行,证明了前面所说的 OnTouchListener 优先级高于 OnClickListener。

我们再看 onTouchEvent 的代码:

if ((viewFlags & ENABLED_MASK) == DISABLED) {
    if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
        setPressed(false);
    }
    return (((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}

即使 View 为 disabled 状态也能消耗事件。

if (((viewFlags & CLICKABLE) == CLICKABLE ||
        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
        (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
    // 只要 clickable | longClickable 其中一个为 true 就能执行这里,onTouchEvent 就会返回 true
    switch (action) {
        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)) {
                            // performClick 将会调用 onClick
                            performClick();
                        }
                    }
                }
                ...
            }
            break;
            ...
    }
    ...
    return true;
}

我们接着看 performClick 方法

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

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result;
}

还没结束,我们再看一看 setOnClickListener 方法

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

可以看到在设置 View 的 OnClickListener会自动改变 View 的 clickable 属性,而 setOnLongClickListener 也是一样的。

总结

  1. 事件分发机制就是点击事件的分发,在手指接触屏幕后产生的同一个事件序列都是点击事件。
  2. 点击事件的传递顺序是由外向内。
  3. 正常情况下一个事件序列只能被一个 View 拦截且消耗。
  4. 如果 View 决定拦截事件,那么这一个事件序列都会由这个View来处理。
  5. 当子 View 拦截却不不消耗点击事件,那点击事件将交由给他的父View去处理,如果所有的 View 都没有消耗掉点击事件(onTouchEvent 返回 false),最终 Activity 会调用自己的 onTouchEvent。
  6. onInterceptTouchEvent 方法不一定会每次都执行,一个 View 一旦决定拦截将不会调用 onInterceptTouchEvent
  7. OnTouchListener的优先级高于onTouchEvent()。这样做的好处是方便在外部处理事件。
  8. 当我们把 View 设置为不可用状态,View 依然会消耗事件。