Android控件系统(七)——ViewGroup触摸事件分发

275 阅读8分钟

Android版本:7.0(API27)

[TOC]


Activity对点击事件的分发

  当一个点击操作发生时,事件最先传递给当前的Activity,由Activity的dispatchTouchEvent来进行事件分发,具体的工作由Activity内部的Window来完成。Window会将事件传递给decor view,decor view一般就是当前界面的底层容器(即setContentView所设置的View的父容器)。我们先从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进行分发,如果返回true,整个事件循环就结束了,返回false意味着事件没人处理,所有View的onTouchEvent都返回fasle,那么Activity的onTouchEvent就会被调用。

  接下来看Window是如何将事件传递给ViewGroup的。通过源码我们知道,Window是个抽象类,而Window的superDispatchTouchEvent方法也是个抽象方法,因此我们必须找到Window的实现类才行。

Window.superDispatchTouchEvent

public abstract boolean superDispatchTouchEvent(MotionEvent event);

那么到底Window的实现类是什么?其实是PhoneWindow,这一点从Window的源码中也可以看出来,在Window的说明中,有这么一段话:

/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
public abstract class Window

PhoneWindow.superDispatchTouchEvent

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

PhoneWindow将事件直接传递给了DecorView,这个DecorView是什么呢?请看下面:

// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;

@Override
public final View getDecorView() {
    if (mDecor == null) {
        installDecor();
    }
    return mDecor;
}

我们知道,通过((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)这种方式就可以获取Activity所设置的View,这个mDecor显然就是getWindow().getDecorView()返回的View,而我们通过setContentView设置的View是它的一个子View。目前事件传递到了DecorView这里,犹豫DecorView继承自FrameLayout且是父View,所以最终事件传递给View。换句话说,事件肯定会传递到View,不然应用如何响应点击事件?所以,重点就到了ViewGroup中事件分发机制了。

ViewGroup事件分发源码解析

基础知识:

  • 由于一个完整的事件序列是以down开始,以up结束;
  • 哪一个view消费了down事件,那么后续事件都将交给它处理(如果它的父view不拦截事件);

事件分发会调用ViewGroup.dispatchTouchEvent方法,该方法的核心是按照如下步骤分析:

  • down事件的分发;
    (1)ViewGroup是否拦截;
    (2)ViewGroup中是否有子View消费down事件;
  • 非down事件分发;

重要的字段

该方法中有两个重要的字段:
intercepted:ViewGroup是否拦截事件,true表示拦截;
mFirstTouchTarget:是否有子View消费了down事件,如果有则mFirstTouchTarget != null;

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

  首先这里先判断事件是否为DOWN事件,如果是,则初始化,resetTouchState方法中把mFirstTouchTarget置为null。由于一个完整的事件序列是以DOWN开始,以UP结束,所以如果是DOWN事件,那么说明是一个新的事件序列,所以需要初始化之前的状态。
  这里的mFirstTouchTarget非常重要,后面会说到当ViewGroup的子元素成功处理事件的时候,mFirstTouchTarget会指向子元素,这里要留意一下。

检查ViewGroup是否要拦截事件

// Check for interception.
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(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;
}

通过源码分析,在如下两种情况下会调用ViewGroup的nInterceptTouchEvent检查ViewGroup是否拦截事件:

  • 事件序列的开始事件ACTION_DOWN;
  • 子View消耗了ACTION_DOWN事件,非DOWN的事件都会经过ViewGroup的nInterceptTouchEvent,看看ViewGroup是否需要拦截;

如果ViewGroup拦截了事件则intercepted = true。

注:此处还有一个FLAG_DISALLOW_INTERCEPT属性,为了流程清晰,我们暂且不分析,后续再说明。

ViewGroup不拦截事件

如果ViewGroup不拦截事件且事件是ACTION_DOWN事件,就会执行这个双if语句中:

 if (!canceled && !intercepted) {

    // If the event is targeting accessiiblity focus we give it to the
    // view that has accessibility focus and if it does not handle it
    // we clear the flag and dispatch the event to all children as usual.
    // We are looking up the accessibility focused host to avoid keeping
    // state since these events are very rare.
    View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
            ? findChildWithAccessibilityFocus() : null;

    if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            
                
            }
                        
}

这个双if语句的核心作用是:将ACTION_DOWN分发到自己的子View中,找到消费DOWN事件的子View,并赋值给mFirstTouchTarget;如果没有子View处理down事件那么mFirstTouchTarget == null。

