通俗演义手势事件分发

作者: 皮球二二 | 来源:发表于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天时间,来来回回反复看源码看了很多遍终于理解透彻了。希望大家也能耐下心来来回多读几遍,肯定每一遍给你的感悟都会不一样

相关文章

  • 通俗演义手势事件分发

    由于目前还是处于比较闲置的状态,所以我依然在github上面乱看别人的优秀项目。最近看到一个仿今日头条详情页的效果...

  • 关于触摸

    事件分发 所有触摸(Event、手势、Button) 的事件分发流程都是一样的。都是根据HitTest 方法找到这...

  • 谈一谈安卓的事件分发

    事件分发的规则 安卓中的手势动作,都会产生MotionEvent对象;所谓点击事件的分发,其实就是对MotionE...

  • 事件的处理机制和手势的操作

    事件的处理机制和手势的操作 iOS中的事件分发## 事件的分类### Touch Events(多点触摸事件) 视...

  • Android 事件分发

    View 事件分发 事件的种类 手势类型事件名称说明按下MotionEvent.ACTION_DOWN一切事件的起...

  • Android 事件分发机制

    事件分发机制在android中非常常见,比如:手势滑动,自定义View,多点触控都有它的身影。事件分发的顺序是:A...

  • 《iOS事件触摸与手势》

    iOS事件触摸与手势 一、事件分发处理【由外到内】在iOS中发生触摸后,事件会加到UIApplication事件队...

  • Flutter 之 手势原理与手势冲突 (五十)

    手势的识别和处理都是在事件分发阶段的 1.手势识别原理 GestureDetector 是一个 Stateless...

  • View事件分发学习笔记

    首先推荐郭霖的真正的通俗易懂的View的事件分发文章: Android事件分发机制完全解析,带你从源码的角度彻底理...

  • Flutter完整开发实战详解(十三、全面深入触摸和滑动原理)

    本篇将带你深入了解 Flutter 中的手势事件传递、事件分发、事件冲突竞争,滑动流畅等等的原理,帮你构建一个完整...

网友评论

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

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