关于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事件也会自动交给它处理,那么这一过程是如何实现的呢?
在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
指向了可以处理事件的子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,效果图如下:
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"的链式结构。