Android事件分发之ACTION_MOVE与ACTION_UP的传递机制

目录

  • 引言
  • ACTION_MOVE与ACTION_UP的传递机制
    • mFirstTouchTarget作用
    • mFirstTouchTarget为什么是链表结构

引言

关于Android事件分发机制网上相关的文章很多,多数都是一些较为基础并且重复的内容。本系列将从源码带领大家探究一些事件分发机制的“细枝末节”。但是在此之前,还是简单重复一下基础内容。即事件分发的三个重要方法:
事件传递给当前view时,dispatchTouchEvent方法会被调用。在方法内部会判断是否拦截事件onInterceptTouchEvent及如何处理事件onTouchEvent
一个完整的事件序列以Down开始,中间经过一个或者多个Move,最后以Up结束。
用一张图来总结ViewGroup的Down事件传递机制:

True
True
Flase
True
False
False
True
False
dispatchTouchEvent
onInterceptTouchEvent
mOnTouchListener.onTouch
End
子view dispatchTouchEvent
End
onTouchEvent
End
上层view或者Activity处理

事件Down传递给当前ViewGroup时,首先回调用dispatchTouchEvent方法,该方法内部会通过onInterceptTouchEvent方法判断是否拦截该Down事件。

如果拦截,则会首先判断ViewGroup是否设置了mOnTouchListener并且onTouch方法是否返回true,如果满足,则该事件处理结束。如果不满足,则会交给ViewGroup的onTouchEvent来处理,如果onTouchEvent返回true,则该事件处理结束;如果返回false,表示当前ViewGroup无法处理该事件,那么该事件回传递给上层View或者Activity来处理。
如果不拦截,则事件会交给子View来处理,回调用子View的dispatchTouchEvent方法,在子View的dispatchTouchEvent方法内部,也会判断是否设置了mOnTouchListener并且onTouch方法是否返回true,如果满足,则该事件处理结束。如果不满足,则会交给View的onTouchEvent来处理,如果onTouchEvent返回true,则该事件处理结束;如果返回False,则会继续走到父View的mOnTouchListener.onTouch判断逻辑中。

以上说的是Down事件的传递机制,我们知道如果一个View处理的Down事件,那么Move和Up事件也会自动交给它处理,那么这一过程是如何实现的呢?

ACTION_MOVE与ACTION_UP的传递机制

mFirstTouchTarget作用

在ViewGroup源码中使用了一个全局变量mFirstTouchTarget来记录是否有View处理了Down事件。mFirstTouchTarget默认为null,如果发现了View可以处理,那么就会把mFirstTouchTarget的值设置为对应的View。那么随之而来的Down和Up都会交给该View处理。
下面通过源码来说明:

