View 的事件分发拦截机制

作者: 墨染书 | 来源:发表于2018-11-15 12:01 被阅读4次

    这一个知识点也是写烂了的,可是作为 Android 开发者又不得不学习这部分,学习了呢,总觉得要写点东西出来才觉得有感觉,得,就有这一篇文章了。

    API 27

    流程介绍

    在单点触摸中,我们对屏幕的点击,滑动,抬起等一系的动作都是由一个一个MotionEvent对象组成的触摸事件。MotionEvent 是对一个对一个事件的封装,里面包括动作、坐标等等信息,根据不同动作,主要有以下三种事件类型:

    1. ACTION_DOWN:手指刚接触屏幕,按下去的那一瞬间产生该事件
    2. ACTION_MOVE:手指在屏幕上移动时候产生该事件
    3. ACTION_UP:手指从屏幕上松开的瞬间产生该事件

    要要注意触摸事件不是独立的,而是成组的,每一组事件都是由按下事件开始的,由抬起事件或者取消事件结束。我们把由 ACTION_DOWN 开始(按下),ACTION_UP (抬起)或者 ACTION_CANCEL(取消) 结束的一组事件称为事件序列或者说事件流。取消事件是一种特殊的事件,它对应的是事件序列非人为的提前结束。

    举个例子:
    点击事件:ACTION_DOWN -> ACTION_UP
    滑动事件:ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP

    Android 每产生一个 TouchEvent 事件,他会先问最表面是否消费,如果不消费就交给他的ViewGroup,一层一层向上传递,最终被消费掉(消费就是以为着事件被处理了,代码体现为返回值,true为消费,false为不消费,消费后不再传递)。TouchEvent 不断产生,事件就会不断分发,处理,实现对事件对应的操作进行判断和反馈处理。

    还是举个栗子:
    一个button被点击一下,就会产生两个 TouchEvent 事件,当第一个 TouchEvent 产生,button 发现自己被按下,背景风格变成按下状态,如水波纹、颜色变深等。当第二个Up 的 TouchEvent 产生、分发的时候,button判别自己被点击,背景风格恢复默认状态,并且如果设置了ClickListener的话,调用 OnClick 方法。

    那么如果你的ViewGroup里面不止一个View呢(不是废话吗),不止一个ViewGroup呢?那是不是我就要制定一个机制来决定谁来处理这个事件啊?安排

    当事件刚触摸到屏幕的时候,即 ACTION_DOWN 这个 MotionEvent 产生的时候,如果ViewGroup中的View消费(返回true),就将这个View记录下来。后续这一个事件流都直接交给它处理。

    事件分发机制-简图.png

    其实只有 ACTION_DOWN 事件需要返回 true,其后的像 UP啊,Move啊,他们的返回值并没有什么影响,但是还是推荐都写成true,降低维护成本。

    当情况复杂,比如说你现在操作的是列表,点一下会触发点击事件,滑一下就会滑动,那么这样的隔着一个View如何实现的呢?这就是依靠着的就是事件拦截机制

    我们将这个过程细分,当你触摸的时候(DOWN事件),这个事件其实是先传到Activity、再传到ViewGroup、最终再传到 View,先问问ViewGroup你拦不拦截啊?一层一层的向下问,如果拦截呢,就直接交给他,如果不拦截呢?就直接往下传,直到传到底层的View,底层的View没有拦截方法,直接问他消不消费,不消费,向上分发,问他的ViewGroup是否分发,如果消费就直接交给它消费掉。这样的话,就可以把消费的权力先交给子View,在合适的时候父View可以马上接管过来。

    那么滑动的过程呢?就是在DOWN事件发生的时候,先交给子View消费,当出现MOVE事件的时候,列表发现这个是滑动,需要自己处理,就拦截并且消费掉。但是这时候View还等着后续的事件流,就比如说背景风格还是按下状态,那么父View就会发给它一个cancel事件,让他恢复状态,并且后续事件交给拦截的父View来处理。


    事件分发拦截机制-详细图解

    始于 Activity

    点击事件产生最先传递到当前的Activity,由Acivity的dispatchTouchEvent方法来对事件进行分发。

        /**
         * Called to process touch screen events.  You can override this to
         * intercept all touch screen events before they are dispatched to the
         * window.  Be sure to call this implementation for touch screen events
         * that should be handled normally.
         *
         * @param ev The touch screen event.
         *
         * @return boolean Return true if this event was consumed.
         */
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                onUserInteraction();
            }
            if (getWindow().superDispatchTouchEvent(ev)) {
                return true;
            }
            return onTouchEvent(ev);
        }
    

    代码很简单,我们来一行一行进行解析。最开始,就是就是判断当前这个事件是否是按下这个事件( MotionEvent.ACTION_DOWN),如果是,就执行一个空方法( onUserInteraction() 等待程序猿大爷重写)

        /**
         * Called whenever a key, touch, or trackball event is dispatched to the
         * activity.  Implement this method if you wish to know that the user has
         * interacted with the device in some way while your activity is running.
         * This callback and {@link #onUserLeaveHint} are intended to help
         * activities manage status bar notifications intelligently; specifically,
         * for helping activities determine the proper time to cancel a notfication.
         *
         * <p>All calls to your activity's {@link #onUserLeaveHint} callback will
         * be accompanied by calls to {@link #onUserInteraction}.  This
         * ensures that your activity will be told of relevant user activity such
         * as pulling down the notification pane and touching an item there.
         *
         * <p>Note that this callback will be invoked for the touch down action
         * that begins a touch gesture, but may not be invoked for the touch-moved
         * and touch-up actions that follow.
         *
         * @see #onUserLeaveHint()
         */
        public void onUserInteraction() {
        }
    

    这里多说几句,这个空方法是在哪些时候会调用呢?毕竟我们也是要重写的嘛,那就必须知道其执行的时期:activity在分发各种事件的时候会调用该方法,旨在提供帮助Activity智能地管理状态栏通知。当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法。下拉statubar、旋转屏幕、锁屏不会触发此方法。所以它会用在屏保应用上,因为当你触屏机器,就会立马触发一个事件,而这个事件又不太明确是什么,正好屏保满足此需求;或者对于一个Activity,控制多长时间没有用户点响应的时候,自己消失等。

    我们接着往下看getWindow().superDispatchTouchEvent(ev)

        public Window getWindow() {
            return mWindow;
        }
    

    直接返回当前界面的 mWindow,mWindow 是什么啊,是 Window ,Window 我们都知道,是一个 抽象类,它的唯一实现类就是 PhoneWindow,那我们来点一下 superDispatchTouchEvent(MotionEvent)

        /**
         * Used by custom windows, such as Dialog, to pass the touch screen event
         * further down the view hierarchy. Application developers should
         * not need to implement or call this.
         *
         */
        public abstract boolean superDispatchTouchEvent(MotionEvent event);
    

    Window 的抽象方法啊,那我们在 PhoneWindow找一找

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

    哇,实现要不要就这么简单,直接由Window 直接传递给了 mDecor,mDecor是什么啊?是 DecorView。

    public class DecorView extends FrameLayout implements RootViewSurfaceTaker, 
                                                          WindowCallbacks {
    }
    

    DecorView就是Window的顶级View,是一个ViewGroup,我们通过setContentView设置的View是它的子View(Activity的setContentView,最终是调用PhoneWindow的setContentView).

    这里放一张 Activity->视图 的图片


    Activity 结构

    是不是简单几步就实现了由Activity到ViewGroup的传递,这个中间传递者呢,就是Window。

    上面传递到了 DecorView,他直接调用了 ViewGroup 的dispatchTouchEvent()进行分发了。

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

    在陷入复杂的分发逻辑之前,我们先看 Acivity#dispatchTouchEvent留下的一个尾巴 -- 最后这个return onTouchEvent(ev);

        /**
         * Called when a touch screen event was not handled by any of the views
         * under it.  This is most useful to process touch events that happen
         * outside of your window bounds, where there is no view to receive it.
         *
         * @param event The touch screen event being processed.
         *
         * @return Return true if you have consumed the event, false if you haven't.
         * The default implementation always returns false.
         */
        public boolean onTouchEvent(MotionEvent event) {
            if (mWindow.shouldCloseOnTouch(this, event)) {  // 当超出边界要关闭Window,且超出边界,且顶层的 DecorView 不为空
                finish();    
                return true;
            }
    
            return false;   // 默认情况
        }
    

    Activity#onTouchEvent 是我们经常重写的方法,执行了 onTouchEvent表示 getWindow().superDispatchTouchEvent(ev)返回的是 false,我们都知道在事件分发体系中,true 表示消费了这个事件(处理了这个事件),那么onTouchEvent 被调用表示这个事件没有任何View消费,只能交给 Activity 处理,如何处理?就是调用 onTouchEvent 这个方法。

    来看一下Window#shouldCloseOnTouch

        /** @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标记及是否为ACTION_DOWN事件,同时判断event的x、y坐标是不是超出Bounds,然后检查FrameLayout的content的id的DecorView是否为空,进行简单判断,由此决定是否销毁这个 Activity。

    到这里 Activity 这一层就分析完了。我们在这里理一下:

    1. 先判断是否是按下事件,是则 调用onUserInteraction();空方法
    2. 在 if 括号中分发,首先是交给Activity上的 Window,Window交给顶级视图 DecorView,DecorView 调用父类 ViewGroup#dispatchTouchEvent 进行分发。
    3. 如果在分发结束后,没人消费这个事件,就调用Activity#onTouchEvent 进行处理,处理得很简单,就是判断是否需要超出边界就销毁当前的Activity,需要且超出边界就finish 并且返回true,默认为false。

    ViewGroup

    书接上文,当我们将事件交给 ViewGroup#dispatchTouchEvent ,那他怎么处理的呢?

    真的可以说是超级长了,墙裂推荐使用编辑器看。还有就是看注释。

        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            // 检查合法性代码省略
    
            boolean handled = false;  // 是否消费
            if (onFilterTouchEventForSecurity(ev)) { // 以安全策略判断是否可以分发,true->可以分发
                final int action = ev.getAction();  // 事件动作  不同的位存储有不同的信息
                final int actionMasked = action & MotionEvent.ACTION_MASK;  // 事件类型
    
                // 注释1
                // 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);  // 将当前事件分发下去,并且将整个TouchTarget链表回收
                    resetTouchState();  // 重置Touch状态标识
                }
    
                // Check for interception.  标记ViewGroup是否拦截Touch事件的传递
                final boolean intercepted;
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || mFirstTouchTarget != null) {  // 当事件是按下或者已经找到能够接收touch事件的目标组件
                    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  // 是否禁止拦截  注释2
                    if (!disallowIntercept) {  // 如果自己可以拦截,默认可以
                        intercepted = onInterceptTouchEvent(ev);  // 注释3 默认不拦截,用于重写
                        ev.setAction(action); // restore action in case it was changed
                    } else {  // 不可以拦截,直接将intercepted 设置为false
                        intercepted = false;
                    }
                } else {  // 注意,重点,当不是事件序列开始,而且还没有设置分发的子View,那么只有一种可能,就是在这之前就被我自己拦截过了,后续序列我默认拦截消费
                    // There are no touch targets and this action is not an initial down
                    // so this view group continues to intercept touches.
                    // 不是事件流开始的 ACTION_DOWN,也没有事件流的消费组件,那么直接拦截。
                    intercepted = true;
                }
    
                // If intercepted, start normal event dispatch. Also if there is already
                // a view that is handling the gesture, do normal event dispatch.
                if (intercepted || mFirstTouchTarget != null) {
                    ev.setTargetAccessibilityFocus(false);
                }
    
                // Check for cancelation. 检查 cancel 事件
                final boolean canceled = resetCancelNextUpFlag(this)
                        || actionMasked == MotionEvent.ACTION_CANCEL;
    
                // 开始事件分发
                // Update list of touch targets for pointer down, if needed.
                // 是否把事件分发给多个子View,设置: ViewGroup#setMotionEventSplittingEnabled
                final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
                TouchTarget newTouchTarget = null;  // 用于存储已经是事件流承受者的TargetView(在mFirstTouchTarget 这个事件流消费者链表中)
                boolean alreadyDispatchedToNewTouchTarget = false;
                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;
    
                    // 处理ACTION_DOWN事件
                    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
                        // 当前 MotionEvent 的动作标识
                        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;   // 子View数量
                        if (newTouchTarget == null && childrenCount != 0) {  // 有子View可分发
                            final float x = ev.getX(actionIndex);  // 得到点击的X坐标
                            final float y = ev.getY(actionIndex);  // 得到y坐标
                            // Find a child that can receive the event.
                            // Scan children from front to back.
                            final ArrayList<View> preorderedList = buildTouchDispatchChildList();  // 子View的集合 注释4(顺序问题)
                            final boolean customOrder = preorderedList == null
                                    && isChildrenDrawingOrderEnabled();
                            final View[] children = mChildren;  // 也是所有子View
                            for (int i = childrenCount - 1; i >= 0; i--) {  // 倒序访问
                                final int childIndex = getAndVerifyPreorderedIndex(
                                        childrenCount, i, customOrder);  // 得到下标,正常情况下就是 i
                                final View child = getAndVerifyPreorderedView(
                                        preorderedList, children, childIndex);  // 取出 i 对用的View
    
                                // 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)   //  注意,这就是主要的筛选条件:1. 能不能接收事件(不可见或者在动画)
                                        || !isTransformedTouchPointInView(x, y, child, null)) {  // 2. 是不是在他的范围内
                                    ev.setTargetAccessibilityFocus(false);
                                    continue;
                                }
    
                               // 注释5 如果在 mFirstTouchTarget中,就返回当前这个封装了child 的 TouchTarget,没有就返回null(注意,这时候这个View已经是在)
                                newTouchTarget = getTouchTarget(child); 
                                if (newTouchTarget != null) {   // 在mFirstTouchTarget 这个事件流消费者链表中,找到事件流的消费者,跳出循环
                                    // Child is already receiving touch within its bounds.
                                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                                    break;    // 像UP、MOVE等事件就是从这里跳出循环的
                                }
    
                                resetCancelNextUpFlag(child);  // 重置flag:cancel next up
                                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {  // 注释6 重中之重  就是这里分发,看子View是否消费
                                    // Child wants to receive touch within its bounds. 如果消费了
                                    mLastTouchDownTime = ev.getDownTime();  // 更新按下事件
                                    if (preorderedList != null) {
                                        // childIndex points into presorted list, find original index 
                                      // 找到在ViewGroup 中存储的child,最原始的下标
                                        for (int j = 0; j < childrenCount; j++) {
                                            if (children[childIndex] == mChildren[j]) { 
                                                mLastTouchDownIndex = j;  // 找到ViewGroup 中的数组的原始下标,保存在ViewGroup的成员变量中
                                                break;
                                            }
                                        }
                                    } else {   // 临时的排过序的数组为null
                                        mLastTouchDownIndex = childIndex;   
                                    }
                                    mLastTouchDownX = ev.getX();  // 被消费的事件流的DOWN事件的触摸点X(起点x坐标)
                                    mLastTouchDownY = ev.getY();  // 起点y坐标
                                    newTouchTarget = addTouchTarget(child, idBitsToAssign);   // 将消费事件流的子View的父View(当前ViewGroup)记录在消费的链表头  插入操作可见注释7
                                    alreadyDispatchedToNewTouchTarget = true;  // 表示已经成功分发给自己的子View
                                    break;
                                }
    
                                // The accessibility focus didn't handle the event, so clear
                                // the flag and do a normal dispatch to all children.
                                ev.setTargetAccessibilityFocus(false);
                            } // for循环结束
                            if (preorderedList != null) preorderedList.clear();
                        }   // 处理是 if (newTouchTarget == null && childrenCount != 0),意味着子View不为0并且没有记录的情况下的处理
    
                        //  dispatchTransformedTouchEvent方法返回false,意味着子View也不消费
                        if (newTouchTarget == null && mFirstTouchTarget != null) {
                            // Did not find a child to receive the event.没有child接收事件
                            // Assign the pointer to the least recently added target.
                            newTouchTarget = mFirstTouchTarget;  
                            while (newTouchTarget.next != null) {
                                newTouchTarget = newTouchTarget.next;
                            }
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                        }
                    }  // DOWN 事件的处理结束
                }
    
                // Dispatch to touch targets.
                if (mFirstTouchTarget == null) {  // 子View不消费
                    // 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) {   // 这两个值是在第一次dispatchTransformedTouchEvent的时候返回true赋值的,意味着事件被子View消费
                            handled = true;  // 如果被消费了
                        } else {  
                            // 不分发给子View,意味着被拦截或者子View与父ViewGroup临时视图分离(mPrivateFlags设置了PFLAG_CANCEL_NEXT_UP_EVENT),就向记录在的
                            // 是否分发给子View
                            final boolean cancelChild = 
    resetCancelNextUpFlag(target.child)
                                    || intercepted;   // 当前ViewGroup是否拦截
                            if (dispatchTransformedTouchEvent(ev, cancelChild,
                                    target.child, target.pointerIdBits)) {   // 如果不分发分发子View,调用dispatchTransformedTouchEvent发送cancel事件,已经分发过了就排除新的触摸目标
                                handled = true;  // 是否自己或者子View消费
                            }
                            if (cancelChild) {   // 事件不分发给子View,有可能是被拦截了
                                if (predecessor == null) {   // 具体链表操作看 注释8
                                    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;
        }
    

    注释1:
    这里呢,就是当一个 ACTION_DOWN 事件来了以后,需要清除一些以前事件序列的标记,开始下一个事件序列。在 cancelAndClearTouchTargets(ev) 方法中有一个非常重要的操作就是将mFirstTouchTarget设置为了null,在resetTouchState()方法中重置Touch状态标识。

    mFirstTouchTarget 是 TouchTarget,ViewGroup 的成员变量,记录要消费整个事件流的View,一个触摸事件可能有多个View可以接收到,该参数把他们连接成链状。

    注释2
    这里介绍一下几个基础知识,让大家知道为什么有这个事件拦截。

    当我们按下的时候,即 ACTION_DOWN 发生的时候,标志着整个事件流的开始,这时候我们会去找整个事件流的处理者,对应的就是整个事件分发流程,一旦找到这个事件流的处理者(消费了这个事件的ACTION_DOWN),那么后续的整个事件流都会直接发送给这个处理者进行消费掉。

    就比如说屏幕上有一个button,我滑动一下按钮,则从 ACTION_DOWN 的时候找到消费这个事件的组件了,然后button表现出按下状态。而后续整个 ACTION_MOVE 事件和 ACTION_UP 事件都直接发送给这个button处理。当下一个事件流来到又重复上述过程。

    当情况变复杂的时候,比如说是列表,首先一来就是一个 ACTION_DOWN 事件,可是我也不知道他是点击还是按下啊,所以只能分发下去,交给了item消费了,可是我发现他是滑动事件,那么我就要从子View 中把消费事件的权利抢过来,就是拦截了。而item呢?还是一个按下状态,就发送一个 ACTION_CANCEL 事件给他让他恢复状态。这里呢,意思就是说,当一个事件流我交给子View消费过后,后续不再分发给我,但是在整个事件流处理过程中,我可以随时拦截,交给我来处理

    而假如我是子View,我又不希望我的ViewGroup拦截怎么办呢?当然有办法:ViewGroup#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);
            }
        }
    

    很简单,设置 ViewGroup的标志位,并递归告诉父ViewGroup不要拦截。

    注释3

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

    在当前ViewGroup可以拦截的情况下,看自己拦不拦截呢?不拦截,鼠标那个事件就不考虑了,看到没有,默认返回false,不拦截。当然这个方法主要也是用于我们重写。

    注释4
    preorderedList中的顺序:按照addView或者XML布局文件中的顺序来的,后addView添加的子View,会添加在列表的后面,会因为Android的UI后刷新机制显示在上层;

    在事件分发的时候倒序遍历分发,那么最上层的View就可以最先接收到这个事件流,并决定是否消费这个事件流。

    注释5

        /**
         * Gets the touch target for specified child view.
         * Returns null if not found.
         */
        private TouchTarget getTouchTarget(@NonNull View child) {
            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                if (target.child == child) {
                    return target;
                }
            }
            return null;
        }
    

    从这里我们可以很清楚的明白,首先存储消费事件的目标组件的数据结构是链表,其次 mFirstTouchTarget 就是头节点。而 getTouchTarget 就是遍历整个链表,如果有就返回这个TouchTarget,没有就返回null,最后返回的值存储在 newTouchTarget 中。

    这里我们介绍一下 TouchTarget ,TouchTarget 作为 ViewGroup 的内部类,原理很像Message的原理。Android 的消息机制 介绍传送门

        /* Describes a touched view and the ids of the pointers that it has captured.
         *
         * This code assumes that pointer ids are always in the range 0..31 such that
         * it can use a bitfield to track which pointer ids are present.
         * As it happens, the lower layers of the input dispatch pipeline also use the
         * same trick so the assumption should be safe here...
         */
        private static final class TouchTarget {
            private static final int MAX_RECYCLED = 32;  // 回收池最大容量
            private static final Object sRecycleLock = new Object[0];  // 回收时候同步控制需要持有的对象锁
            private static TouchTarget sRecycleBin; // 回收池的头节点,注意是 static
            private static int sRecycledCount;  // 当前回收池的数量
    
            public static final int ALL_POINTER_IDS = -1; // all ones
    
            // The touched child view.
            public View child;   //存储的数据:View。整个事件流的消费者
    
            // The combined bit mask of pointer ids for all pointers captured by the target.
            public int pointerIdBits; 
    
            // The next target in the target list.
            public TouchTarget next;   //下一个节点
    
            private TouchTarget() {  // 不能在外部new出来
            }
    
            // 将传入的数据封装成一个TouchTarget链表的结点
            public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
                if (child == null) {  // 需要传入封装的对象吖
                    throw new IllegalArgumentException("child must be non-null");
                }
    
                final TouchTarget target;  // 最后构建出来存储的链表节点
                synchronized (sRecycleLock) {  // 拿到同步锁
                    if (sRecycleBin == null) {
                        target = new TouchTarget();  // 回收池为空,直接内部new出来
                    } else {
                        target = sRecycleBin;   // 将头节点作为目标节点
                        sRecycleBin = target.next;   // 将头节点下移一个
                         sRecycledCount--;   // 回收池数量减一
                        target.next = null;  // 将取出的节点与链表的联系断掉
                    }
                }
                target.child = child;  // 装进节点
                target.pointerIdBits = pointerIdBits;
                return target;
            }
    
            // 提供回收当前节点的方法
            public void recycle() {  
                if (child == null) {
                    throw new IllegalStateException("already recycled once");
                }
    
                synchronized (sRecycleLock) {  // 拿到同步锁
                    if (sRecycledCount < MAX_RECYCLED) {  // 没有超过回收池容量
                        next = sRecycleBin;  // 当前回收节点指向回收池链表的头结点
                        sRecycleBin = this;  // 回收池头结点指向自己,相当于上移
                        sRecycledCount += 1;  // 数量加1
                    } else {
                        next = null;  // 置空,help Gc
                    }
                    child = null;  // 抹除记录的数据
                }
            }
        }
    

    既然最后是一条以为头结点的链表,那么他到底存的是哪些View呢?上一张图:

    mFirstTouchTarget 链表

    当我们按下 button2 的时候,会一层一层的传下去,最下层的消费了,然后返回上层接着执行代码(方法调用的时候是当前方法就被压入栈中,调用方法执行结束再弹出执行),上层会在 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))的时候得到true,将刚刚消费的子View(ViewGroup/View)记录进链表。

    注释6
    下面就是在第一次什么都没有的时候进行分发,注意哦,这里还在循环里面,就意味着这次循环没找到记录,并且触摸点在这个ViewGroup范围内,可见,那我就分发。

    接下来详细看一下ViewGroup#dispatchTransformedTouchEvent

        /**
         * Transforms a motion event into the coordinate space of a particular child view,
         * filters out irrelevant pointer ids, and overrides its action if necessary.
         * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
         */
        private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                View child, int desiredPointerIdBits) {
            final boolean handled;  // 是否消费
    
            // Canceling motions is a special case.  We don't need to perform any transformations
            // or filtering.  The important part is the action, not the contents.
            final int oldAction = event.getAction();  // 获取当前事件
            if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {  // 取消,或者是取消事件
                event.setAction(MotionEvent.ACTION_CANCEL);
                if (child == null) {  // 传进来的子View为空
                    handled = super.dispatchTouchEvent(event);  // 当前ViewGroup 来执行,调用的是父类View的方法
                } else {
                    handled = child.dispatchTouchEvent(event);  // 直接交给传进来的子View,在这里就是循环的时候倒序获取的View
                }
                event.setAction(oldAction);  // 设置为 ACTION_CANCEL
                return handled;
            }
    
            // 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;
            }
    
            // 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);
            }
    
            // 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();   // 回收TouchTarget
            return handled;
        }
    

    这里引用大神的分析:

    在dispatchTouchEvent()中多次调用了dispatchTransformedTouchEvent()方法,而且有时候第三个参数为null,有时又不是,他们到底有啥区别呢?这段源码中很明显展示了结果。在dispatchTransformedTouchEvent()源码中可以发现多次对于child是否为null的判断,并且均做出如下类似的操作。其中,当child == null时会将Touch事件传递给该ViewGroup自身的dispatchTouchEvent()处理,即super.dispatchTouchEvent(event)(也就是View的这个方法,因为ViewGroup的父类是View);当child != null时会调用该子view(当然该view可能是一个View也可能是一个ViewGroup)的dispatchTouchEvent(event)处理,即child.dispatchTouchEvent(event)。别的代码几乎没啥需要具体注意分析的。

    具体的什么时候会传空呢,我们接着往下看,后面会分析和总结。

    注释7

        /**
         * Adds a touch target for specified child to the beginning of the list.
         * Assumes the target child is not already present.
         */
        private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
            final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);  // 获取节点,并将数据装进去
            target.next = mFirstTouchTarget;  // 将新节点的next指向下一个节点
            mFirstTouchTarget = target;   // 头结点记录为当前节点
            return target;   // 返回头节点
        }
    

    到这里,整个 ViewGroup 层就结束啦,这里来总结下,dispatchTransformedTouchEvent()什么时候会传入一个null的child呢?

    • ViewGroup 没有子View
    • 子元素处理了点击事件,但是在 dispatchTouchEvent 中返回了false,这一般都是因为子 View 在onTouchEvent 中返回了 false。

    注释8
    这里主要分析的是循环中的链表操作

            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的dispatchTouchEvent方法,将事件传递给子View,接下来就是View的show time。

    View#dispatchTouchEvent

        /**
         * Pass the touch screen motion event down to the target view, or this
         * view if it is the target.
         * 传递给目标View 或者 查看它是否是目标
         *
         * @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 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);
            }
    
            boolean result = false;  // 是否被处理、消费
    
            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onTouchEvent(event, 0);
            }
    
            final int actionMasked = event.getActionMasked();
            if (actionMasked == MotionEvent.ACTION_DOWN) {  // 当按下事件发生
                // Defensive cleanup for new gesture
                stopNestedScroll();   // 停止嵌套滚动
            }
    
            if (onFilterTouchEventForSecurity(event)) {   // 根据参数确定是否可以分发:这是一种安全策略(正常情况况下为true)
                if ((mViewFlags & ENABLED_MASK) == ENABLED && 
                          handleScrollBarDragging(event)) {  // 作为滚动条拖动就直接处理滚动事件,并直接消费,返回true
                    result = true;   // 滚动条的时候
                }
                //noinspection SimplifiableIfStatement 
                ListenerInfo li = mListenerInfo;   // 各种listener定义在一起的静态内部类,包括我们熟悉的 onClickListener
                if (li != null && li.mOnTouchListener != null   
                        && (mViewFlags & ENABLED_MASK) == ENABLED    // 验证 li 中的 mOnTouchListener 不为空,可以调用
                        && li.mOnTouchListener.onTouch(this, event)) {     // 调用onTouch 方法
                    result = true;   // onTouch返回true就消费
                }
    
                if (!result && onTouchEvent(event)) {   // onTouch 不消费就交给onTouchEvent,消费就变true
                    result = true;
                }
            }
    
            if (!result && mInputEventConsistencyVerifier != null) {  
                mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
            }
    
            // Clean up after nested scrolls if this is the end of a gesture;
            // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
            // of the gesture.
            if (actionMasked == MotionEvent.ACTION_UP ||
                    actionMasked == MotionEvent.ACTION_CANCEL ||
                    (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
                stopNestedScroll();
            }
    
            return result; 
        }
    

    看着注释基本都可以看懂,但是这里又一个东西得看一下,方便对一些事件的理解,那就是 onTouchEvent 方法:

    
        /**
         * Implement this method to handle touch screen motion events.
         * <p>
         * If this method is used to detect click actions, it is recommended that
         * the actions be performed by implementing and calling
         * {@link #performClick()}. This will ensure consistent system behavior,
         * including:
         * <ul>
         * <li>obeying click sound preferences
         * <li>dispatching OnClickListener calls
         * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
         * accessibility features are enabled
         * </ul>
         *
         * @param event The motion event.
         * @return True if the event was handled, false otherwise.
         */
        public boolean onTouchEvent(MotionEvent event) {
            final float x = event.getX();  // 获取点击坐标
            final float y = event.getY();
            final int viewFlags = mViewFlags;  
            final int action = event.getAction();  // 获取Action类型
    
            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;
            }
            if (mTouchDelegate != null) {
                if (mTouchDelegate.onTouchEvent(event)) {
                    return true;
                }
            }
    
            if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
                switch (action) {
                    case MotionEvent.ACTION_UP:   // 抬起的时候
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        if ((viewFlags & TOOLTIP) == TOOLTIP) {
                            handleTooltipUp();   // 处理弹窗类型的抬起事件
                        }
                        if (!clickable) {  // 如果不可点击,移除相关接口设置和设置不可点击,并跳出选择
                            removeTapCallback();
                            removeLongPressCallback();
                            mInContextButtonPress = false;
                            mHasPerformedLongPress = false;
                            mIgnoreNextUpEvent = false;
                            break;
                        }
                        boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                            // take focus if we don't have it already and we should in
                            // touch mode.
                            boolean focusTaken = false;
                            if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                                focusTaken = requestFocus();
                            }
    
                            if (prepressed) {
                                // The button is being released before we actually
                                // showed it as pressed.  Make it show the pressed
                                // state now (before scheduling the click) to ensure
                                // the user sees it.
                                // 标志着被按下,背景风格转化为按下状态
                                setPressed(true, x, y);
                            }
    
                            if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                                // This is a tap, so remove the longpress check
                                removeLongPressCallback();
    
                                // Only perform take click actions if we were in the pressed state 如果我们处于按下状态,则仅执行点击操作
                                if (!focusTaken) {
                                    // Use a Runnable and post this rather than calling
                                    // performClick directly. This lets other visual state
                                    // of the view update before click actions start.
                                    if (mPerformClick == null) {
                                        mPerformClick = new PerformClick();
                                    }
                                    if (!post(mPerformClick)) {  // post到主线程执行这个Runnable,这Runnable是由View实现,内部调用li.mOnClickListener.onClick(this);
                                        performClick();
                                    }
                                }
                            }
    
                            if (mUnsetPressedState == null) {
                                mUnsetPressedState = new UnsetPressedState();
                            }
    
                            if (prepressed) {  
                                postDelayed(mUnsetPressedState,
                                        ViewConfiguration.getPressedStateDuration());
                            } else if (!post(mUnsetPressedState)) {
                                // If the post failed, unpress right now
                                mUnsetPressedState.run();
                            }
    
                            removeTapCallback();
                        }
                        mIgnoreNextUpEvent = false;
                        break;
    
                    case MotionEvent.ACTION_DOWN:  // 按下状态
                        if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                            mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                        }
                        mHasPerformedLongPress = false;
    
                        if (!clickable) {  // 不是点击的话,有可能就是长按
                            checkForLongClick(0, x, y);
                            break;
                        }
    
                        if (performButtonActionOnTouchDown(event)) {
                            break;
                        }
    
                        // Walk up the hierarchy to determine if we're inside a scrolling container.
                        boolean isInScrollingContainer = isInScrollingContainer();
    
                        // For views inside a scrolling container, delay the pressed feedback for
                        // a short period in case this is a scroll.
                        if (isInScrollingContainer) {
                            mPrivateFlags |= PFLAG_PREPRESSED;
                            if (mPendingCheckForTap == null) {
                                mPendingCheckForTap = new CheckForTap();
                            }
                            mPendingCheckForTap.x = event.getX();
                            mPendingCheckForTap.y = event.getY();
                            postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                        } else {
                            // Not inside a scrolling container, so show the feedback right away  视图不是在滚动中,就把自己变为按下状态
                            setPressed(true, x, y);   // 按下状态,为点击事件做准备
                            checkForLongClick(0, x, y);  // 为长按做准备
                        }
                        break;
    
                    case MotionEvent.ACTION_CANCEL:  // 恢复默认状态
                        if (clickable) {
                            setPressed(false);  // 恢复默认背景风格
                        }
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        break;
    
                    case MotionEvent.ACTION_MOVE:
                        if (clickable) {
                            drawableHotspotChanged(x, y);
                        }
    
                        // Be lenient about moving outside of buttons
                        if (!pointInView(x, y, mTouchSlop)) {
                            // Outside button
                            // Remove any future long press/tap checks
                            removeTapCallback();
                            removeLongPressCallback();
                            if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                                setPressed(false);
                            }
                            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        }
                        break;
                }
    
                return true;
            }
    
            return false;
        }
    

    所有的流程最后都可以归结到这张图上

    事件分发拦截机制-详细图解

    整个事件传递就这样结束了,在这个过程中,拦截分发的代码交错在一起,我这里总结一下流程:

    1. 事件分发开始于Activity#dispatchTouchEvent,先交给getWindow().superDispatchTouchEvent(ev),返回false再交给Activity#onTouchEvent(ev)

    2. 在 PhoneWindow()#superDispatchTouchEvent(ev) 中,直接交给了顶层View:DecorView#superDispatchTouchEvent

    3. 在 DecorView#superDispatchTouchEvent 直接 super.dispatchTouchEvent(event),意味着调用父类ViewGroup#dispatchTouchEvent 处理。

    4. 调用 ViewGroup#onInterceptTouchEvent 判断是否拦截

    如果拦截,就super.

    如果不拦截并且是事件流的开始的话(DOWN 事件),就调用ViewGroup#dispatchTransformedTouchEven 分发下去

    如果分发成功,就将分发成功的View存在 mFirstTouchTarget 链表中

    如果遍历分发,没人消费,或没有子View的话,就调用父类(也是View啊)的 dispatchTouchEvent,这里面就会执行onTouch / onTouchEvent 方法

    相关文章

      网友评论

        本文标题:View 的事件分发拦截机制

        本文链接:https://www.haomeiwen.com/subject/uwgztqtx.html