Android 事件分发

概念

在移动设备上,我们去做一些操作,无论是 Android 还是 IOS 其实在系统中是根据事件驱动的,用户通过屏幕与手机交互的时候,每一次的点击,长按,移动等都是一个事件。
而事件分发机制呢?他其实是因为 Android 每一个页面都是基于 Activity 进行实现的,一个 Activity 里面有若干个 View 以及若干个 ViewGroup 组成的,而事件分发机制就是某一个事件从屏幕传递给各个 View,由这个 View 来消费这个事件或者忽略这个事件,交与其他 View 进行消费的这个过程的控制。
事件分发的对象是什么呢?系统会把整个事件封装为 MotionEvent 对象,事件分发的过程就是 MotionEvent 对象分发的过程

事件的类型

结合我们的人为操作过程,事件的类型有下面四种:

  • 按下(ACTION_DOWN)
  • 移动(ACTION_MOVE)
  • 抬起(ACTION_UP)
  • 取消(ACTION_CANCEL)

所以说一个完整的事件序列是从手指按下屏幕开始,到手指离开屏幕为止所产生的一系列事件。也就是说一个完整的事件序列是以一个 ACTION_DOWN 事件开始,到一个 ACTION_UP 事件为止,中间有若干个 ACTION_MOVE 事件(当然可以没有)。
在同一个事件序列中,如果子 View / ViewGroup 没有消费该事件,那同一事件序列的后续事件就不会传递到该子 View / ViewGroup 中去。

事件分发

那 Android 中是怎样传递事件的呢?

接触屏幕
Activity
Window
DecorView
ViewGroup
View

其实主要的对象就是 Activity,ViewGroup 以及 View。事件的分发就是对这一系列的传递的操作。接下来我们就围绕这三种主要的对象的事件分发来进行理解。

Activity

流程图

下面就是 Activity 事件分发的流程图:
Android 事件分发_第1张图片

源码分析

我们从流程图中可以知道当事件开始触发的时候会调用 dispatchTouchEvent 方法,那我们来看下对应的源码:

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

从源码中我们可以知道当事件的类型是 DOWN 的时候,会执行 onUserInteraction 方法

public void onUserInteraction() {
}

然后进入这个方法,我们可以发现在源码中该方法为空方法。所以说当我们需要监听按下手势的时候,重写 onUserInteraction 方法就可以达到监听的效果。
然后接着往下面看,我们就会发现会调用 Window 的 superDispatchTouchEvent 方法。假如消费了该事件的话,就会返回 true ,代表事件已被消费,否则调用 onTouchEvent 方法消费该事件。

public abstract boolean superDispatchTouchEvent(MotionEvent event);

观看源码后我们发现其实 superDispatchTouchEvent 方法是一个抽象方法。我们看 Window 类的注释会发现

* <p>The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.

其实他有唯一的实现,就是 PhoneWindow 类,然后我们看下对应的实现类的方法:

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

其实可以发现调用的是 DecorView 的 superDispatchTouchEvent 方法,而 DecorView 呢?其实就是 Activity 顶层的View,也我们 setContentView 方法传递进来的 layout 就是添加到了这个 View 上面。

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

然后我们就发现他其实调用了他的父类的 dispatchTouchEvent 方法,也就是 ViewGroup 的 dispatchTouchEvent 方法,那这个方法就一起在后面的 ViewGroup 里面记录。
接下来就是说假如 getWindow().superDispatchTouchEvent(ev) 返回了 true ,那就什么该事件已经被消费了,直接返回就行,如果返回的是 false ,就说明当前任何视图都没有处理这个事件,那我们就是要调用 Activity 的 onTouchEvent 方法去消费该事件,并且直接返回 onTouchEvent 方法的结果。

public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }
    return false;
}

如果 mWindow.shouldCloseOnTouch(this, event) 返回结果为 true ,就将该 Activity finish掉,并且返回true,否则为 false。

/** @hide */
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    final boolean isOutside =
            event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
            || event.getAction() == MotionEvent.ACTION_OUTSIDE;
    if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
        return true;
    }
    return false;
}

