Android事件分发机制完全解析,带你从源码的角度彻底理解dispatchTouchEvent,onInterceptTouchEvent

一、概述

之前写了很多项目,多多少少会使用到Android View 和 ViewGroup的事件分发机制。
即:View Group的3兄弟,dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent;
和View 的事件分发2兄弟:dispatchTouchEvent,onTouchEvent。
首先说明,本文此次只重点说明,dispatchTouchEvent 和 onInterceptTouchEvent这两个事件分发的方法,至于onTouchEvent,相信大家都已经很熟悉,也用过很多,具体操作也没什么难点。

二、ViewGroup 的事件分发

1、viewgroup 的事件分发 dispatchTouchEvent 源码分析
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

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

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

            // Check for cancelation.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);

                    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 View[] children = mChildren;

                        final boolean customOrder = isChildrenDrawingOrderEnabled();
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder ?
                                    getChildDrawingOrder(childrenCount, i) : i;
                            final View child = children[childIndex];
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                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)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                mLastTouchDownIndex = childIndex;
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

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

            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }

.

·

代码较长,我们直接看这一段,比较重点的代码:

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

google给的这段代码的解释是

// Check for interception.(确认是否拦截)

那么说明,这段代码是拦截ViewGroup的事件分发的核心。再来看看下面的代码,默认拦截变量final boolean intercepted;为false,接下来就是第一个if判断,需要2个条件触发,

1 是actionMasked == MotionEvent.ACTION_DOWN,想必大家很熟悉,意思是当前点击按下,一般当有触摸事件时,都会先触发ACTION_DOWN,所以当有点击事件时,所以1为true;
2 是mFirstTouchTarget != null,我们不知道mFirstTouchTarget具体是什么意思,我们先看,如果第一个if失败,进入else是什么:
 else {
    // There are no touch targets and this action is not an initial down
    // so this view group continues to intercept touches.
    intercepted = true;
}
google的注释意思是,没有触摸事件的目标而且一开始不是down,简单说就是viewgroup的子view没有注册触摸事件的监听,或者触摸事件不是down开始(这个我也不是很明白,意思就是说非正常的触摸事件)所以,viewgroup会继续拦截此触摸事件,看看返回值,true。好的,到此我们应该已经知道了,若想拦截子view 的触摸事件只需要viewGroup的dispatchTouchEvent事件,返回true即可。
那么我们回到第一个if为true的方法内(即正常的触摸事件执行流程),这里又定义一个变量disallowIntercept ,顾名思义是指“不允许拦截”的意思,那么看看,赋值的代码如下:
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
这里我不清楚 mGroupFlags 是多少,但是不要紧,我们继续往下看,接下来就出现第二个if判断,而且条件就是 disallowIntercept 变量的取反是否为 true,如果 disallowIntercept = true;意思是不允许viewgroup,拦截触摸事件,好的,刚才我们说 viewgroup会拦截此触摸事件就会赋值 intercepted = true,那么此时应该赋值false,看看第二个else方法,果然赋值 false。
intercepted = false;
现在我们直接看第二个if里的代码
if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); // restore action in case it was changed
    }
此时有对intercepted的赋值代码为
intercepted = onInterceptTouchEvent(ev);
其实,第二个if内的代码,才是我们日常编程中接触的,触摸事件的传递处理方法,其他只是我们不知道出什么问题时的紧急处理。个人觉得,此处的代码才是我们前期 重点分析的地方。继续往下看
onInterceptTouchEvent方法的返回值赋值给 intercepted ,决定着分发事件的结果,此方法也正是 ViewGroup事件分发3兄弟中的一员,那么我们直接进入onInterceptTouchEvent方法看看:
2、viewgroup 的事件分发 dispatchTouchEvent 源码分析
 public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }
