简介
Android开发中触摸事件是经常用到,除了对基本事件的处理,另外一个很重要的就是事件的分发,事件的分发是指在一个屏幕中那么多的视图,究竟是遵循什么规则分发,从而将整个事件完成
以下代码均来源与Android的API25的源码
事件
首先说一下Android中常用的触摸事件
ACTION_DOWN:手指按下触摸,一定是某一个事件的开端
ACTION_MOVE:手指移动
ACTION_UP:手机松开
正常来说一个序列会有
DOWN->UP(手指按下然后松开,类似点击,虽然点击的判断没有那么简单)
DOWN->MOVE...->MOVE->UP(手指按下然后移动,最后松开)
等等
ACTION_POINTER_DOWN:当前已经有一个手指触发了DOWN事件,此时有其它手指按下的时候会触发的事件。
ACTION_POINTER_UP:当前有手指不再触摸,并且一定还有手指在继续触摸的时候触发的事件。
getActionIndex:每一个手指触摸到屏幕之后都会分配一个index,这个index会随着手指离开之类的情况变化,但是可以通过index获取当前手指唯一对应的PointerId。
getPointerId:每一个手指触摸到屏幕之后都会分配一个唯一的id,后续有手指离开之类的话也不会发生变化。
分发
在Android中,触摸事件要想到指定的视图上面,那么当手指按下的时候,事件需要层层向下进行分发。而事件首先是来到activity上
Activity
/**
* Activity进行触摸事件的分发
* @param ev 当前对应的事件
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//可以看到,将事件的分发交给了window
if (getWindow().superDispatchTouchEvent(ev)) {
return true;//如果window成功将事件分发下去,则当前事件结束
}
//否则事件回到Activity的onTouchEvent(ev)进行处理
//该方法默认为空,需要自己处理
return onTouchEvent(ev);
}
Activity将当前事件分发给了window,
其中window的实现类为PhoneWindow
//实际上就是Activity的顶层视图
private DecorView mDecor;
/**
* 实际上就是将事件分发给了DecorView
*/
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
PhoneWindow将事件再次分发到DecorView,DecorView是一个Activity的视图的起点,Android的视图结构本身是一个视图树,那么对于一个Activity来说,DecorView就是视图树的根节点。
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks
/**
* 交给父类处理
*/
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
实际上DecorView就是一个FrameLayout,那么这里委托最后就是到了ViewGroup来处理
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//... 去掉一些代码
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 手指按下事件,也是所有事件的开头
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 还原一些标志,主要是还原Touch链表为null,即mFirstTouchTarget为null
cancelAndClearTouchTargets(ev);
resetTouchState();
}
final boolean intercepted;
//1.当前事件为DOWN事件,可以理解为一次新的事件的开始
//2.mFirstTouchTarget != null意味着之前DOWN事件已经有子视图处理,现在是非DOWN的后续事件
//当DOWN事件发下去之后,对于任何ViewGroup来说如果还有子视图(ViewGroup/View)来处理事件
//那么对于这些ViewGroup来说自身的mFirstTouchTarget必然不会为空,从顶向下看形成了一个事件链条
//对于后续事件来说,如果没有禁止当前ViewGroup拦截事件,则事件还是会先询问ViewGroup是否拦截
//也就是说每一个事件都会尝试先问父布局是否拦截事件,也就是说父布局有处理事件的优先权
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//获得当前ViewGroup是否被禁止尝试拦截事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {//允许当前ViewGroup去尝试拦截事件
intercepted = onInterceptTouchEvent(ev);//尝试去拦截当前事件
ev.setAction(action); //还原事件,避免在onInterceptTouchEvent(ev)过程中事件被修改
} else {//当前ViewGroup不拦截事件
intercepted = false;
}
} else {//DOWN事件没有子视图处理,非DOWN的事件默认都由当前ViewGroup处理
intercepted = true;
}
//...
// 标记当前是否为ACTION_CANCEL事件
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
//这个属性默认为true,实际上可以在xml中通过splitMotionEvents设置
//实际是用于标志当前ViewGroup是否允许将事件分发到两个同级的子视图
//后面会说到
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {//当前并非取消事件,并且当前ViewGroup不拦截事件
// ...
//1.当前是处理DOWN事件
//2.当前是否允许分发同级事件
//如果不允许,则不会重新查找子视图来接收ACTION_POINTER_DOWN
//那么将会把ACTION_POINTER_DOWN交给之前处理了事件的视图
//如果允许,会重新查找子视图来接收ACTION_POINTER_DOWN,此时mFirstTouchTarget有可能出现多个节点的情况
//ACTION_HOVER_MOVE不考虑,基本没用过
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex();
//每一个手指的触摸事件的PointerId都是固定的,0-1-2...N
//设计的时候通过移位来表示即 0000 .... 0010表示id为1
//0000 .... 0111 表示当前手指有3个,id分别为0,1,2
//这里如果允许同级分发的话
//idBitsToAssign实际上就是用于记录当前事件的PointerId,类似0000 .... 0001
//不允许同级,使用默认的-1
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
//将之前TouchTarget链表中有着相同PointerId的节点移除
removePointersFromTouchTargets(idBitsToAssign);
//获得当前ViewGroup的所有孩子数目
final int childrenCount = mChildrenCount;
//如果当前有可以分发的子视图
if (newTouchTarget == null && childrenCount != 0) {
//获取当前DOWN事件的坐标
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
//获得一个子视图组,这里的子视图组顺序会影响到那一个子视图可以优先处理DOWN事件
//在setChildrenDrawingOrderEnabled的基础上,可以通过getChildDrawingOrder改变映射
//具体见buildTouchDispatchChildList
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//倒序遍历,如果没有设置setChildrenDrawingOrderEnabled的情况,默认是按照正序
//简单说就是在一个xml中,节点位置越靠后的越先
//比方说FrameLayout中有两个全屏的View,那么在xml中后面的View会优先(不考虑Z轴的前提)
for (int i = childrenCount - 1; i >= 0; i--) {
//之前可能通过setChildrenDrawingOrderEnabled,然后重写了getChildDrawingOrder改变了位置映射
//所以后续都要映射回原来View[]的下标
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//...
//判断当前view是否可以处理当前事件,
//主要是要求当前view可见并且当前事件的坐标在view的范围内
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;//否则继续循环查找下一个可以接收这个事件的view
}
//当前子视图可以接收当前事件
//尝试从当前事件节点链表中查找当前view,链表中不应该重复
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
//可能出现DOWN和ACTION_POINTER_DOWN同时由一个子视图处理
//此时复用链表中的一个节点即可
//修改节点对应的PointerId
//类似于0000 .... 0011表示当前视图处理id为0和1的两个手指事件
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//尝试进行DOWN、ACTION_POINTER_DOWN的事件分发
//这里实际上是一个递归调用,将事件交给当前ViewGroup的下一级ViewGroup
//然后下一级ViewGroup重新走DispatchTouchEvent进行分发
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//有视图成功处理了当前事件
//... 记录一些DOWN事件的信息
//将事件节点添加到链表的头部
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//标记DOWN事件已经分发给了新的节点处理
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
//...
}
//上面主要是处理当前ViewGroup不拦截事件,然后分发DOWN事件给子视图的逻辑
//如果之前有子视图处理了DOWN事件,同时默认split为true
//此时如果是ACTION_POINTER_DOWN事件,但是却没有找到合适的子视图处理
if (newTouchTarget == null && mFirstTouchTarget != null) {
//这里相当于找到最开始处理DOWN事件的节点
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
//标记当前节点处理当前手指,简单理解就是一个手指对应一个id
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
if (mFirstTouchTarget == null) {
//1.当前DOWN事件没有子视图处理,那么最后会到这里由当前ViewGroup处理
//形象来说就是把事件从顶向下发送,如果最下面的视图都不处理,那么事件会向上传递
//2.当前事件本来就被当前ViewGroup拦截,无论是DOWN还是MOVE之类的事件,mFirstTouchTarget也会一直为null
//直接把事件交给当前ViewGroup处理即可
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {//之前事件已经分发给了子视图处理
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
//链表在一个手指的前提下基本可以认为只有一个节点,但是有的时候比方说允许split,而且当前DOWN和ACTION_POINTER_DOWN所处理的子视图不同,此时的节点数量就会大于1
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
//因为之前专门处理了DOWN/POINTED_DOWN事件,这里可以直接标记事件已经处理
handled = true;
} else {//到这里的一般都是MOVE、UP之类的事件
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//在之前DOWN事件已经分发给了指定的子视图的前提下
//1.如果当前ViewGroup要求拦截事件
//假设现在有一个MOVE事件到来,但是当前ViewGroup要拦截MOVE事件
//这意味这事件序列不能交给之前的子视图处理
//此时向之前处理事件的子视图分发一个cancel事件,告诉它这个事件序列被取消了
//此时子视图的事件序列完成
//2.当前ViewGroup不拦截事件
//当前事件直接交给上一次处理事件的子视图即可
//这里会通过一直向下分发起到最终到之前的子视图
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
//因为当前ViewGroup拦截当前事件,导致子视图的事件被取消
//此时回收节点,意味着mFirstTouchTarget链表为null,事件会给当前ViewGroup处理
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//当前事件已经取消、或者是ACTION_UP
//还原状态,主要是清空TouchTarget链表等操作
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
//当前允许同级分发,此时为ACTION_POINTER_UP事件,意味着有一个手指离开
//此时将该手指对应的链表节点移除,后续不应该处理其它事件
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
//...
return handled;
}
1.首先可以看到对于每一个ViewGroup来说都会有一个TouchTarget链表,这个是用来存储当前ViewGroup确定分发事件的下一级视图(ViewGroup或者View),如果只是单纯的单指操作,那么TouchTarget只会有一个节点,如果有多指操作,并且接收的子视图不一样,此时会有多个节点。每一个ViewGroup都有一个链表,从顶往下看就可以形成一个事件传递链表。
2.可以通过在xml中设置splitMotionEvents="false"来禁止多个同级视图处理事件,常用的场景就是一个页面,有时候不希望有人同时点击两个地方,导致同时拉起两个页面或者出现一些不可预知问题。
3.对于简单的单指操作来说,事件本身可以看做一个序列,只要有一个子视图处理了DOWN事件,后续事件都会交给它继续处理,但是每一个事件是否可以到达子视图,要看当前ViewGroup是否拦截事件,如果ViewGroup拦截了事件,则子视图只会收到一个ACTION_CANCEL事件,然后事件重新开始。
4.可以通过requestDisallowInterceptTouchEvent禁止ViewGroup拦截事件,不过注意这个标志每一次DOWN事件开始分发的时候都会被重置。
/**
* getAndVerifyPreorderedIndex的实际执行方法
* 获得当前ViewGroup的视图组
* 后面默认是倒序遍历,主要用于确认哪一个视图优先可以尝试处理事件
*/
ArrayList<View> buildOrderedChildList() {
final int childrenCount = mChildrenCount;
if (childrenCount <= 1 || !hasChildWithZ()) return null;
if (mPreSortedChildren == null) {
mPreSortedChildren = new ArrayList<>(childrenCount);
} else {
// callers should clear, so clear shouldn't be necessary, but for safety...
mPreSortedChildren.clear();
mPreSortedChildren.ensureCapacity(childrenCount);
}
//这个可以通过setChildrenDrawingOrderEnabled设置,默认false
final boolean customOrder = isChildrenDrawingOrderEnabled();
for (int i = 0; i < childrenCount; i++) {
//如果customOrder为true,这里可以通过重写ViewGroup的getChildDrawingOrder来改变接收事件的视图顺序
//默认就是i
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
//拿出当前视图
final View nextChild = mChildren[childIndex];
//在Android5.0之后提出的Z轴概念
final float currentZ = nextChild.getZ();
//无论是否重新映射了下标位置,但是都会按照Z轴排序
//简单说就是Z坐标越大的在列表的位置越后
//列表是按照Z坐标的升序排列
int insertIndex = i;
while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
insertIndex--;
}
mPreSortedChildren.add(insertIndex, nextChild);
}
return mPreSortedChildren;
}
可以通过setChildrenDrawingOrderEnabled和重写getChildDrawingOrder来改变优先尝试处理事件的位置,比方说ViewA的index为0,ViewB的index为1,此时事件同时在ViewA和ViewB的区域,事件默认会先到ViewB,如果要到ViewA,可以倒序映射。不过实际上这两个参数就是用于改变绘制的顺序,传统的是index小的先画即ViewA,然后ViewB可能会盖住它,此时事件先到ViewB也是正常,如果颠倒绘制顺序,那么就会先画ViewB,ViewA会盖住ViewB,此时事件先到ViewA也是正常的。
之前提到事件的分发
/**
* 分发指定的事件给指定的视图
* @param event 当前应该被分发的事件
* @param cancel 当前事件是否被取消
* @param child 当前应该被分发事件的子视图,null表示当前ViewGroup处理
* @param desiredPointerIdBits 当前事件的id
* @return true表示当前事件分发完成,并且被处理,false表示没视图处理当前事件
*/
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) {//由当前ViewGroup处理
handled = super.dispatchTouchEvent(event);
} else {//将事件交由子视图处理
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// 获得当前有多少个手指触摸中,并且可以获得对应id
// 比方0000 .... 0111表示3个手指,其中ID为0,1,2
final int oldPointerIdBits = event.getPointerIdBits();
//与当前事件id
//比方说当前事件id为1,即0000 .... 0010
//得到就是0000 .... 0010
//除非当前事件id不在记录的触摸点中,此时会得到0
//对于一个手指的事件来说,oldPointerIdBits总是等于newPointerIdBits
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
//因为一些原因可能产生了一个没有触摸点对应的事件
//丢弃当前事件
if (newPointerIdBits == 0) {
return false;
}
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
//当前是同一个触摸手指的事件,不需要进行事件的转换
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
//当前有多个手指的触摸中,此时要转换事件
//比方说在Split模式下,POINTED_DOWN和POINTED_UP在某一个视图中会被转换为DOWN和UP
//从而触发点击事件等等
transformedEvent = event.split(newPointerIdBits);
}
//将转换后的事件分发下去
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// 回收当前事件
transformedEvent.recycle();
return handled;
}
最终处理事件的时候会到super.dispatchTouchEvent,其实就是View的dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent event) {
//...
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
//...
//首先回调了OnTouchListener
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//如果OnTouchListener返回true
//不会继续调用OnTouchEvent
if (!result && onTouchEvent(event)) {
//实际上默认的OnClickListener是在默认的onTouchEvent里面处理的
result = true;
}
}
//...
return result;
}
可以看到,实际上处理事件的时候,如果通过setOnTouchListener设置了OnTouchListener,则优先回调OnTouchListener,如果OnTouchListener处理后返回true,则当前事件处理完成。
否则继续onTouchEvent方法,其中onTouchEvent方法里面默认实现了onClickListener的回调。
结语
可以简单的总结Android中的事件传递成几个规则
1.事件是序列化的,即一般情况下,DOWN先开始,然后是MOVE或者UP等操作,这就是一个完整序列。
2.每一个事件父布局总是有优先拦截权利的,子视图可以通过requestDisallowInterceptTouchEvent禁止父布局拦截事件,合理利用这些就可以处理大部分手势冲突。
3.当事件到一个视图处理的时候onTouchListener>onTouchEvent>onClickListener
网友评论