mCloseOnTouchOutside 只有当 Activity 是以 Dialog 方式进行实现的时候才会为 true 否则为 false
然后怎么确保有 View 呢?其实 peekDecorView 方法就是用来获取当前 Activity 的 DecorView 的
最后怎么保证是点击在 View 外部的呢,其实就是靠 isOutside 变量。所以我们就可以理解为什么之前会调用 finish 方法了。所以说只有在当 Activity 是以 Dialog 方式进行实现的时候,并且点击了 View 外部的空白才会将该 Activity 关闭,否则不做任何处理直接消费事件。

ViewGroup

流程图

上面说到了之后就会进入 ViewGroup 的 dispatchTouchEvent 方法,这个方法就是标志着事件已经到了 ViewGroup 这一层。然后呢?就是 onInterceptTouchEvent 方法,这个方法的意义就是是否拦截事件,假如返回结果为 true 的话,则代码该事件,当前的 ViewGroup 会拦截该事件,事件就不会再向下传递了。最后就是 onTouchEvent 方法了。这个方法在 ViewGroup 中没有实现,而是在 View 中进行实现的。这个方法就是用来当我们把事件拦截了以后,自己来处理这个事件重写的。
下面就是 ViewGroup 的事件分发流程图:
Android 事件分发_第2张图片

源码分析

我们首先就来看一下 ViewGroup 的 dispatchTouchEvent 方法,源码的行数较多,就不贴上来了,其实这个方法主要就是做了三件事:

  1. 判断是否需要拦截事件
  2. 在当前的 ViewGroup 中找到用户真正点击的 View
  3. 分发事件到 View 上

根据流程图我们可以发现从 onFilterTouchEventForSecurity 方法开始进入事件分发的过程
onFilterTouchEventForSecurity 方法做的就是一些安全策略的操作,主要的用处就是去判断这个 View 是不是可以被触摸,假如这个视图被其他视图遮挡,那就不会去处理这个事件。

public boolean onFilterTouchEventForSecurity(MotionEvent event) {
    //noinspection RedundantIfStatement
    if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
            && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
        // Window is obscured, drop this touch.
        return false;
    }
    return true;
}

如果这个方法返回了 false,那就说明安全策略不通过,所以直接返回 false,否则的话再进行后面的事件分发。
然后我们就会对这个事件的类型进行判断,假如这个事件的类型是一个按下的操作的时候,就回去做一些初始化的操作,因为按下是一个事件系列的开始。

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

cancelAndClearTouchTargets 方法是用于取消和清除所有的触摸目标,然后通过 resetTouchState 方法来重置触摸状态。
然后就要开始检测当前的事件是否是需要拦截的,就是靠 intercepted 这个变量去记录的。

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

如果当前事件为按下事件或者是已经有处理改事件的子 View 的时候就要进入后面的判断,否则就为 true,表示的是事件被拦截,事件就不会再向下传递。disallowIntercept 变量的意义就是判断当前事件是否可以拦截,如果为 true 的话,就代表当前事件在这个 ViewGroup 是不允许被拦截的,如果为 false 代表这个事件是可以被拦截的,然后就要通过 onInterceptTouchEvent 方法来判断是否对事件拦截。

public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}

这几个判断条件都是什么呢?

  1. 判断当前事件是不是来自鼠标(因为一般我们使用都不会使用鼠标进行操作,所有一般来说,这个返回为 false)
  2. 当前事件是不是按下事件
  3. 判断当前我们是否按下鼠标左键(如果按下返回 true,否则返回 false)
  4. 判断当前触摸位置是不是在一个滚动条的上面(如果是的话返回为 true,否则为 false)

只有这四个条件都满足的话,我们才会去拦截这个事件。所以说一般情况下 onInterceptTouchEvent 方法都是返回 false,不去拦截该事件的。
接下来就是判断改事件是不是一个取消事件:

final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;

然后再去判断该事件是不是作用与多个视图:

final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;

