美文网首页Android自定义ViewAndroid面试Android开发
这篇事件分发机制里的东西你都懂的话,我赔钱!

这篇事件分发机制里的东西你都懂的话,我赔钱!

作者: 吴愣 | 来源:发表于2017-12-06 22:47 被阅读270次

    1.前言

    事件分发这个东西嘛,大家一直都在讲,但总有人觉得吃不透。为什么呢?因为事件分发是多维的,有好多条思维分岔路口,而文章基本上只能用一维的方式从左到右,从上到下进行表达,所以基本不可能让普通智力的人从入门到精通。我们所要做的,就是踏踏实实打开源码,自己多琢磨,多整理。才能彻底理解这些多维的知识点。
    下面内容请配合源码食用!不然基本上索然无味!

    2.Touch与Click的前生今世

    首先,我们先来做点前戏,搞清楚setOnTouchListenersetOnClickListener以及onTouchEvent之间的关系。

    2.1 setOnTouchListener

    因为这一系列操作都是针对View的,所以我们直接看其源码,精准定位到dispatchTouchEvent()方法。

     public boolean dispatchTouchEvent(MotionEvent event) {
            ...
            boolean result = false;
            ...
                if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {
                    result = true;
                }
    
                if (!result && onTouchEvent(event)) {
                    result = true;
                }
            }
            ...
            return result;
        }
    

    这段代码非常简单,直接将一切都暴露了出来。

    result变量十分关键,它是用来控制Touch与Click执行流程的。最开始result为false,如果我们通过setOnTouchListener()为某个View设置了touch监听,并且在监听的onTouch()方法中返回true,那么result变量就会被赋值为true,此时dispatchTouchEvent()执行完毕,就不会执行接下来View本身的onTouchEvent()方法。

    2.2 onTouchEvent

    相反,如果我们没有为View设置touch监听,或者设置了touch监听但是在监听的onTouch()方法中返回false,那么result依旧为false,就会执行View本身的onTouchEvent()方法。我们来看看onTouchEvent()做了什么,由于只是热身运动,所以只贴出了与其有关的部分代码。

    public boolean onTouchEvent(MotionEvent event) {
          ...
            if (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                    (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
                switch (action) {
                    case MotionEvent.ACTION_UP:
                       ...
                                    if (!post(mPerformClick)) {
                                        performClick();
                                    }
                                }
                            }
          ...
        }
    

    可以看到,在View本身的onTouchEvent()方法中,先去判断了该View是否可以被点击,接着判断触摸事件的类型,如果是ACTION_UP类型,则执行performClick()方法。

    2.3 setOnClickListener

    performClick()这个方法比较短,直接展示出来。

    public boolean performClick() {
            final boolean result;
            final ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnClickListener != null) {
                playSoundEffect(SoundEffectConstants.CLICK);
                li.mOnClickListener.onClick(this);
                result = true;
            } else {
                result = false;
            }
    
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
            return result;
        }
    

    显而易见,如果通过setOnClickListener为当前View设置了Click监听,此时就会去执行监听中onClick()方法。

    2.4 小结

    到此为止,前戏就算结束了。我们总结下,setOnTouchListenersetOnClickListener是程序员可以设置的,而onTouchEvent是View本身的方法,在onTouchEvent中会去执行setOnClickListener中设置的OnClick方法。而在View的dispatchTouchEvent()中,首先会去判断是否设置了OnTouchListener并且其OnTouch方法返回为true,如果是,则不会执行View本身的onTouchEvent方法,如果不是,则会执行onTouchEvent进而执行OnClick方法。

    我个人是这样记住他们的关系的:Touch是触摸,Click是点击,从逻辑上来说,触摸包含了点击。所以如果设置了触摸的监听,那么其必定包含点击,于是点击的监听也就没什么必要了。

    3.事件分发

    3.1 事件分发的开始

    下面进入正题,在使用安卓手机时,我们用手指触摸了屏幕,物理设备就会一层层将触摸事件传递出来,这是底层的活儿,我们暂且不去了解。属于Android开发的故事,从Activity的dispatchTouchEvent()方法开始。请注意,这是Activity的dispatchTouchEvent(),不要和View的dispatchTouchEvent()混淆起来。

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

    分析一波,首先判断触摸事件,如果是ACTION_DOWN,则调用onUserInteraction(),这是一个空方法,专门用来让用户重写的,可以用于在事件发生前做一些操作。
    接着getWindow().superDispatchTouchEvent(ev)就比较重要了。一路跟来的同学肯定知道Activity中的Window就是PhoneWindw,不知道的传送门在这里。我们直接看其superDispatchTouchEvent方法。

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

    显而易见,这里调用了DecorView中的superDispatchTouchEvent方法

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

    DecorView是PhoneWindow的内部类,我们去看看他的父类是谁。

    private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
    

    可见,FrameLayout 是DecorView的父类,所以就会调用FrameLayout的dispatchTouchEvent方法,遗憾的是,FrameLayout并没有这个方法,所以还要去找FrameLayout 的父类ViewGroup。ViewGroup中的dispatchTouchEvent是本篇最大的高潮,我们下一节专门来讲。在此,我们回到Activity的dispatchTouchEvent方法中

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

    看最后一行代码——年轻的程序员啊,请记住,只要外层ViewGroup的dispatchTouchEvent返回为true,那么就代表事件被消耗了,此时连Activity中的onTouchEvent都不会被执行!

    3.2 ViewGroup.dispatchTouchEvent

    3.2.1 事件重置

    我们从上往下,慢慢分析ViewGroup的dispatchTouchEvent方法。

    Android是支持残障人士使用的,AccessibilityService能够模拟触摸事件,而现在我们通常用它来抢红包,其实现原理就和下面这段代码息息相关,先挖个坑。

      // If the event targets the accessibility focused view and this is it, start
            // normal event dispatch. Maybe a descendant is what will handle the click.
            if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
                ev.setTargetAccessibilityFocus(false);
            }
    

    下一行代码对handled赋值为false,这里单独拿出来就说明这个参数很重要,从名字可以看出这个值代表了事件是否被处理,后面还会多次遇到。

    boolean handled = false;
    

    接着判断事件是否是安全的,如果OJBK,则通过事件掩码获取actionMasked,这里提一嘴,MotionEvent.ACTION_MASK可以翻译成事件掩码,主要作用是在多点触摸时分辨触摸事件。

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

    继续分析,如果事件是ACTION_DOWN,则调用cancelAndClearTouchTargets(ev)resetTouchState()两兄弟。先看前面一个方法。

     /**
         * Cancels and clears all touch targets.
         */
        private void cancelAndClearTouchTargets(MotionEvent event) {
            if (mFirstTouchTarget != null) {
                boolean syntheticEvent = false;
                if (event == null) {
                    final long now = SystemClock.uptimeMillis();
                    event = MotionEvent.obtain(now, now,
                            MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                    event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                    syntheticEvent = true;
                }
    
                for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                    resetCancelNextUpFlag(target.child);
                    dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
                }
                clearTouchTargets();
    
                if (syntheticEvent) {
                    event.recycle();
                }
            }
        }
    

    顾名思义,cancelAndClearTouchTargets是用来重新初始化TouchTarget的,毕竟Down事件是一次用户触摸的开始,所以在开始之前都要把之前的TouchTarget都清除掉。那么TouchTarget又是个啥玩意儿呢?

     /* 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;
            ...
            // The next target in the target list.
            public TouchTarget next;
    
    

    从注释上可以看出,TouchTarget形容了触摸的点。它是一个单向链表,最大长度为32,也就是说,Android最多允许32个触摸点同时进行操作,算一下,起码2个人把手脚都放在同一个屏幕上才能(先不考虑能不能放得下)把设备弄成傻逼。

    resetTouchState()的作用是清除标志位,就不仔细看了。我们稍微总结一下这部分功能,ViewGroup的dispatchTouchEvent会判断触摸事件类型,如果当前为DOWN事件,则会将所有状态都初始化,开始新的一轮事件处理。

    3.2.2 事件拦截

    下面继续分析dispatchTouchEvent。结束了事件重置之后,这里定义了一个intercepted变量,显而易见,这是用来做事件拦截的。

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

    在if判断中,只要当前触摸事件为DOWN或者存在TouchTarget就会继续执行intercepted判断。这里有一个与运算mGroupFlags & FLAG_DISALLOW_INTERCEPT(两位同时为“1”,结果才为“1”,否则为0)。来想想我们是如何请求父控件不要拦截触摸事件的?没错,就是getParent().requestDisallowInterceptTouchEvent(true)

    @Override
        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);
            }
        }
    

    当参数disallowIntercept为true时,会执行mGroupFlags |= FLAG_DISALLOW_INTERCEPT运算(参加运算的两个对象只要有一个为1,其值为1),由于是getParent,所以此时的mGroupFlags 就是ViewGroup中的mGroupFlags ,下面的运算属于计算机基础,X|A&A=A,所以最终mGroupFlags的结果就是FLAG_DISALLOW_INTERCEPT。我们看源码发现FLAG_DISALLOW_INTERCEPT的值为0x80000不等于0,因此如果子View执行了getParent().requestDisallowInterceptTouchEvent(true)这个方法,父View中的intercepted参数就会被赋值为false。

    知道了这样一个流程后,我们再把思维分叉开来,回到之前的代码中

     if (!disallowIntercept) {
                        intercepted = onInterceptTouchEvent(ev);
                        ev.setAction(action); // restore action in case it was changed
                    } else {
                        intercepted = false;
                    }
    

    默认情况下,disallowIntercept结果是false,此时就会执行onInterceptTouchEvent(ev)并将其返回值赋值给intercepted,而onInterceptTouchEvent一般是会由程序员来重写的。

    好了,现在你已经搞清楚intercepted这个标志位是怎么被赋值为true或者false的,不过这只是个标志位,并没有执行什么拦截的操作,接下来我们回到ViewGroup的dispatchTouchEvent方法中,去看看具体的拦截操作是怎么执行的。

                // 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) {
                ...这里有很多很多代码...
    

    此时又获取了canceled 标志位,顾名思义用来判断事件是否被取消,这位兄弟一般都为true,并不是什么重点。重点在于if判断,这里先讨论intercepted为true的情况,此时就不会执行if中的一大段代码,直接跳到下面的流程中:

                // Dispatch to touch targets.
                if (mFirstTouchTarget == null) {
                    // No touch targets so treat this as an ordinary view.
                    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                            TouchTarget.ALL_POINTER_IDS);
                }
    

    由于之前的重置操作会将mFirstTouchTarget 设置为Null,所以此时会执行dispatchTransformedTouchEvent()方法,这是事件分发中最重要的方法,请注意第三个参数为null:

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

    我们截取了方法中最关键的部分,child就是调用方法时传入的第三个参数,当child==null时,会执行super.dispatchTouchEvent(transformedEvent)并将返回结果赋值给handled,我们此时是在ViewGroup中,其父类是View,所以我们要去考察View的dispatchTouchEvent方法:

    什么?
    你居然还在等着看View的dispatchTouchEvent源码?
    文章的第二部分是白看的吗?
    前戏是白做的吗?
    快回去重新读一遍!
    

    请注意!虽然最后代码跑到了View中,但这个View是ViewGroup的父类!也就是说最终执行的Touch或Click方法依然是外层ViewGroup中重写的Touch或Click方法!请区分ViewGroup、View、父View与子View的区别~

    我知道有人还是懵逼的,我们总结下。导致intercepted为true的原因有两个,一是父View重写了onInterceptTouchEvent方法并返回true,二是子View没有请求getParent().requestDisallowInterceptTouchEvent(true)方法。而当intercepted为true时,父View就会执行拦截操作,在源码中的表现就是dispatchTransformedTouchEvent()的第三个参数为null,从而执行super.dispatchTouchEvent(transformedEvent)方法,这个方法最终会调用在父View中重写的Touch或Click方法。

    别放松,还没完呢。看View中的这段代码:

    if (!result && onTouchEvent(event)) {
                    result = true;
                }
    

    如果父View中onTouchEvent返回false,那么result的结果就是false(结合文章第二段看更加清晰哟)。此时再回到Activity的dispatchTouchEvent方法中:

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

    由于result为false,所以getWindow().superDispatchTouchEvent(ev)也为false,此时就仍然会执行Activity的onTouchEvent(ev)方法!因此,纵使父View拦截了事件,只要他的onTouchEvent返回false,Activity中的onTouchEvent(ev)方法依旧会得到执行!

    OK,到此为止事件拦截就算讲完了。道友们且好好消化,下面继续发车!

    3.2.3 事件分发

    在前面事件拦截的分析中,我们假设intercepted为true,所以就会跳过下面代码中的if判断

                // 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) {
                ...这里有很多很多代码...
    

    而跳过的一大段代码恰恰是实现事件分发的代码。默认情况下,intercepted都为false,因此if判断中的代码基本都会执行,现在让我们一起来看看事件分发是如何实现的。

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

    在事件分发的开始,又出现了Accessibility相关的字段,这是android可以实现自动化测试的原因之一,这里先加深一波印象。

    下面还是条件判断,由于此时仍然是DOWN事件,自然而然就进去了。

                    if (actionMasked == MotionEvent.ACTION_DOWN
                            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                        ...省略几行代码...
                        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 ArrayList<View> preorderedList = buildTouchDispatchChildList();
                            ...省略下面代码...
    

    接着判断当前控件是否含有子控件,如果包含子View,则通过buildTouchDispatchChildList()对所有子View进行重排序,这个方法也是挺有意思的,它会回调buildOrderedChildList()

      ArrayList<View> buildOrderedChildList() {
            ...省略...
            for (int i = 0; i < childrenCount; i++) {
                // add next child (in child order) to end of list
                final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
                final View nextChild = mChildren[childIndex];
                final float currentZ = nextChild.getZ();
    
                // insert ahead of any Views with greater Z
                int insertIndex = i;
                while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
                    insertIndex--;
                }
                mPreSortedChildren.add(insertIndex, nextChild);
            }
            return mPreSortedChildren;
        }
    

    为什么要重排序呢?因为View是一层层添加到Window上的,在事件分发的时候,如果某一触摸点下面有多层子View,自然应该是最外层的子View先接收到事件。遗憾的是,在View的添加过程中,并不是先添加到父View中的子View就一定在最外层,因此我们就有必要通过每个子View的Z轴数值对他们进行重排序。

    重排序之后,就按照排好的顺序一个个拿到子View,并通过if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null))这句代码来判断子View是否可以接收当前的触摸事件。

                     for (int i = childrenCount - 1; i >= 0; i--) {
                                final int childIndex = getAndVerifyPreorderedIndex(
                                        childrenCount, i, customOrder);
                                final View child = getAndVerifyPreorderedView(
                                        preorderedList, children, childIndex);
                                ...省略Accessibility相关代码...
                                if (!canViewReceivePointerEvents(child)
                                        || !isTransformedTouchPointInView(x, y, child, null)) {
                                    ev.setTargetAccessibilityFocus(false);
                                    continue;
                                }
    

    先看前一个方法,显而易见接收触摸事件有两个条件,一是可见,二是不在执行动画。

    private static boolean canViewReceivePointerEvents(@NonNull View child) {
            return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                    || child.getAnimation() != null;
        }
    

    接着看下一个方法,关在在于transformPointToViewLocal会加上偏移值,而child.pointInView(point[0], point[1])会判断该child是否可以接收到(x,y)点的触摸事件

     protected boolean isTransformedTouchPointInView(float x, float y, View child,
                PointF outLocalPoint) {
            final float[] point = getTempPoint();
            point[0] = x;
            point[1] = y;
            transformPointToViewLocal(point, child);
            final boolean isInView = child.pointInView(point[0], point[1]);
            if (isInView && outLocalPoint != null) {
                outLocalPoint.set(point[0], point[1]);
            }
            return isInView;
        }
    

    综合上述两处,我们总结View能够接收触摸事件的条件一共有四个:

    1.可见
    2.不在执行动画
    3.可点击
    4.触摸点在View内
    

    在之前的分析中,如果遍历到的子View不能接收事件,就直接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;
                                }
    

    getTouchTarget()也是重点方法:

      private TouchTarget getTouchTarget(@NonNull View child) {
            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                if (target.child == child) {
                    return target;
                }
            }
            return null;
        }
    

    当触摸事件为DOWN时,mFirstTouchTarget会被重置为null,因此此时getTouchTarget返回null,什么都没有发生,代码继续向下执行。那么为什么又说这是重点方法呢,因为当触摸事件为MOVE时,mFirstTouchTarget不为null,此时就会直接break出当前循环。我们知道MOVE是十分频繁的调用,所以这里相当于是做了一层性能优化。具体是怎么优化的,我们在下个篇章还会介绍。

    拓展完毕回到主线上,代码再次进入条件判断

    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
    ...
    

    dispatchTransformedTouchEvent()在之前出现过,当父View拦截事件时,该方法被调用,第三个参数为null,并最终调用了父View的Touch或Click方法。而此时,第三个参数不再为null,取而代之的是可以接收触摸事件的子View,我们重新来看dispatchTransformedTouchEvent()中最关键的代码:

                    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;
    

    当child不为空时,先进行触摸点的偏移计算,接着执行handled = child.dispatchTouchEvent(event)。如果child是ViewGroup,就相当于重新执行上面的一大波步骤;如果child是View,则类似于父View拦截事件的过程,会执行View本身的Touch或Click方法。

    3.2.4 事件回调

    好了好了,事件分发下去的过程终于梳理完了,我们接着看分发结果的回调。

    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                    // Child wants to receive touch within its bounds.
                                    mLastTouchDownTime = ev.getDownTime();
                                    if (preorderedList != null) {
                                        // childIndex points into presorted list, find original index
                                        for (int j = 0; j < childrenCount; j++) {
                                            if (children[childIndex] == mChildren[j]) {
                                                mLastTouchDownIndex = j;
                                                break;
                                            }
                                        }
                                    } else {
                                        mLastTouchDownIndex = childIndex;
                                    }
                                    mLastTouchDownX = ev.getX();
                                    mLastTouchDownY = ev.getY();
                                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                    alreadyDispatchedToNewTouchTarget = true;
                                    break;
                                }
    

    如果子View的dispatchTouchEvent()返回true,就会进入判断体并执行最重要的一行代码newTouchTarget = addTouchTarget(child, idBitsToAssign):

     private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
            final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
            target.next = mFirstTouchTarget;
            mFirstTouchTarget = target;
            return target;
        }
    

    之前说过TouchTarget表示触摸目标,其本质是一个单向链表。显而易见,addTouchTarget()的作用就是为mFirstTouchTarget 赋值,初始化这个链表。

    现在mFirstTouchTarget 不为Null了,我们来看最后一段源码:

               // 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 {
                           ...
                    }
                }
    

    当子View返回true时,子View就消耗了这个事件,执行else中的代码,handled被赋值为true;而当子View不消耗事件返回false时,执行if中的代码,dispatchTransformedTouchEvent一共出现了3次,大家应该很熟悉了,当第三个参数为null时,会调用父View的Touch或Click方法,就这样一层一层的回调上去,整个过程是一个很完美的递归。

    3.3 MOVE事件

    在前面的文章中,我们基本是以DOWN事件为例进行分析的,如果你熟练理解了上面所讲的内容,那么请换上这辆快车,继续来看看MOVE事件是怎样的玩法。

    3.3.1 MOVE事件拦截

    MOVE事件也能够被拦截的原因就在于这个if判断。

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

    由于DOWN事件会为mFirstTouchTarget赋值,因此MOVE时mFirstTouchTarget!=null,该拦截的继续拦截。

    3.3.2 MOVE事件分发

    现在回忆一下DOWN事件分发的那一大堆步骤,什么子View重排序啊、遍历啊、判断能否接收触摸事件啊等等等等,其过程十分复杂,因为DOWN是点一下就完事了,所以可以这么整,而MOVE的调用非常频繁,要也这样操作,用户界面绝对会被卡死。

    所以我们才会用到TouchTarget,触发Down事件后,TouchTarget被赋值,其目标就是可以接收触摸事件的子View,因此在MOVE事件中,我们可以直接跳过前面的一大段代码,直接从mFirstTouchTarget获取需要的子View:

    // Dispatch to touch targets.
                if (mFirstTouchTarget == null) {
                    // No touch targets so treat this as an ordinary view.
                    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                            TouchTarget.ALL_POINTER_IDS);
                } 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;
                    }
                }
    

    这段代码不久前出现过,只是我省略了else中的部分代码,因为之前在说DOWN事件,与MOVE无关。现在我们来看完整的流程,当mFirstTouchTarget不为Null时,最关键的是这段:

      if (dispatchTransformedTouchEvent(ev, cancelChild,
                                    target.child, target.pointerIdBits)) {
                                handled = true;
                            }
    

    MOVE事件就是在这里进行分发与回调的!

    4.总结

    如果读到最后,你有一种什么都联系起来了,豁然开朗的感觉,那就点个赞呗!如果读完心想这文章写的什么[哔]东西,请务必摆上一份源码再读一次!

    而如果你真的什么都懂,底下留言!我赔钱!

    相关文章

      网友评论

      • 杨杰C:我写了一篇,大家提提建议https://www.jianshu.com/p/709c8a2fbed8
      • seraphzxz:想问一下,当子 View 是如何通过 requestDisallowInterceptTouchEvent 来影响父 View 对本次事件的拦截流程。比如说子 View 拦截了 ACTION_DOWN 事件,并在之后的事件处理中执行 requestDisallowInterceptTouchEvent(false),对于本次事件已经是从父 View 的 dispatchTouchEvent 传递过来的,那么如何将本次事件又交由父 View 处理呢?父 View 要再次执行 dispatchTouchEvent 才能进入 onInterceptTouchEvent 方法,在代码中没有找到相关实现啊,requestDisallowInterceptTouchEven 只是递归设置了标志位啊,求解答。
      • 604a54fbdc76:前面的写的实在是太详细了 :+1: 基本上一行行代码解释了 后面的是不是没写完?:grin:
      • 81c17c76d562:cancel事件是什么时候触发的?
      • 池塘细雨:有没有想过事件分发为什么做成这样?这样做,解决了什么问题?这才是真正的本

      本文标题:这篇事件分发机制里的东西你都懂的话,我赔钱!

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