这是Android触摸事件系列的第二篇,系列文章目录如下:
把上一篇中领导分配任务的故事,延展一下:
大领导安排任务会经历一个“递”的过程:大领导先把任务告诉小领导,小领导再把任务告诉小明。也可能会经历一个“归”的过程:小明告诉小领导做不了,小领导告诉大领导任务完不成。然后,就没有然后了。。。。但如果这次完成了任务,大领导还会继续将后序任务分配给小明。
故事的延展部分和今天要讲的ACTION_DONW
后序事件很类似,先来回答上一篇中遗留的另一个问题“拦截事件”:
拦截事件
ViewGroup
在遍历孩子分发触摸事件前还有一段拦截逻辑:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// Check for interception.
//检查ViewGroup是否要拦截触摸事件的下发
final boolean intercepted;
//第一个条件表示拦截ACTION_DOWN事件
//第二个条件表示拦截ACTION_DOWN事件已经分发给孩子,现在拦截后序事件
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 {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
...
//当事件没有被拦截的时候,将其分发给孩子
if (!canceled && !intercepted) {
//遍历孩子并将事件分发给它们
//如果有孩子声称要消费事件,则将其添加到触摸链上
//这段逻辑在上一篇中分析过,这里就省略了
}
}
//将触摸事件分发给触摸链
if (mFirstTouchTarget == null) { //没有触摸链
//如果事件被ViewGroup拦截,则触摸链为空,ViewGroup自己消费事件
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
...
}
}
//返回true表示拦截事件,默认返回false
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
...
if (child == null) {
//ViewGroup孩子都不愿意接收触摸事件或者触摸事件被拦截 则其将自己当成View处理(调用View.dispatchTouchEvent())
handled = super.dispatchTouchEvent(transformedEvent);
}
...
}
}
当允许拦截时,onInterceptTouchEvent()
会被调用,如果重载这个方法并且返回true
,表示ViewGroup
要对事件进行拦截,此时不再将事件分发给孩子而是自己消费(通过调用View.dispatchTouchEvent()
最终走到ViewGroup.onTouchEvent()
)。
用一张图总结一下:
图1
- 图中黑色的箭头表示触摸事件传递的路径,灰色的箭头表示触摸事件消费的回溯路径。
onInterceptTouchEvent()
返回true
,导致onTouchEvent()
被调用,因为onTouchEvent()
返回true
,导致dispatchTouchEvent()
返回true
。 - 准确的说,拦截触摸事件的受益者是所有上层的
ViewGroup
(包括自己),因为触摸事件不再会向下层的View
传递。
ACTION_MOVE和ACTION_UP
上一篇在阅读源码的时候,埋下了一个伏笔,现在将其补全:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
//触摸链头结点
private TouchTarget mFirstTouchTarget;
...
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!canceled && !intercepted) {
...
//当ACTION_DOWN的时候才遍历寻找消费触摸事件的孩子,若找到则将其加入到触摸链
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//遍历孩子
for (int i = childrenCount - 1; i >= 0; i--) {
...
//转换触摸坐标并分发给孩子(child参数不为null)
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
//有孩子愿意消费触摸事件,将其插入“触摸链”
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//表示已经将触摸事件分发给新的触摸目标
alreadyDispatchedToNewTouchTarget = true;
break;
}
...
}
}
}
if (mFirstTouchTarget == null) {
//如果没有孩子愿意消费触摸事件,则自己消费(child参数为null)
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
//触摸链不为null,表示有孩子消费了ACTION_DOWN
else {
//将伏笔补全
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
//遍历触摸链将ACTION_DOWN的后序事件分发给孩子
while (target != null) {
final TouchTarget next = target.next;
//上一篇分析了,ACTION_DOWN会走这里
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
//如果已经将触摸事件分发给新的触摸目标,则返回true
handled = true;
}
//ACTION_DONW的后序事件走这里
else {
...
//将触摸事件分发给触摸链上的触摸目标
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
...
}
predecessor = target;
target = next;
}
}
...
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//如果是ACTION_UP事件,则将触摸链清空
resetTouchState();
}
return handled;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
// Perform any necessary transformations and dispatch.
//进行必要的坐标转换然后分发触摸事件
if (child == null) {
//ViewGroup孩子都不愿意消费触摸事件 则其将自己当成View处理(调用View.dispatchTouchEvent())
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);
}
...
return handled;
}
/**
* Resets all touch state in preparation for a new cycle.
* 重置Touch标志
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
/**
* Clears all touch targets.
* 清空触摸链
*/
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
}
触摸事件是一个序列,序列总是以ACTION_DOWN
开始,紧接着有ACTION_MOVE
和ACTION_UP
。当ACTION_DOWN
发生时,ViewGroup.dispatchTouchEvent()
会将愿意消费触摸事件的孩子存储在触摸链中,当后序事件会分发给触摸链上的对象。
用一种图总结一下:
图2
- 图中黑色箭头表示
ACTION_DOWN
事件的传递路径,灰色箭头表示ACTION_MOVE
和ACTION_UP
事件的传递路径。即只要有视图声称消费ACTION_DOWN
,则其后序事件也传递给它,不管它是否声称消费ACTION_MOVE
和ACTION_UP
,如果它不消费,则后序事件会像上一篇分析的ACTION_DOWN
一样向上回溯给上层消费。
- 图中黑色箭头表示
ACTION_DOWN
事件的传递路径,灰色箭头表示ACTION_MOVE
和ACTION_UP
事件的传递路径。即所有视图都不消费ACTION_DOWN
,则其后序事件只会传递给Activity.onTouchEvent()
。
ACTION_CANCEL
把领导布置任务的故事继续延展一下:大领导给小领导布置了任务1,小领导把他传递给小明,小明完成了。紧接着大领导给小领导布置了任务2,小领导决定自己处理任务2,于是他和小明说后序任务我来接手,你可以忙别的事情。
故事对应的触摸事件传递场景是:Activity
将ACTION_DOWN
传递给ViewGroup
,ViewGroup
将其传递给View
,View
声称消费ACTION_DOWN
。Activity
继续将ACTION_MOVE
传递给ViewGroup
,但ViewGroup
对其做了拦截,此时ViewGroup
会发送ACTION_CANCEL
事件给View
。
看下源码:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
public boolean dispatchTouchEvent(MotionEvent ev) {
//检查ViewGroup是否要拦截触摸事件的下发
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;
}
}
...
//如果孩子消费ACTION_DOWN事件,则会在这里将其添加到触摸链中
if (!canceled && !intercepted) {
...
}
//将触摸事件分发给触摸链
if (mFirstTouchTarget == null) { //没有触摸链 表示当前ViewGroup中没有孩子愿意接收触摸事件
//将触摸事件分发给自己
} else {
// 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 {
//如果事件被拦截则cancelChild为true
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//将ACTION_CANCEL事件传递给孩子
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
//如果发送了ACTION_CANCEL事件,将孩子从触摸链上摘除
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
...
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don‘t need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
//将ACTION_CANCEL事件传递给孩子
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
}
当孩子消费了ACTION_DOWN
事件,它的引用被会保存在父亲的触摸链中。当父亲拦截后序事件时,父亲会向触摸链上的孩子发送ACTION_CANCEL
事件,并将孩子从触摸链上摘除。后序事件就传递到父亲为止。
总结
经过两篇文章的分析,对Android触摸事件的分发有了初步的了解,得出了以下结论:
-
Activity
接收到触摸事件后,会传递给PhoneWindow
,再传递给DecorView
,由DecorView
调用ViewGroup.dispatchTouchEvent()
自顶向下分发ACTION_DOWN
触摸事件。 -
ACTION_DOWN
事件通过ViewGroup.dispatchTouchEvent()
从DecorView
经过若干个ViewGroup
层层传递下去,最终到达View
。 - 每个层次都可以通过在
onTouchEvent()
或OnTouchListener.onTouch()
返回true
,来告诉自己的父控件触摸事件被消费。只有当下层控件不消费触摸事件时,其父控件才有机会自己消费。 - 触摸事件的传递是从根视图自顶向下“递”的过程,触摸事件的消费是自下而上“归”的过程。
-
ACTION_MOVE
和ACTION_UP
会沿着刚才ACTION_DOWN
的传递路径,传递给消费了ACTION_DOWN
的控件,如果该控件没有声明消费这些后序事件,则它们也像ACTION_DOWN
一样会向上回溯让其父控件消费。 - 父控件可以通过在
onInterceptTouchEvent()
返回true
来拦截事件向其孩子传递。如果在孩子已经消费了ACTION_DOWN
事情后才进行拦截,父控件会发送ACTION_CANCEL
给孩子。
网友评论