最后如果即通过了安全判断也不是取消事件以后就是 开始进入事件分发的逻辑。

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

当事件的类型是按下或者是移动的时候进入事件的分发,首先系统就会清除之前触摸点的信息,然后判断当前触摸点是否大于 0,之后就去获取当前触摸点的坐标。并且获取到可以接受到该触摸事件的子 View 的集合 preorderedList,以及判断是否对自定义 View 绘制顺序有要求。

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

然后就开始对这个子 View 列表进行遍历。然后通过索引获取到每一个子 View

final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);

getAndVerifyPreorderedIndex 方法里面就利用到了我们之前的 customOrder 变量,如果当前的 View 的绘制是有自定义顺序的话就要通过 getChildDrawingOrder 方法去获取,这个方法就是在我们自定义绘制顺序的时候需要重写的方法,否则索引就是列表的下标。

private int getAndVerifyPreorderedIndex(int childrenCount, int i, boolean customOrder) {
    final int childIndex;
    if (customOrder) {
        final int childIndex1 = getChildDrawingOrder(childrenCount, i);
        if (childIndex1 >= childrenCount) {
            throw new IndexOutOfBoundsException("getChildDrawingOrder() "
                    + "returned invalid index " + childIndex1
                    + " (child count is " + childrenCount + ")");
        }
        childIndex = childIndex1;
    } else {
        childIndex = i;
    }
    return childIndex;
}

然后就是通过 getAndVerifyPreorderedView 方法去获取对应的 View。

private static View getAndVerifyPreorderedView(ArrayList<View> preorderedList, View[] children, int childIndex) {
    final View child;
    if (preorderedList != null) {
        child = preorderedList.get(childIndex);
        if (child == null) {
            throw new RuntimeException("Invalid preorderedList contained null child at index "
                    + childIndex);
        }
    } else {
        child = children[childIndex];
    }
    return child;
}

然后就是判断这个 View 能否接受到触摸事件以及当前的触摸事件是不是在这个 View 范围之内。

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

如果两个方法都返回了 true,就说明这个 View 就可以处理该事件。然后就要获取当前 View 的触摸对象。

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

如果该 View 已经有对应的触摸对象的话直接退出循环即可,否则的话就表示该 View 还没有对应的触摸事件,然后就要去判断,该 View 有没有设置不接收触摸事件的标志位,如果有的话就清除这个标志。
然后就是最主要的一个方法了,dispatchTransformedTouchEvent 方法里面讲述了一个事件是如何从一个 ViewGroup 传递到一个具体的 View 中是如何过度的。

final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
    event.setAction(MotionEvent.ACTION_CANCEL);
    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }
    event.setAction(oldAction);
    return handled;
}

首先我们会判断该事件是否为一个取消事件,如果是取消事件的话,就要去判断是否有 View 处理,如果 child 为 null 则直接用 ViewGroup 父类的 dispatchTouchEvent 方法处理,否则调用 View 的方法,最后直接返回结果。

final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
    event.setAction(MotionEvent.ACTION_CANCEL);
    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }
    event.setAction(oldAction);
    return handled;
}

如果不是取消事件的话,就会去获取一个新的指针位,如果指针位为 0 的话,直接返回 false

// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
    return false;
}

然后呢就要去判断原有的指针位和新的指针位是不是一样的,如果是一样的就要去判断子视图是否存在,如果没有的话还是调用父类的 dispatchTouchEvent 方法处理,否则就要去计算子 View 的偏移量,然后调用子 View 的 dispatchTouchEvent 方法处理。

// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
    if (child == null || child.hasIdentityMatrix()) {
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            event.offsetLocation(offsetX, offsetY);
            handled = child.dispatchTouchEvent(event);
            event.offsetLocation(-offsetX, -offsetY);
        }
        return handled;
    }
    transformedEvent = MotionEvent.obtain(event);
} else {
    transformedEvent = event.split(newPointerIdBits);
}

