这是Android触摸事件系列文章的第一篇。
大领导安排任务会经历一个“递”的过程:大领导先把任务告诉小领导,小领导再把任务告诉小明。也可能会经历一个“归”的过程:小明告诉小领导做不了,小领导告诉大领导任务完不成。然后,就没有然后了。。。。
Android触摸事件和领导安排任务的过程很相似,也会经历“递”和“归”。这一篇会试着阅读源码来分析ACTION_DOWN
事件的这个递归过程。
(ps: 下文中的 粗斜体字 表示引导源码阅读的内心戏)
分发触摸事件起点
写一个包含ViewGroup
、View
、Activity
的demo,并在所有和touch有关的方法中打log。当触摸事件发生时,Activity.dispatchTouchEvent()
总是第一个被调用,就以这个方法为切入点:
public class Activity{
private Window mWindow;
//分发触摸事件
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//让PhoneWindow帮忙分发触摸事件
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
//获得PhoneWindow对象
public Window getWindow() {
return mWindow;
}
//参数太长,省略了
final void attach(...) {
...
//构造PhoneWindow
mWindow = new PhoneWindow(this, window, activityConfigCallback);
...
}
}
Activity
将事件传递给PhoneWindow
:
public class PhoneWindow extends Window implements MenuBuilder.Callback {
// This is the top-level view of the window, containing the window decor.
//一个窗口的顶层视图
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
//将触摸事件交给DecorView分发
return mDecor.superDispatchTouchEvent(event);
}
}
//DecorView继承自ViewGroup
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks{
public boolean superDispatchTouchEvent(MotionEvent event) {
//事件最终由ViewGroup.dispatchTouchEvent()分发触摸事件
return super.dispatchTouchEvent(event);
}
}
-
PhoneWindow
继续将事件传递给DecorView
,最终调用了ViewGroup.dispatchTouchEvent()
- 至此可以做一个简单的总结:触摸事件的传递从
Activity
开始,经过PhoneWindow
,到达顶层视图DecorView
。DecorView
调用了ViewGroup.dispatchTouchEvent()
。
触摸事件之“递”
- 在分析View绘制时,也遇到过“dispatchXXX”函数
ViewGroup.dispatchDraw()
,它用于遍历孩子并触发它们自己绘制自己。那dispatchTouchEvent()
会不会也遍历孩子并将触摸事件传递给它们? 带着这个疑问来看下源码:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!canceled && !intercepted) {
...
//遍历孩子
for (int i = childrenCount - 1; i >= 0; i--) {
//按照索引顺序或者自定义绘制顺序遍历孩子
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : I;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
...
//如果孩子不在触摸区域则直接跳过
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
...
//转换触摸坐标并分发给孩子(child参数不为null)
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//这里的代码也很关键,先埋伏笔1
}
...
}
}
if (mFirstTouchTarget == null) {
//这里的代码也很关键,先埋伏笔2
} else {
//这里的代码也很关键,先埋伏笔3
}
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
// Perform any necessary transformations and dispatch.
//进行必要的坐标转换然后分发触摸事件
if (child == null) {
//这里的代码也很关键,先埋伏笔3
} else {
//将ViewGroup坐标系转换为它孩子的坐标系(坐标原点从ViewGroup左上角移动到孩子左上角)
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;
}
}
果然没猜错!父控件在ViewGroup.dispatchTouchEvent()
中会遍历孩子并将触摸事件分发给被点中的子控件,如果子控件还有孩子,触摸事件的“递”将不断持续,直到叶子结点。 最终View
类型的叶子结点调用的是View.dispatchTouchEvent()
:
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
//1.通知触摸监听器OnTouchListener
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//2.调用onTouchEvent()
//只有当OnTouchListener.onTouch()返回false时,onTouchEvent()才有机会被调用
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
//返回值就是onTouch()或者onTouchEvent()的返回值
return result;
}
ListenerInfo mListenerInfo;
//监听器容器类
static class ListenerInfo {
...
private OnTouchListener mOnTouchListener;
...
}
//设置触摸监听器
public void setOnTouchListener(OnTouchListener l) {
//将监听器存储在监听器容器中
getListenerInfo().mOnTouchListener = l;
}
//获得监听器管理实例
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
}
View.dispatchTouchEvent()
是传递触摸事件的终点,消费触摸事件的起点。- 消费触摸事件的标志是调用
OnTouchListener.onTouch()
或View.onTouchEvent()
,前者优先级高于后者。只有当没有设置OnTouchListener
或者onTouch()
返回false
时,View.onTouchEvent()
才会被调用。 -
读到这里,画一张图总结一下触摸事件之“递”:
- 图中ViewGroup层后面的N表示在Activity层和View层之间可能有多个ViewGroup层。
- 图中自上而下一共有三类层次,触摸事件会从最高层次开始沿着箭头往下层传递。
- 为简单起见,图中省略了另一种触摸事件的处理方式:
OnTouchListener.onTouch
- 图示触摸事件的传递只是众多传递场景中的一种:被点击的View嵌套在ViewGroup中,ViewGroup在Activity中。
触摸事件之“归”
触摸事件之所以在“递”之后还会发生“归”是因为:分发触摸事件的函数还没有执行完。沿着刚才调用链相反的方向重新看一遍源码:
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
/**
* Implement this method to handle touch screen motion events.
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
* 返回true表示触摸事件被消费,否则表示未被消费
*/
public boolean onTouchEvent(MotionEvent event) {
...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
//省略了对不同触摸事件的默认处理
...
//只要控件是可点击的,就表示触摸事件已被消费
return true;
}
//若控件不可点击则不消费触摸事件
return false;
}
}
View.dispatchTouchEvent()
调用了View.onTouchEvent()
后并没有执行完。View.onTouchEvent()
的返回值会影响View.dispatchTouchEvent()
的返回值:
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
//返回当前View是否消费触摸事件的布尔值
return result;
}
同样的,ViewGroup.dispatchTouchEvent()
调用了View.dispatchTouchEvent()
后也没有执行完,View.dispatchTouchEvent()
的返回值会影响ViewGroup.dispatchTouchEvent()
的返回值:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
//触摸链头结点
private TouchTarget mFirstTouchTarget;
...
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!canceled && !intercepted) {
...
//遍历孩子
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);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
//遍历触摸链分发触摸事件给所有想接收的孩子
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
//如果已经将触摸事件分发给新的触摸目标,则返回true
handled = true;
} else {
//这里的代码很重要,继续埋伏笔,待下一篇分析。
}
predecessor = target;
target = next;
}
}
...
//返回触摸事件是否被孩子或者自己消费的布尔值
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 {
//将触摸事件分发给孩子
}
...
return handled;
}
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
* 添加View到触摸链头部
* @param child View
* @param pointerIdBits
* @return 新触摸目标
*/
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
}
- 上面这段代码补全了上一节中买下的伏笔。原来当孩子愿意消费触摸事件时,
ViewGroup
会将其接入“触摸链”,如果触摸链中没有结点则表示没有孩子愿意消费事件,此时ViewGroup
只能自己消费事件。ViewGroup
是View
的子类,他们消费触摸事件的方式一摸一样,都是通过View.dispatchTouchEvent()
调用View.onTouchEvent()
或OnTouchListener.onTouch()
。 - 沿着回溯链,再向上“归”一步:
public class Activity {
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
//如果布局中有控件愿意消费触摸事件,则返回true,onTouchEvent()不会被调用
return true;
}
return onTouchEvent(ev);
}
}
View
、ViewGroup
和Activity
,虽然它们分发触摸事件的逻辑不太一样,但基本结构都和上面这段代码神似,用伪代码可以写成:
//“递”
if(分发事件给孩子){
如果孩子消费了事件 直接返回(将触摸事件被消费这一事实往上传递)
}
//“归”
如果孩子没有消费事件,则自己消费事件
“分发事件给孩子”这个函数的调用表示“递”,即将触摸事件传递给下层。“分发事件给孩子”这个函数的返回表示“归”,即将触摸事件的消费结果回溯给上层,以便上层采取进一步的行动。
同样的套路,用图片总结下触摸事件之“归”:
- 这张图是对图1描述场景的补全。图中黑色的线表示触摸事件的传递路径,灰色的线表示触摸事件回溯的路径。
- 因为
View.onTouchEvent()
返回true,表示消费触摸事件,所以ViewGroup.onTouchEvent()
以及Activity.onTouchEvent()
都不会被调用。
- 这张图是对图1描述场景的扩展。图中黑色的线表示触摸事件的传递路径,灰色的线表示触摸事件回溯的路径。
- 图示所对应的场景是:被点击的
View
不消费触摸事件,而ViewGroup
在onTouchEvent()
中返回true
自己消费触摸事件。
- 这张图是对图1描述场景的扩展。图中黑色的线表示触摸事件的传递路径,灰色的线表示触摸事件回溯的路径。
- 图示所对应的场景是:被点击的
View
和ViewGroup
都不消费触摸事件,最后只能由Activity
来消费触摸事件。
总结
-
Activity
接收到触摸事件后,会传递给PhoneWindow
,再传递给DecorView
,由DecorView
调用ViewGroup.dispatchTouchEvent()
自顶向下分发ACTION_DOWN
触摸事件。 -
ACTION_DOWN
事件通过ViewGroup.dispatchTouchEvent()
从DecorView
经过若干个ViewGroup
层层传递下去,最终到达View
。 - 每个层次都可以通过在
onTouchEvent()
或OnTouchListener.onTouch()
返回true
,来告诉自己的父控件触摸事件被消费。只有当下层控件不消费触摸事件时,其父控件才有机会自己消费。 - 触摸事件的传递是从根视图自顶向下“递”的过程,触摸事件的消费是自下而上“归”的过程。
读到这里可能对于触摸事件还充满诸多疑问:
-
ViewGroup
层是否有办法拦截触摸事件? -
ACTION_DOWN
只是触摸序列的起点,后序的ACTION_MOVE
、ACTION_UP
、ACTION_CANCEL
是如何传递的?
这些问题会在下一篇继续分析。