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


Android版本:7.0(API27)android

[TOC]bash


Activity对点击事件的分发

  当一个点击操做发生时,事件最早传递给当前的Activity,由Activity的dispatchTouchEvent来进行事件分发,具体的工做由Activity内部的Window来完成。Window会将事件传递给decor view,decor view通常就是当前界面的底层容器(即setContentView所设置的View的父容器)。咱们先从Activity的dispatchTouchEvent分析。app

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就会被调用。ide

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

Window.superDispatchTouchEvent动画

public abstract boolean superDispatchTouchEvent(MotionEvent event);
复制代码

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

/**
 * 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.superDispatchTouchEventthis

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
复制代码

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

// 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中事件分发机制了。rest

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是否要拦截事件"中能够看到。

相关文章
相关标签/搜索