首先看一下mFirstTouchTarget的赋值:


    /**
     * 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;
        mFirstTouchTarget = target;
        return target;
    }

上面的这段代码可以看出这是一个单链表的插入操作,将mFirstTouchTarget插入到链表的队头并且返回。再来看下这个方法的调用:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

            // 代码省略,主要是判断当前事件是否被cancel和intercepted
            if (!canceled && !intercepted) {

                // 处理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

                    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();
                        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);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            //... 省略部分代码

                            // 找到可以处理该事件的view
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // 省略部分代码
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);    // 重点在这里!!!!将可以处理该事件的view设置为mFirstTouchTarget
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
                    // 略部分代码
                    }
                }
            }
            // 省略部分代码。。。

        return handled;
    }

通过上面的代码可以看到,如果当前是Down事件,而且没有被拦截或者取消的话,就会遍历这个ViewGroup的children,找到可以处理事件的view,并且添加到mFirstTouchTarget单链表中。也就是说,mFirstTouchTarget单链表中存储的view是可以处理该Down事件的子view。

那么当Move事件及Up事件来的时候,又是如何根据mFirstTouchTarget的值来进行分发的呢?
继续看这部分源码:


    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        // 省略部分代码
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
            // 省略部分代码
            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                
                // 重点1   mFirstTouchTarget != null时 会走这个if
                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 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.
            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;
            // 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 {
                // 重点2 mFirstTouchTarget != null会走到这里
                // 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;
                }
            }
            
        }
        
        return handled;
    }


上面的代码部分一共标注了两处重点。在重点一的地方,我们看到ViewGroup仍然可能会通过onInterceptTouchEvent方法对事件进行拦截。假设ViewGroup没有进行拦截。那么在重点二的地方,就会遍历mFirstTouchTarget链表中的节点,并且将事件分发给对应的view,但是注意的是此时分发的不一定的Move或者Up事件,有可能是Cancel事件,详细可以查看Android事件分发之ACTION_CANCEL机制及作用。

mFirstTouchTarget为什么是链表结构

在上一小节可以看到,mFirstTouchTarget指向了可以处理事件的子view,但是直观上来说能够处理的子view只有一个,为什么会是一个链式结构呢?我们再通过源码看下mFirstTouchTarget的插入时机。

 if (actionMasked == MotionEvent.ACTION_DOWN
        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        // 。。。
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
    }

我们可以看到,在三种情况下,可能会走到addTouchTarget方法中。分别看下这三种情况:

1.actionMasked == MotionEvent.ACTION_DOWN,这个就是我们所说的Down事件。
2.actionMasked == MotionEvent.ACTION_HOVER_MOVE,这个是用于监听鼠标移动的事件。暂时忽略。
3.split && actionMasked == MotionEvent.ACTION_POINTER_DOWN,重点来看下这个。MotionEvent.ACTION_POINTER_DOWN出现在多指触控时。第一根按下的手指触发ACTION_DOWN事件,之后按下的手指触发ACTION_POINTER_DOWN事件。
所以当有多指进行触控的时候,addTouchTarget方法可能会被调用多次,mFirstTouchTarget以链式结构存储对应的view。
下面我们用代码来验证一下我们的结论:


import android.content.Context
import android.support.constraint.ConstraintLayout
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import java.lang.reflect.Field

class CustomViewGroup @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        // Log.d("TAG", "${ev?.action}:CustomViewGroup onInterceptTouchEvent")
        return false
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        // Log.d("TAG", "${event?.action}:CustomViewGroup onTouchEvent")
        return true
    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        super.dispatchTouchEvent(ev)

        ev?.let {
            if (it.action == MotionEvent.ACTION_DOWN) {
                val count = getField(this, this.javaClass, "mFirstTouchTarget")
                Log.d("TAG", "MotionEvent.ACTION_DOWN-->mFirstTouchTarget count:$count")
            }
            if ((it.action and MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) {
                val count = getField(this, this.javaClass, "mFirstTouchTarget")
                Log.d("TAG", "MotionEvent.ACTION_POINTER_DOWN-->mFirstTouchTarget count:$count")
            }
        }

        return true
    }


}

fun getField(
    obj: Any,
    clazz: Class<*>, fieldName: String
): Int? {
    if (clazz.superclass == null) {
        return null
    }
    var field: Field? = null
    try {
        field = clazz.getDeclaredField(fieldName)
    } catch (e: NoSuchFieldException) {
        return getField(obj, clazz.superclass, fieldName)
    }

    var nodeCount = 0
    val declaredClasses = clazz.declaredClasses
    declaredClasses.iterator().forEach {
        if (it.simpleName == "TouchTarget") {
            field.isAccessible = true
            var mTouchTarget = field.get(obj)

            var next = it.getDeclaredField("next")
            while (mTouchTarget != null) {
                nodeCount++
                mTouchTarget = next.get(mTouchTarget)
            }
        }
    }

    return nodeCount
}

上面是一段kotlin代码。实现了一个自定义ViewGroup,继承于ConstraintLayout,在覆盖的dispatchTouchEvent方法中,判断当前的事件是MotionEvent.ACTION_DOWN或者MotionEvent.ACTION_POINTER_DOWN时,通过反射拿到ViewGroup中"mFirstTouchTarget"属性对应的链表中节点的数量。

我们看下布局文件:


<com.lee.myapplication.CustomViewGroup
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/parent"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <Button
            android:layout_width="160dp"
            android:layout_height="126dp"
            android:text="button1"
            android:gravity="center"
            android:textColor="#FFF"
            android:background="#8cFF0000"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:id="@+id/button1"
            android:layout_marginBottom="8dp" app:layout_constraintBottom_toTopOf="@+id/button2"/>


    <Button
            android:layout_width="159dp"
            android:layout_height="142dp"
            android:text="button2"
            android:gravity="center"
            android:textColor="#FFF"
            android:background="#8c00FF00"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:id="@+id/button2"
    />
    <Button
            android:layout_width="159dp"
            android:layout_height="142dp"
            android:text="button3"
            android:gravity="center"
            android:textColor="#FFF"
            android:background="#8c0000FF"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            android:id="@+id/button3"
            android:layout_marginTop="8dp" app:layout_constraintTop_toBottomOf="@+id/button2"/>

com.lee.myapplication.CustomViewGroup>

其实就是我们自定义的ViewGroup中有三个Button,效果图如下:

Android事件分发之ACTION_MOVE与ACTION_UP的传递机制_第1张图片
假设我们现在点击button1,查看Log:

04-23 13:51:30.017 13325-13325/com.lee.myapplication D/TAG: MotionEvent.ACTION_DOWN–>mFirstTouchTarget count:1

假设我们先点击了button1,然后手指不松开,又点击了button2:

04-23 13:52:55.599 13325-13325/com.lee.myapplication D/TAG: MotionEvent.ACTION_DOWN–>mFirstTouchTarget count:1
04-23 13:52:56.217 13325-13325/com.lee.myapplication D/TAG: MotionEvent.ACTION_POINTER_DOWN–>mFirstTouchTarget count:2

假设我们先点击了button1,手指不松开又点击了button2,手指不松开最后点击了button3:

04-23 13:53:56.316 13325-13325/com.lee.myapplication D/TAG: MotionEvent.ACTION_DOWN–>mFirstTouchTarget count:1
04-23 13:53:56.763 13325-13325/com.lee.myapplication D/TAG: MotionEvent.ACTION_POINTER_DOWN–>mFirstTouchTarget count:2
04-23 13:53:57.292 13325-13325/com.lee.myapplication D/TAG: MotionEvent.ACTION_POINTER_DOWN–>mFirstTouchTarget count:3

通过上面的结论,验证了"mFirstTouchTarget"的链式结构。

你可能感兴趣的:(Android事件分发)