概念

在移动设备上,我们去做一些操作,无论是 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 事件分发的流程图:

源码分析

我们从流程图中可以知道当事件开始触发的时候会调用 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 类,然后我们看下对应的实现类的方法:

@Overridepublic 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 的事件分发流程图:

源码分析

我们首先就来看一下 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 的事件分发流程图:

源码分析

首先判断该 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 SimplifiableIfStatementListenerInfo 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;    }

更多相关文章

  1. android 触摸事件、点击事件的区别
  2. j2me与android的区别
  3. 关于android连续点击出现多个Activity界面的解决方法
  4. Android位图操作
  5. Android(安卓)Hook 机制之简单实战
  6. [置顶] Android类加载之PathClassLoader和DexClassLoader
  7. Android(安卓)Service生命周期及用法!
  8. 浅谈Java中Collections.sort对List排序的两种方法
  9. Python list sort方法的具体使用

随机推荐

  1. HTML5新增标签与属性
  2. 如何确定在web页面上呈现的字符串的长度(
  3. telnet建立http连接获取网页HTML内容
  4. jQuery:流体同位素仅在调整大小后才工作
  5. html中显示div的时候,超出浏览器的宽,怎么
  6. 用Jsoup实现html中标签替换
  7. 一步一步学Silverlight 2系列(3):界面布局
  8. vim / vi / linux:正确缩进html文件
  9. Javascript将ID添加到HTML href
  10. 如何使用JavaScript验证此HTML表单?