看到这里,顿时心里微微一笑,哈哈,只有一行,而且返回值固定为 false,那么我们回头再看看,第二个if的条件判断,也就是说,不管if条件是真是假都会赋值 intercepted = false; 好的,那么我们如何修改intercepted 的赋值呢? 很简单,直接修改 onInterceptTouchEvent的返回值就好了,而且它只有一句话,是不是觉得 是gooogle故意留给开发者修改返回值,从而达到开发者想要的事件拦截的目的呢(我猜的O(∩_∩)O~)
不过事实证明,修改viewGroup的事件传递确实只需要修改 onInterceptTouchEvent的返回值,就可以了,人为返回true后,子view的触摸事件将不会执行,这里就不将测试结果贴出来了了。
那么我们现在对viewGroup的3兄弟已经处理好2个了,而且 dispatchTouchEvent根本不需要开发者重写。还有一个 onTouchEvent事件,其实一般来说,ViewGroup 的onTouchEvent事件都不会写些什么,主要是把事件如何分发给子view 的 onTouchEvent去处理。就是处理的话,和View的onTouchEvent处理方法类似。

三、View的事件分发

1、view 的事件分发 dispatchTouchEvent 源码分析
 /**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                return true;
            }

            if (onTouchEvent(event)) {
                return true;
            }
        }

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        return false;
    }
View 的 dispatchTouchEvent代码相对较少,分析起来也相对容易。
我们直接看重点代码第二个if的判断,因为我们可以看到,除此之外就没有修改返回值的地方了
`if (onFilterTouchEventForSecurity(event))
我们来看看 onFilterTouchEventForSecurity 具体操作是什么
/**
     * Filter the touch event to apply security policies.(过滤触摸事件应用的安全策略。)
     *
     * @param event The motion event to be filtered.
     * @return True if the event should be dispatched, false if the event should be dropped.
     *
     * @see #getFilterTouchesWhenObscured
     */
    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.(Window 被遮盖了,终止触摸事件)
            return false;
        }
        return true;
    }
google的官方解释是过滤触摸事件应用的安全策略,就是说为了防止出现一些紧急情况,系统帮助我们处理了,再看看里面的if条件 成立时返回false,google解释是,Window 被遮盖了,终止触摸事件就是说,返回false时是系统的应急处理,正常情况返回true,所以,就像之前我们说过的,我们前期只需要关心,正常的触摸事件分发即可
再回到View 的dispatchTouchEvent 方法的 if (onFilterTouchEventForSecurity(event))此处if判断为我们正常情况为true,进入方法内部,看此段代码:
if (onTouchEvent(event)) {
       return true;
}
由此我们可以看出,关系到View 的 dispatchTouchEvent 方法的返回值,是和 View 的onTouchEvent(event)方法相关的,且返回值的结果 和 onTouchEvent 的返回值一致。
正如我们所知,当View 的 onTouchEvent返回true时,表示此View消费了当前的触摸事件,故我们得出如下结论:
dispatchTouchEvent作用是:
将touch事件向下传递直到遇到被触发的目标view,如果返回true,表示当前view就是目标view,事件停止向下分发。否则返回false,表示当前view不是目标view,需要继续向下分发寻找目标view.这个方法也可以被重载,手动分配事件。
所以,如果我们想手动分发View 的触摸事件,就重写 View 的 dispatchTouchEvent 方法,人为返回true 则不会向下分发,返回false,则本身,不能监听到触摸事件。

不管ViewGroup 还是 View 最终只会有一个 控件/View 能处理当次的触摸事件。
对于继承ViewGroup的一些控件,默认onInterceptTouchEvent 的返回值可能有所不同,如ViewPage重写了此方法,根据具体的滑动距离等不同,返回true或者false。(我之前因为这个卡了)

总结:

现在我们来总结一下,ViewGroup 和 View 的 事件分发机制。

1、 Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View的。
2、ViewGroup 的dispatchTouchEvent 方法,开发者可以不处理,仅处理onInterceptTouchEvent即可。
3、在ViewGroup中我们可以通过onInterceptTouchEvent方法对事件传递进行拦截,方法返回true代表不允许事件继续向子View传递,返回false代表不对事件进行拦截,默认返回false。(有些继承ViewGroup的控件可能重写了此方法,会有不同)。
4、子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件,即一个触摸事件只能有一个控件消费。
5、View 的 dispatchTouchEvent 方法返回值决定View 是否处理改触摸事件,返回true,事件由当前View处理不向下传递,返回false,则本身不处理继续向下传递。

PS:附一张 Android 事件分发机制流程图。

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值