theme: github
这是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 {
//'一个窗口的顶层视图'
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;
...
//'进行必要的坐标转换然后分发触摸事件'
if (child == null) {
//这里的代码也很关键,先埋伏笔4
} 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{
//'返回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 {
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
。View.dispatchTouchEvent()
被调用。View.dispatchTouchEvent()
是传递事件的终点,消费事件的起点。它会调用onTouchEvent()
或OnTouchListener.onTouch()
来消费事件。- 每个层次都可以通过在
onTouchEvent()
或OnTouchListener.onTouch()
返回true
,来告诉自己的父控件触摸事件被消费。只有当下层控件不消费触摸事件时,其父控件才有机会自己消费。 - 触摸事件的传递是从根视图自顶向下“递”的过程,触摸事件的消费是自下而上“归”的过程。
读到这里可能对于触摸事件还充满诸多疑问:
ViewGroup
层是否有办法拦截触摸事件?ACTION_DOWN
只是触摸序列的起点,后序的ACTION_MOVE
、ACTION_UP
、ACTION_CANCEL
是如何传递的?
这些问题会在下一篇继续分析。