如果前面的两个条件都不满足,没有返回值的话,就要创建出一个 MotionEvent 类,然后再去判断子 View 是否为空,如果为空的话还是调用父类的 dispatchTouchEvent 方法处理,否则就要去计算子 View 的偏移量,然后调用子 View 的 dispatchTouchEvent 方法处理。最后释放相关资源。

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

然后如果 dispatchTransformedTouchEvent 方法返回了 true 的话,就代表了已经传递到了子 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();
    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    alreadyDispatchedToNewTouchTarget = true;
    break;
}

这样我们就解决了当前事件不为取消事件以及子 View 允许事件传递的情况。然后就要看通用的情况了。

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

在这个时候 mFirstTouchTarget 还是为 null 的话,就说明没有子 View 去执行这个事件,就要通过 dispatchTransformedTouchEvent 方法去消费事件获取返回值,我们就可以发现他的 View 的传值为 null,在之前讲的 dispatchTransformedTouchEvent 方法里面,就会交与他的父类去执行 dispatchTouchEvent 方法。
如果不为空的话就会遍历整个链表,假如在之前已经处理了的话,他就会直接返回 true,否则的话就会去重新分发事件。

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

View

流程图

最后就到了最后一步 View 了。Android 中事件在 View 里面会怎么进行处理呢?首先和前面是一样的,就是 dispatchTouchEvent 方法,这个方法就是标志着事件已经到了 View 这一层。然后呢?就是 onTouchEvent 方法了。这个方法里面就是 Android 系统处理触摸事件的相关逻辑。
下面就是 View 的事件分发流程图:
Android 事件分发_第3张图片

源码分析

首先判断该 View 是否有可相应焦点,如果没有的话直接返回 false。

// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
    // We don't have focus or no virtual descendant has it, do not handle the event.
    if (!isAccessibilityFocusedViewOrHost()) {
        return false;
    }
    // We have focus and got the event, then use normal event dispatch.
    event.setTargetAccessibilityFocus(false);
}

然后对事件的类型进行判断,如果当前的事件为按下事件的话,如果存在视图滚动效果的话就要立刻停止滚动

final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Defensive cleanup for new gesture
    stopNestedScroll();
}

之后就开始进入真正的事件分发的过程了,首先和 ViewGroup 是一样的,通过 onFilterTouchEventForSecurity 方法来进行事件的安全判断。这块的逻辑和 View Group 中是一样的,如果不符合的话就返回 false ,如果符合的话就要接下来进行事件的处理了。

if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
    result = true;
}
//noinspection SimplifiableIfStatement
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;
}

首先就是判断当前的操控方式是不是为鼠标操作,如果为鼠标操作的话,返回值就可以直接为 ture 了,表示消费了该事件。然后就要检查是否有触摸事件的监听,然后就要调用 listener 的 onTouch 方法,如果结果返回的是 true,则我们的返回结果也为 true。如果还是不去消费该事件的话,就要调用 View 的 onTouchEvent 方法,根据返回的结果来进行返回。
然后就要进入 View 的 onTouchEvent 方法里面来看了

final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
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;
}

首先获取到点击的位置坐标,View 的标志位,事件类型以及可点击状态,然后去判断该 View 是否处于一个禁用状态的话,返回结果就为可点击的状态,这样就可以说明,当 View 是处于一个禁用状态的话,如果是可点击的,也会去消费这个事件,但是因为是直接返回的,所以说不会去多事件有所响应。
然后就会去判断有没有设置触摸的代理。

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

如果有的话就要通过代理的 onTouchEvent 方法去获取结果,如果能够消费事件的话就直接返回 true,否则继续后面的事件处理。
最后就是去判断是否为一个可点击的状态或者在标记位上 TOOLTIP 位为 1 的话,就会开始根据事件类型的不同做不同的处理,然后返回 true,表示事件已经被消费,否则返回结构为 false。

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    ...
                case MotionEvent.ACTION_DOWN:
                    ...
                case MotionEvent.ACTION_CANCEL:
                    ...
                case MotionEvent.ACTION_MOVE:
                    ...
            }

            return true;
        }

        return false;
    }

你可能感兴趣的:(Android,Android小技巧)