看啥推荐读物
专栏名称: 拾遗
Android工程师
目录
相关文章推荐
今天看啥  ›  专栏  ›  拾遗

View 的事件分发拦截机制

拾遗  · 掘金  ·  · 2019-10-03 12:03
阅读 10

View 的事件分发拦截机制

这一个知识点也是写烂了的,可是作为 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 方法




原文地址:访问原文地址
快照地址: 访问文章快照