目录
引言ACTION_MOVE与ACTION_UP的传递机制mFirstTouchTarget作用mFirstTouchTarget为什么是链表结构引言
关于Android事件分发机制网上相关的文章很多,多数都是一些较为基础并且重复的内容。本系列将从源码带领大家探究一些事件分发机制的“细枝末节”。但是在此之前,还是简单重复一下基础内容。即事件分发的三个重要方法:
事件传递给当前view时,dispatchTouchEvent
方法会被调用。在方法内部会判断是否拦截事件onInterceptTouchEvent
及如何处理事件onTouchEvent
。
一个完整的事件序列以Down开始,中间经过一个或者多个Move,最后以Up结束。
用一张图来总结ViewGroup的Down事件传递机制:
事件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
插入到链表的队头并且返回。再来看下这个方法的调用:
@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {// 代码省略,主要是判断当前事件是否被cancel和interceptedif (!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 downfinal 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;// 遍历每一个子viewfor (int i = childrenCount - 1; i >= 0; i--) {final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);//... 省略部分代码// 找到可以处理该事件的viewif (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {// 省略部分代码newTouchTarget = addTouchTarget(child, idBitsToAssign); // 重点在这里!!!!将可以处理该事件的view设置为mFirstTouchTargetbreak;}// 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
的值来进行分发的呢?
继续看这部分源码:
@Overridepublic 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时 会走这个iffinal 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.Contextimport android.support.constraint.ConstraintLayoutimport android.util.AttributeSetimport android.util.Logimport android.view.MotionEventimport java.lang.reflect.Fieldclass 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? = nulltry {field = clazz.getDeclaredField(fieldName)} catch (e: NoSuchFieldException) {return getField(obj, clazz.superclass, fieldName)}var nodeCount = 0val declaredClasses = clazz.declaredClassesdeclaredClasses.iterator().forEach {if (it.simpleName == "TouchTarget") {field.isAccessible = truevar 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"
属性对应的链表中节点的数量。
我们看下布局文件:
<?xml version="1.0" encoding="utf-8"?><com.lee.myapplication.CustomViewGroupxmlns:android="/apk/res/android"xmlns:tools="/tools"xmlns:app="/apk/res-auto"android:id="@+id/parent"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><Buttonandroid: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"/><Buttonandroid: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"/><Buttonandroid: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,效果图如下:
假设我们现在点击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事件分发之ACTION_MOVE与ACTION_UP的传递机制》对你有帮助,请点赞、收藏,并留下你的观点哦!