final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
    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 = buildOrderedChildList();
    final boolean customOrder = preorderedList == null
            && isChildrenDrawingOrderEnabled();
    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) { // ------------------------------------1
        final int childIndex = customOrder
                ? getChildDrawingOrder(childrenCount, i) : i;
        final View child = (preorderedList == null)
                ? children[childIndex] : preorderedList.get(childIndex);

        // If there is a view that has accessibility focus we want it
        // to get the event first and if not handled we will perform a
        // normal dispatch. We may do a double iteration but this is
        // safer given the timeframe.
        if (childWithAccessibilityFocus != null) {
            if (childWithAccessibilityFocus != child) {
                continue;
            }
            childWithAccessibilityFocus = null;
            i = childrenCount - 1;
        }

        if (!canViewReceivePointerEvents(child)
                || !isTransformedTouchPointInView(x, y, child, null)) {// -----------------------2
            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);
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {// -----------------------3
            // 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();
            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();
}

  这里获取了childrenCount的值,表示该ViewGroup内部有多少个子View,如果有子View就开始遍历所有子View判断是否要把事件分发给子View。
  代码也比较长,我们只关注重点部分:

  • 代码标记1
    是一个for循环,这里表示对所有的子View进行循环遍历,由于以上判断了ViewGroup不对事件进行拦截,那么在这里就要对ViewGroup内部的子View进行遍历,一个个地找到能接受事件的子View,这里注意到它是倒序遍历的,即从最上层的子View开始往内层遍历,这也符合我们平常的习惯,因为一般来说我们对屏幕的触摸,肯定是希望最上层的View来响应的,而不是被覆盖这的底层的View来响应,否则这有悖于生活体验。

  • 代码标记2
    根据方法名字我们得知这个判断语句是判断触摸点位置是否在子View的范围内或者子View是否在播放动画,如果均不符合则continue,表示这个子View不符合条件,开始遍历下一个子View

  • 代码标记3
    这里调用了dispatchTransformedTouchEvent()方法,这个方法有什么用呢?

 private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
    final boolean handled;

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

当child != null时,将down事件传递到子view的dispatchTouchEvent;否则由ViewGroup的父类dispatchTouchEvent处理。 前面的分析条件是有子View的情况,所有child != null,down事件传递到子view的dispatchTouchEvent处理。

如果子View的onTouchEvent()返回true,那么就是消耗了Down事件,接着会调用addTouchTarget方法:

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

该方法中对mFirstTouchTarget = target进行了赋值,这也证实了前面所说的 “如果子View消耗了事件,那么mFirstTouchTarget不为null”。

小结:
整一个if(!canceled && !intercepted){ … }代码块所做的工作就是对ACTION_DOWN事件的特殊处理。因为ACTION_DOWN事件是一个事件序列的开始,所以我们要先找到能够处理这个事件序列的一个子View,如果一个子View能够消耗事件,那么mFirstTouchTarget会指向子View,如果所有的子View都不能消耗事件,那么mFirstTouchTarget将为null。

分发非ACTION_DOWN之外的其它事件

上面代码的整个过程目的只有一个,找到消费down事件的子View,如果找到mFirstTouchTarget != null。下面就是根据mFirstTouchTarget对move、up事件进行分发。

 // 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);
} else {
    // Dispatch to touch targets, excluding the new touch target if we already
    // dispatched to it.  Cancel touch targets if necessary.
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            handled = true;
        } else {
            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;
    }
}

(1)mFirstTouchTarget == null 说明没有子View消耗down事件,那么后续所有事件都交给ViewGroup的super.dispatchTouchEvent去处理;
(2)mFirstTouchTarget != null

if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) 

这里的判断就是区分了ACTION_DOWN事件和别的事件,因为在上面的分析我们知道,如果子View消耗了ACTION_DOWN事件,那么alreadyDispatchedToNewTouchTarget和newTouchTarget已经有值了,所以就直接置handled为true并返回;那么如果alreadyDispatchedToNewTouchTarget和newTouchTarget值为null,那么就不是ACTION_DOWN事件,即是ACTION_MOVE、ACTION_UP等别的事件,这些事件都会传递给消耗down事件的子View即mFirstTouchTarget。

FLAG_DISALLOW_INTERCEPT设置

FLAG_DISALLOW_INTERCEPT标志位是通过requestDisallowInterceptTouchEvent方法来设置

public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
        // We're already in this state, assume our ancestors are too
        return;
    }

    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }

    // Pass it up to our parent
    if (mParent != null) {
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
}

一旦设置了FLAG_DISALLOW_INTERCEPT为true,那么ViewGroup就不能通过onInterceptTouchEvent返回true来拦截任何事件了。这一点在分析"检查ViewGroup是否要拦截事件"中可以看到。