通俗演义手势事件分发

作者: 皮球二二 | 来源:发表于2018-03-14 22:47 被阅读176次

    由于目前还是处于比较闲置的状态,所以我依然在github上面乱看别人的优秀项目。最近看到一个仿今日头条详情页的效果,觉得不错,本来想学习一遍之后分享给大家,没想到最后怂了,手势事件分发流程有点不清不楚了。这不,再来一遍源码分析吧
    网上的源码分析很多还是不错的,但是有的源代码版本比较老,虽然说万变不离其宗,但是总不能让别人看2.3的上古源码吧。还有个别只讲结论的,那些就不说了,很多说的还是错的。今天我试图用最通俗的讲解,让大家觉得源码理解也不那么复杂,更重要的是学习到最准确的知识

    怎么样阅读源码

    我不是什么高手、大神,所以我仅仅说一下自己阅读源码的经验:抓大放小。每个开源项目的规模都是不同的,像系统源码这种玩意,一般情况下我们都是以验证功能的目的来阅读。对于本篇文章所说的手势事件分发,基本流程你肯定或多或少知道一些,所以我们就找那些重要节点。我们不需要知道每一行代码的意思,不然你就会陷入茫茫大海不可自拔。

    本文使用的是最新的Android8.1的源码,请提前下载好相应的SDK以便于查阅

    基础知识

    1. 当用户触摸屏幕时(View或ViewGroup派生的控件),将产生点击事件(Touch事件),Touch事件的相关细节(发生触摸的位置、时间等)被封装成MotionEvent对象。
    2. 事件类型有4种:MotionEvent.ACTION_DOWN、MotionEvent.ACTION_UP、MotionEvent.ACTION_MOVE和MotionEvent.ACTION_CANCEL
    3. 事件传递的顺序一般为:Activity -> ViewGroup -> View
    4. 事件分发过程由dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()这三个方法来协作完成。这里要注意一点,Activity和View是没有onInterceptTouchEvent()方法的

    View的事件处理流程

    为了方便我们阅读源码,我先把View的事件处理流程用图来进行说明。

    View的事件处理流程

    简单的对图中的流程进行说明:View的事件处理流程从dispatchTouchEvent开始,随后对其中的OnTouchListener对象进行判断,如果不为null且View为enable,就去检查该对象的onTouch方法返回值。如果这个返回值为true,那么说明事件被消费,后续也不会调用onClickonLongClick方法。如果这个返回值为false,事件流向onTouchEvent方法,默认情况下onTouchEvent的返回值就是dispatchTouchEvent的返回值,这个值会影响ViewGroup的事件分发,我会稍后进行说明。

    所以你应该很清楚,View其实不应该用“分发”两字来描述它的流程,而是单纯的在自己跟自己玩。

    有了大概的印象之后,我们开始来阅读View的源码。既然源头在dispatchTouchEvent,那我们就从这个方法开始看,找到dispatchTouchEvent方法,来看11714行

    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnTouchListener != null
        && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
    }
    
    if (!result && onTouchEvent(event)) {
        result = true;
    }
    

    这里就是事件流程进行判断的地方,整个View事件流程的核心就是在这里。我们开始分解代码

    ListenerInfo是什么?它是一系列事件Listener的总类

    static class ListenerInfo {
        public OnClickListener mOnClickListener;
        protected OnLongClickListener mOnLongClickListener;
        private OnTouchListener mOnTouchListener;
        .......................................
    }
    

    也就是说如果我们定义了OnClickListener或者OnLongClickListenerOnTouchListener,那ListenerInfo对象就不会是null。
    这样我们通过第一个if语句就可以明白,如果你的View是enable并且View的OnTouchListeneronTouch方法返回true,那么这个View就消费了事件。这是因为result值已经变成true,对照剩余部分的源码可以知道其他判断方法都走不进去了,这里实际上已经完成了dispatchTouchEvent的流程。既然这一步骤已经将result改为true了,那么后续的onTouchEvent自然就进不去了。
    解决完onTouchListener出现与否的问题后,我们开始真正进入onTouchEvent研究了
    注意看第二个if,当你绕过onTouchListener之后,当前View的onTouchEvent的返回值与super.dispatchTouchEvent的值是相同的了

    进入onTouchEvent方法,来到12923行

    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    

    只要你设置了CLICKABLE或者LONG_CLICKABLE或者CONTEXT_CLICKABLE,那么这里的clickable就是true。这几个属性都可以在布局中设置。
    在向下来看12942行

    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP)
    

    clickable为true,才能进入各种MotionEvent的action的处理逻辑。因为View本身clickable的值是false,如果我们刚才在布局中没有将其设置为true,那么后面的一系列判断跟我们就都没关系了。

    action的处理逻辑里有你最关心的onClick与onLongClick事件的处理,先来看MotionEvent.ACTION_DOWN

    boolean isInScrollingContainer = isInScrollingContainer();
    if (isInScrollingContainer) {
        mPrivateFlags |= PFLAG_PREPRESSED;
        if (mPendingCheckForTap == null) {
            mPendingCheckForTap = new CheckForTap();
        }
        mPendingCheckForTap.x = event.getX();
        mPendingCheckForTap.y = event.getY();
        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
    } else {
        setPressed(true, x, y);
        checkForLongClick(0, x, y);
    }
    

    首先判断事件是否在滚动的容器内,如果是就延迟100ms再去执行长按操作的检查,否则就立刻开始。这里先将pressed状态变为true,可以联想到selector变色,然后执行checkForLongClick

    private void checkForLongClick(int delayOffset, float x, float y) {
        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
            ........................
            postDelayed(mPendingCheckForLongPress,
                        ViewConfiguration.getLongPressTimeout() - delayOffset);
        }
    }
    

    如果在500ms内这个长按操作message没有被取消,那他就会被执行。执行的就是我们熟悉的performLongClick。来看看这个message

    private final class CheckForLongPress implements Runnable {
        @Override
        public void run() {
            if ((mOriginalPressedState == isPressed()) && (mParent != null) 
                && mOriginalWindowAttachCount == mWindowAttachCount) {
                if (performLongClick(mX, mY)) {
                    mHasPerformedLongPress = true;
                }
            }
        }
        ··········································
    }
    

    既然说到了取消长按操作message,那这个取消会出现在哪里呢?我们来MotionEvent.ACTION_UP看看,注意这里

    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
        removeLongPressCallback();
        if (!focusTaken) {
            if (mPerformClick == null) {
                mPerformClick = new PerformClick();
            }
            if (!post(mPerformClick)) {
                performClick();
            }
        }
    }
    

    长按事件没有执行,被取消了。removeLongPressCallback将长按事件取消,随后执行了performClick
    到此为止,你就很清楚的明白什么情况下长按以及单击事件会不执行了吧,别忘记onTouchLitener是优先于onClickListener和onLongClickListener执行的。同时通过分析代码,我们也明白了super.onTouchEvent里面确实没有干什么对事件分发的影响事情,如果你对dispatchTouchEvent进行了重写,那么super.onTouchEvent的返回值对你来说没有任何影响

    ViewGroup事件分发

    ViewGroup与之相比会多一些东西,在理解上可能比较绕,因为它是真正的涉及到了分发这个功能,但是多看几遍也就那么一回事。它跟View的分发流程大体上差不多,但是多了一个onInterceptTouchEvent方法。我们依然还是先看图说话

    ViewGroup事件分发

    当事件到了ViewGroup中之后,依然是从dispatchTouchEvent开始,然后去判断是否被onInterceptTouchEvent拦截。如果没有被拦截,则开始遍历子View去寻找是否含有被点击的。如果找不到,则执行自己父类View的dispatchTouchEvent,进而事件交由自身来处理,其中执行结果是true,表明被自己消费;执行结果是false,表明自己对事件不关心。如果能找到,则将事件交由子View来处理,实现了事件的下发。由于查找View的过程是一套递归流程,所以自然而然形成了下发及回溯的“U型管”效果

    一般流程

    如果被拦截,则执行自己父类View的dispatchTouchEvent,进而事件交由自身来处理,其中执行结果是true,表明被自己消费;执行结果是false,表明自己对事件不关心。

    拦截流程

    说了个大概之后继续看ViewGroup的源码。同样,我们进入ViewGroup的dispatchTouchEvent,来看看2497行

    if (actionMasked == MotionEvent.ACTION_DOWN) {
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }
    

    在刚开始的时候重置触摸相关的状态。cancelAndClearTouchTargets里面有很重要的一个参数mFirstTouchTarget,它的意义在于标记有没有子View来消费手势事件。这时该方法中的clearTouchTargets方法在down的时候将其置为null,这个值会参与到dispatchTouchEvent的很多重要逻辑中

    继续往下走,一下子就看到那个熟悉的身影onInterceptTouchEvent

    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); 
        } else {
            intercepted = false;
        }
    } else {
        intercepted = true;
    }
    

    先来关注下mGroupFlags参数,这个参数是“内部拦截法”的核心,可以通过requestDisallowInterceptTouchEvent方法来允许或者不允许父ViewGroup进行事件拦截。

        @Override
        public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
            if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
                return;
            }
    
            if (disallowIntercept) {
                mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
            } else {
                mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
            }
    
            if (mParent != null) {
                mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
            }
        }
    

    通过最下面的if (mParent != null)可以清楚的看到这是一个递归的形式。如果你在底层view调用parent.requestDisallowInterceptTouchEvent,那么它会不断调用父类的这个方法,所以所有的父ViewGroup的这个标记位都是一样的。另外需要注意的是,该方法在down时不具备拦截的功能,因为它的上一级ViewGroup的这段代码在此刻已经执行完了。
    再回到dispatchTouchEvent中来,默认情况下disallowIntercept为false,不进行拦截,这样就来到了onInterceptTouchEvent。一般情况下其返回值默认就是false,表示不进行拦截

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

    那么这一小段代码结束之后,intercepted值就是false,不拦截。继续向下

    如果不被拦截,开始查找该ViewGroup内的子View有没有包含消费事件的

    if (!canceled && !intercepted)
    

    没有被取消事件或者拦截事件才可以进入
    随后又有一个限制

    if (actionMasked == MotionEvent.ACTION_DOWN
        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
        || actionMasked == MotionEvent.ACTION_HOVER_MOVE)
    

    基本上可以理解为只有down的时候才可以进入。接着看

    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
    final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) {
        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
        ...........................................
        if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
            ev.setTargetAccessibilityFocus(false);
            continue;
        }
        ...........................................
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            ...........................................
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }
    }
    

    这边我浓缩了很多代码,有的涉及到多指触控的,这个不在我们本次讨论中。
    反向遍历所有的子View,优先找出后addView进去的View。canViewReceivePointerEvents表示当子View可见或者正在执行动画或准备执行动画条件下才可以接受触摸事件,isTransformedTouchPointInView表示触摸点范围在当前子View内才可以接受触摸事件。只有满足这两个条件的子View才可进行下一步判断。
    dispatchTransformedTouchEvent绝对是一个重头戏,他关系着手势事件的分发。来看看代码,依然看重点部分

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
        final boolean handled;
        ..................................
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            ..................................
            handled = child.dispatchTouchEvent(transformedEvent);
        }
        ..................................
        return handled;
    }
    

    来来来,看到了吧。如果有子View的时候,执行的是子View的dispatchTouchEvent,否则执行的是自身的dispatchTouchEvent。所以为什么说手势事件分发是一个“U形管”呢,就在这个地方。当然这里只是进入子View的部分。无论是自身还是子View甚至子子View的dispatchTouchEvent发生拦截,handler都会为true,反之为false。如果dispatchTransformedTouchEvent为true的话,那么刚才说到的mFirstTouchTarget就在addTouchTarget方法中被赋值,同时alreadyDispatchedToNewTouchTarget标志也被修改为true,表明找到了可以消费事件的子VIew了

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

    最后就是“U型管”的回溯部分了

    if (mFirstTouchTarget == null) {
        handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
    } else {
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            final TouchTarget next = target.next;
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                handled = true;
            } else {
                final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
                if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
                    handled = true;
                }
                if (cancelChild) {
                    if (predecessor == null) {
                        mFirstTouchTarget = next;
                    } else {
                        predecessor.next = next;
                    }
                    target.recycle();
                    target = next;
                    continue;
                }
            }
            predecessor = target;
            target = next;
        }
    }
    

    如果你的子View没有消费事件的,那么mFirstTouchTarget是null,直接执行自身的dispatchTouchEvent并返回false;如果有消费,就啥都不做啦,直接返回true

    下面开始分析up事件的流程,因为move跟up大体上差不多,讲一个就行了。
    还是要重新跑一边刚才dispatchTouchEvent的流程。这个要区分mFirstTouchTarget是不是null两种情况来说。

    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 {
        intercepted = true;
    }
    

    如果mFirstTouchTarget不为null,说明之前有子View消费。既然不是down,所以之前查找子View的过程就没有了,直接来到最后判断mFirstTouchTarget的地方。mFirstTouchTarget为null直接执行自身的dispatchTouchEvent,这一般都发生在Activity的DecorView的。mFirstTouchTarget不为null并且alreadyDispatchedToNewTouchTarget又是false,所以又会通过dispatchTransformedTouchEvent来执行子View的dispatchTouchEvent,并将handled置为true,全套流程走完

    差点忘记一个很重要的事情了,之前看到有人试图在onTouchEvent的move里面修改返回值来达到调整事件分发流程,事实上这是无法实现要求的。从源码来看只有在up的时候或者事件取消,mFirstTouchTarget才会被重置,在move跟up的时候每次都会使用down时候的mFirstTouchTarget

    if (canceled
        || actionMasked == MotionEvent.ACTION_UP
        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            resetTouchState();
    }
    

    小实验

    我们来做如下几个实验来看看你对之前所说知识理解的情况如何
    先来一个ChildView,它继承自普通的View,仅仅在dispatchTouchEvent以及onTouchEvent相关action下打印提示而已

    class ChildView: View {
        constructor(context: Context) : super(context)
        constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
        constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
    
        override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
            when(event?.action) {
                MotionEvent.ACTION_DOWN -> {
                    println("ChildView dispatchTouchEvent ACTION_DOWN")
                }
                MotionEvent.ACTION_MOVE -> {
                    println("ChildView dispatchTouchEvent ACTION_MOVE")
                }
                MotionEvent.ACTION_UP -> {
                    println("ChildView dispatchTouchEvent ACTION_UP")
                }
                MotionEvent.ACTION_CANCEL -> {
                    println("ChildView dispatchTouchEvent ACTION_CANCEL")
                }
            }
            return super.dispatchTouchEvent(event)
        }
    
        override fun onTouchEvent(event: MotionEvent?): Boolean {
            when(event?.action) {
                MotionEvent.ACTION_DOWN -> {
                    println("ChildView onTouchEvent ACTION_DOWN")
                }
                MotionEvent.ACTION_MOVE -> {
                    println("ChildView onTouchEvent ACTION_MOVE")
                }
                MotionEvent.ACTION_UP -> {
                    println("ChildView onTouchEvent ACTION_UP")
                }
                MotionEvent.ACTION_CANCEL -> {
                    println("ChildView onTouchEvent ACTION_CANCEL")
                }
            }
            return super.onTouchEvent(event)
        }
    }
    

    整个app就一个简单的布局,这里注意ChildView的clickable是true

    <?xml version="1.0" encoding="utf-8"?>
    <com.renyu.newsdetaildemo.view.ChildView xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clickable="true" />
    

    在activity里面给ChildView加一个onTouchListener

    view_child.setOnTouchListener { _, event ->
        when(event?.action) {
            MotionEvent.ACTION_DOWN -> {
                println("ChildView OnTouchListener ACTION_DOWN")
            }
            MotionEvent.ACTION_MOVE -> {
                println("ChildView OnTouchListener ACTION_MOVE")
            }
            MotionEvent.ACTION_UP -> {
                println("ChildView OnTouchListener ACTION_UP")
            }
            MotionEvent.ACTION_CANCEL -> {
                println("ChildView OnTouchListener ACTION_CANCEL")
            }
        }
        false
    }
    
    实验一:

    ChildView的dispatchTouchEvent返回值为super.dispatchTouchEvent
    ChildView的onTouchEvent返回值为super.onTouchEvent
    ChildView的setOnTouchListener返回值为false

    ChildView dispatchTouchEvent ACTION_DOWN
    ChildView OnTouchListener ACTION_DOWN
    ChildView onTouchEvent ACTION_DOWN
    ChildView dispatchTouchEvent ACTION_UP
    ChildView OnTouchListener ACTION_UP
    ChildView onTouchEvent ACTION_UP
    

    可以看到ChildView事件传递顺序为dispatchTouchEvent -> OnTouchListener -> onTouchEvent。因为setOnTouchListener返回值是false,所以顺利进入onTouchEvent判断方法中。又因为ChildView设置了clickable的原因,所以onTouchEvent返回true,表明事件被消费。这样ChildView的上一层ViewGroup---DecorView的mFirstTouchTarget值不为null,所以每次都会通过调用了ChildView的dispatchTouchEvent走到ChildView来。

    实验二:

    ChildView的dispatchTouchEvent返回值为super.dispatchTouchEvent
    ChildView的onTouchEvent返回值为super.onTouchEvent
    ChildView的setOnTouchListener返回值为true

    ChildView dispatchTouchEvent ACTION_DOWN
    ChildView OnTouchListener ACTION_DOWN
    ChildView dispatchTouchEvent ACTION_UP
    ChildView OnTouchListener ACTION_UP
    

    对比之前实验一,因为setOnTouchListener返回值是true,所以onTouchEvent事件没了。同样由于事件被ChildView消费,因此每次都会走到ChildView来。

    实验三:

    移除ChildView的setOnTouchListener代码
    ChildView的dispatchTouchEvent返回值为super.dispatchTouchEvent
    ChildView的onTouchEvent去除super.onTouchEvent,并将返回值设置为false

    ChildView dispatchTouchEvent ACTION_DOWN 
    ChildView onTouchEvent ACTION_DOWN
    

    dispatchTouchEvent的返回值跟onTouchEvent相同,由于后者返回false,所以事件未被消费。mFirstTouchTarget为空,下次事件不会再走到这里

    实验四:

    移除ChildView的setOnTouchListener代码
    ChildView的dispatchTouchEvent返回值为super.dispatchTouchEvent
    ChildView的onTouchEvent去除super.onTouchEvent,并将返回值设置为true

    ChildView dispatchTouchEvent ACTION_DOWN
    ChildView onTouchEvent ACTION_DOWN
    ChildView dispatchTouchEvent ACTION_UP
    ChildView onTouchEvent ACTION_UP
    

    之前说了super.onTouchEvent的存在与否对事件分发没有太大影响,关键还是看返回值。只要你返回值是true就代表消费了

    实验五:

    移除ChildView的setOnTouchListener代码
    ChildView的dispatchTouchEvent返回值为super.dispatchTouchEvent
    ChildView的onTouchEvent保留super.onTouchEvent,并将返回值设置为false

    ChildView dispatchTouchEvent ACTION_DOWN 
    ChildView onTouchEvent ACTION_DOWN
    

    这样又与实验三相同了

    实验六:

    移除ChildView的setOnTouchListener代码
    ChildView的dispatchTouchEvent返回值为super.dispatchTouchEvent
    ChildView的onTouchEvent保留super.onTouchEvent,并将返回值设置为true

    ChildView dispatchTouchEvent ACTION_DOWN
    ChildView onTouchEvent ACTION_DOWN
    ChildView dispatchTouchEvent ACTION_UP
    ChildView onTouchEvent ACTION_UP
    

    这样又与实验四相同了

    实验七:

    移除ChildView的setOnTouchListener代码
    ChildView的dispatchTouchEvent去除super.dispatchTouchEvent,并将返回值设置为false
    ChildView的onTouchEvent返回值为super.onTouchEvent

    ChildView dispatchTouchEvent ACTION_DOWN 
    

    dispatchTouchEvent去除super.dispatchTouchEvent只会影响其内部事件流程,对分发没有影响。所以这里没有执行onTouchEvent。直接返回false,就表示没有发生消费事件,所以不会造成后续事件进入该View

    实验八:

    移除ChildView的setOnTouchListener代码
    ChildView的dispatchTouchEvent去除super.dispatchTouchEvent,并将返回值设置为true
    ChildView的onTouchEvent返回值为super.onTouchEvent

    ChildView dispatchTouchEvent ACTION_DOWN
    ChildView dispatchTouchEvent ACTION_UP 
    

    与实验七的区别在于返回值是true,事件被消费了,所以多一个dispatchTouchEvent的up

    实验九:

    移除ChildView的setOnTouchListener代码
    ChildView的dispatchTouchEvent保留super.dispatchTouchEvent,并将返回值设置为false
    ChildView的onTouchEvent返回值为super.onTouchEvent

    ChildView dispatchTouchEvent ACTION_DOWN
    ChildView onTouchEvent ACTION_DOWN
    

    可以对比下实验七

    实验十:

    移除ChildView的setOnTouchListener代码
    ChildView的dispatchTouchEvent保留super.dispatchTouchEvent,并将返回值设置为true
    ChildView的onTouchEvent返回值为super.onTouchEvent

    ChildView dispatchTouchEvent ACTION_DOWN
    ChildView onTouchEvent ACTION_DOWN
    ChildView dispatchTouchEvent ACTION_UP
    ChildView onTouchEvent ACTION_UP
    

    可以对比下实验八

    实验十一

    我们把布局稍微改变一下,加入一个继承自LinearLayout的ParentView,具体功能同ChildView一样

    class ParentView: LinearLayout {
        constructor(context: Context) : super(context)
        constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
        constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
    
        override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
            when(event?.action) {
                MotionEvent.ACTION_DOWN -> {
                    println("ParentView dispatchTouchEvent ACTION_DOWN")
                }
                MotionEvent.ACTION_MOVE -> {
                    println("ParentView dispatchTouchEvent ACTION_MOVE")
                }
                MotionEvent.ACTION_UP -> {
                    println("ParentView dispatchTouchEvent ACTION_UP")
                }
                MotionEvent.ACTION_CANCEL -> {
                    println("ParentView dispatchTouchEvent ACTION_CANCEL")
                }
            }
            return super.dispatchTouchEvent(event)
        }
    
        override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
            when(ev?.action) {
                MotionEvent.ACTION_DOWN -> {
                    println("ParentView onInterceptTouchEvent ACTION_DOWN")
                }
                MotionEvent.ACTION_MOVE -> {
                    println("ParentView onInterceptTouchEvent ACTION_MOVE")
                }
                MotionEvent.ACTION_UP -> {
                    println("ParentView onInterceptTouchEvent ACTION_UP")
                }
                MotionEvent.ACTION_CANCEL -> {
                    println("ParentView onInterceptTouchEvent ACTION_CANCEL")
                }
            }
            return super.onInterceptTouchEvent(ev)
        }
    
        override fun onTouchEvent(event: MotionEvent?): Boolean {
            when(event?.action) {
                MotionEvent.ACTION_DOWN -> {
                    println("ParentView onTouchEvent ACTION_DOWN")
                }
                MotionEvent.ACTION_MOVE -> {
                    println("ParentView onTouchEvent ACTION_MOVE")
                }
                MotionEvent.ACTION_UP -> {
                    println("ParentView onTouchEvent ACTION_UP")
                }
                MotionEvent.ACTION_CANCEL -> {
                    println("ParentView onTouchEvent ACTION_CANCEL")
                }
            }
            return super.onTouchEvent(event)
        }
    }
    

    布局稍微改变一下

    <?xml version="1.0" encoding="utf-8"?>
    <com.renyu.newsdetaildemo.view.ParentView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/view_parent"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <com.renyu.newsdetaildemo.view.ChildView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/view_child"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clickable="true" />
    </com.renyu.newsdetaildemo.view.ParentView>
    

    把ParentView的onInterceptTouchEvent改为true,并将其clickable设置为true

    ParentView dispatchTouchEvent ACTION_DOWN
    ParentView onInterceptTouchEvent ACTION_DOWN
    ParentView onTouchEvent ACTION_DOWN
    ParentView dispatchTouchEvent ACTION_UP
    ParentView onTouchEvent ACTION_UP
    

    参考上面的代码,onInterceptTouchEvent(ev)为true,那么intercepted就是true,那么查找子View的过程就没有了,因此mFirstTouchTarget值为null直接执行自身的dispatchTouchEvent。up的时候再进来,mFirstTouchTarget为null,也就不会再执行onInterceptTouchEvent

    实验十二

    这里还有一种情况,如果我们给ParentView加clickable为true,同时在事件分发过程中改变onInterceptTouchEvent(ev)的值,比如在move事件中,会发生什么事情?

    ParentView dispatchTouchEvent ACTION_DOWN
    ParentView onInterceptTouchEvent ACTION_DOWN
    ChildView dispatchTouchEvent ACTION_DOWN
    ChildView onTouchEvent ACTION_DOWN
    ParentView dispatchTouchEvent ACTION_MOVE
    ParentView onInterceptTouchEvent ACTION_MOVE
    ChildView dispatchTouchEvent ACTION_CANCEL
    ChildView onTouchEvent ACTION_CANCEL
    ParentView dispatchTouchEvent ACTION_MOVE
    ParentView onTouchEvent ACTION_MOVE
    ParentView dispatchTouchEvent ACTION_MOVE
    ParentView onTouchEvent ACTION_MOVE
    ParentView dispatchTouchEvent ACTION_UP
    ParentView onTouchEvent ACTION_UP
    

    可以看到与实验十一相比多了一个ACTION_CANCEL。这个又需要我们回到源码中来。
    结合之前的源码,我们知道在中间过程中intercepted值变为true了,但同时mFirstTouchTarget在第一次move的时候还不为null。所以流程刚开始move的流程与move是一样的。但是不知道你之前在dispatchTouchEvent里有没有注意到这句话

    final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted
    

    这个表明在设置了intercepted为true之后,cancelChild就变成true了,这个是关键。这样在执行dispatchTransformedTouchEvent方法的时候,子View的action的值就被设置为cancel,然后一级一级的传递下去

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                View child, int desiredPointerIdBits) {
            final boolean handled;
            final int oldAction = event.getAction();
            if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
                event.setAction(MotionEvent.ACTION_CANCEL);
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    handled = child.dispatchTouchEvent(event);
                }
                event.setAction(oldAction);
                return handled;
            }
            ..............................................
    }
    

    最终,mFirstTouchTarget被置为null

    if (cancelChild) {
        if (predecessor == null) {
            mFirstTouchTarget = next;
        } else {
            predecessor.next = next;
        }
        target.recycle();
        target = next;
        continue;
    }
    

    由于之前设置了clickable,所以在下次循环过程中执行的将是自己父类View的dispatchTouchEvent

    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                            TouchTarget.ALL_POINTER_IDS);
    

    这12个实验结果你要是能完全想明白为什么是这样的话,你可以高声大喊,View事件传递我全明白了!!!!
    要是你有一个想不明白原因的,请你还是耐心的再看一遍吧。

    心得体会

    其实从开始学习这套源码到今天博客的完成,我大致花费了3天时间,来来回回反复看源码看了很多遍终于理解透彻了。希望大家也能耐下心来来回多读几遍,肯定每一遍给你的感悟都会不一样

    相关文章

      网友评论

        本文标题:通俗演义手势事件分发

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