【Android】View 事件分发机制与源码解析
View 的基本知识
View 的位置参数
按坐标系的不同分别有
- top、bottom、left 和 right 是相对父布局坐标系的偏移量。
- translationX 和 translationY 是属性动画系统相对父布局坐标系平移的额外偏移量,默认值为 0。
- mScrollX 和 mScrollY 是内容相对控件边界滑动的偏移量,默认值也为 0。
- x = left + translationX,y = top + translationY,二者是 View 的实际显示位置,首项的 4 个值是 final 值,逻辑处理按照 x 和 y 的值为基准。
getLocationOnScreen(int[])和getLocationInWindow(int[]),通过上述方法可以获得 View 在屏幕显示区域的绝对坐标和相对当前 Window 的坐标。
MotionEvent 和 TouchSlop
MotionEvent 是用户手指接触屏幕后产生的事件,分别有
- ACTION_DOWN,手指接触屏幕的瞬间
- ACTION_MOVE,手指在屏幕上移动
- ACTION_UP,手指离开屏幕的瞬间
通过 MotionEvent 的 getX 和 getRawX 方法可以获得此次点击事件相对接受事件的当前 View 和在屏幕显示区域的横轴绝对坐标,纵轴同理。
TouchSlop 表示滑动事件的最小认定距离,可以通过 ViewConfiguration.get(getContext()).getScaledTouchSlop() 获得该常量,在滑动控件内细心判别 TouchSlop 常量值可以有更好的用户体验
VelocityTracker 和 GestureDetector
VelocityTracker 速度追踪,用来追踪手指在滑动过程中的速度,包括水平和竖直方向的速度,使用方法是获取 VelocityTracker 实例后通过在每次事件回调中 addMovement 形成完整的事件序列,实例内部会计算相应速度,以及 VelocityTracker 是支持多指触控的速度识别的,系统为同时在屏幕发生 MotionEvent 的手指分配了 pointerId 编号,在 getXVelocity 等速度获取方法传入其编号即可获得速度信息
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);velocityTracker.computeCurrentVelocityTracker(units);
int vx = (int) velocityTracker.getXVelocity();
int vy = (int) velocityTracker.getYVelocity();velocityTracker.clear();
velocityTracker.recycle();
GestureDetector 手势检测,首先要创建 GestureDetector 对象并实现 OnGestureListener 接口,根据实际业务也可以实现 OnDoubleTapListener 从而监听双击行为,可以在 View 的 onTouchListener 等方法调用 GestureDetector 的 onTouchEvent 方法来监听各种回调方法
GestureDetector gestureDetector = new GestureDetector(context, new GestureDetector.OnGestureListener() {@Overridepublic boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {Log.d("Gesture", "Scroll dx=" + distanceX + " dy=" + distanceY);return true;}
}); // distanceX/Y 是事件序列两次移动的距离差
View 的事件分发机制
点击事件的传递规则
事件分发过程由 3 个重要的方法共同完成,分别是 dispatchTouchEvent 分发事件、onInterceptTouchEvent 拦截事件和 onTouchEvent 处理事件,伪代码逻辑是
public boolean dispatchTouchEvent(MotionEvent ev) {boolean consume = false;if (onInterceptTouchEvent(ev)) { // 控件可以拦截事件则调用控件的 onTouchEvent 处理事件consume = onTouchEvent(ev);} else {consume = child.dispatchTouchEvent(ev); // 传递给子控件分发}return consume; // 表示事件是否被消费
}
当 View 回调 onTouchEvent 方法开始处理事件时,如果其实现 OnTouchListener,则其中的 onTouch 方法会回调,如果该方法最终返回 false,则会回调当前 View 的 OnTouchEvent,在 onTouchEvent 方法中如果设置有 OnClickListener,则会回调其 onClick 方法。
点击事件在 Java 层的传递过程是 Activity -> Window -> DecorView,其中 Window 是位于 Activity 和屏幕内容之间起到桥梁作用的层抽象层级,管理 1 块可绘制区域的容器,其唯一实现类是 PhoneWindow。每个 Window 都有 1 个根视图实例 DecorView,DecorView 继承自 FrameLayout,Activity 调用 setContentView 方法时,传入的布局实例会添加到 DecorView 中,是 DecorView 的子控件。
如果上述分发代码的 consume 实例始终 false 未得到消费,则事件会最终传递给 Activity 处理,结合 View 树来看类似于递归的自底而上收束。
Android 开发艺术探索书中,对事件分发机制有一些结论,其分别是
- 1 个事件序列是 1 根手指接触屏幕 MotionEvent.ACTION_DOWN 到离开屏幕 MotionEvent.ACTION_UP 或事件取消 MotionEvent.ACTION_CANCEL 的过程。
- 1 个事件序列只能被 1 个 ViewGroup 拦截并处理,ViewGroup 拦截事件后,该事件所在序列的所有事件都会直接交给该实例处理,且 ViewGroup 的 onInterceptTouchEvent 方法不会再次被调用。
- View 开始处理事件时如果 onTouchEvent 对 MotionEvent.ACTION_DOWN 事件返回 false,则同一序列的其他事件都不会再交给该 View 处理,且会重新交给其父元素处理。
- View 只消耗 MotionEvent.ACTION_DOWN 事件时,该点击事件会消失,父元素和 Activity 无法接受处理,因为 Android 的点击事件中,MotionEvent.ACTION_DOWN 决定该序列的事件归属,如果 DOWN 被 View 消费,则后续的 MOVE 和 UP 都只会发给该 View。
- ViewGroup 默认不拦截任何事件,且只有 ViewGroup 能够拦截事件。
- View 的 onTouchEvent 默认消耗事件,除非该 View 不可点击,点击属性分为 clickable 和 longClickable,后者属性在控件中几乎都默认为 false,前者则会视情况而定。
所以,分发链 Activity -> Window -> ViewTree 中,优先寻找愿意消费事件序列 DOWN 事件的子元素,如果子元素没有愿意消费 DOWN 的则会向上查找父元素,事件序列可以被父元素提前拦截。
事件分发的源码分析
首先看 Activity 对点击事件的分发过程
public boolean dispatchTouchEvent(MotionEvent ev) {if (ev.getAction() == MotionEvent.ACTION_DOWN) {onUserInteraction(); // 用户进行了操作}if (getWindow().superDispatchTouchEvent(ev)) {return true;}return onTouchEvent(ev);
}
Activity 的 dispatchTouchEvent 方法传递到 PhoneWindow 的 superDispatchTouchEvent 方法,紧接着直接传递给 DecorView,DecorView 也会直接传递给父类 FrameLayout,其是 ViewGroup,所以最后会进入 ViewGroup 的 dispatchTouchEvent 方法
// PhoneWindow
public boolean superDispatchTouchEvent(MotionEvent event) {return mDecor.superDispatchTouchEvent(event);
}// DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {return super.dispatchTouchEvent(event); // 即FrameLayout
}
// ViewGroup.dispatchTouchEvent// 1. 清除残留状态
if (actionMasked == MotionEvent.ACTION_DOWN) {cancelAndClearTouchTarget(ev);resetTouchState();
}// 2. 拦截决策
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCELT) != 0;if (!disallowIntercept) {intercepted = onInterceptTouchEvent(ev);ev.setAction(action);} else {intercepted = false;}
} else {intercepted = true;
}// 3. 事件分发
final View[] children = mChildren;
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)) {continue;}newTouchTarget = getTouchTarget(child);if (newTouchTarget != null) {newTouchTarger.pointerIdBits |= idBitsToAssign;break;}resetCancelNextUpFlag(child);if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {mLastTouchDownTime = ev.getDownTime();if (preorderedList != null) {for (int j = 0; j < childrenCount; j++) {if (children[childrenIndex] == mChildren[j]) {mLastTouchDownIndex = j;break;}}} else {mLastTouchDownIndex = childIndex;}mLastTouchDownX = ev.getX();mLastTouchDownY = ev.getY();newTouchTarget = addTouchTarget(child, idBitsToAssign);alreadyDispatchedToNewTouchTarget = true;break;}
}// 4. 自己处理
if (mFirstTouchTarget == null) {handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
}
我们把 ViewGroup.dispatchTouchEvent 方法分成 4 个部分,注释 1 处的源码含义是,ViewGroup 会在 DOWN 事件到来时做重置状态的操作,清除上一个事件序列的残留状态,在 resetTouchState 方法会对标志位 FLAG_DISALLOW_INTERCEPT 进行重置(mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT),因此子 View 的 requestDisallowInterceptTouchEvent 方法无法影响 ViewGroup 对 DOWN 事件的拦截处理。
注释 2 处的拦截决策的大致意思是,如果 MotionEvent 是 DOWN 起点事件或子 View 没有该事件的执行权,mFirstTouchTarget == null,则 ViewGroup 会进行拦截检测,也就是由子类重写的 onInterceptTouchEvent 方法所返回的 boolean 值,同时在内层 if 有前面提到的 requestDisallowInterceptTouchEvent 方法的实现,即 disallowIntercept 字段。
注意代码块 16 行的 setAction 方法,在事件分发过程中 MotionEvent 实例会复用避免重复 new 实例带来的开销,而 MotionEvent 实例本身会被子类重写的 onInterceptTouchEvent 方法持有,开发者可以修改指向,这样可能会导致复用错位问题,所以要 setAction 恢复成调用前的状态,是防御型编程。
注释 3 处即 ViewGroup 未拦截事件,将其分发到子 View 的阶段,要区分其中 mChildren 字段和 preorderedList 的区别,前者是 ViewGroup 内部默认的子视图数组,Android 控件的绘制过程是类似盖房自底而上的,我们在屏幕看到的 Activity 显示相当于房间的俯视图,浮在上层的 View 在 mChildren 处于末尾,事件分发又倾向先分给上层 View,所以外层 for 循环是逆序排序;后者 preorderedList 是开发者自定义排序的 View 序列,可以实现高度定制化的业务场景,所以如果 preorderedList != null,要进行 index 对齐以便于后续的事件分发。
dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign) 会调用子 View 的 dispatchTouchEvent 方法,若其返回 true,则会执行到 newTouchTarget = addTouchTarget(child, idBitsToAssign),addTouchEvent 的源码是
private TouchTarget addTouchTarget(View child, int pointerIdBits) {TouchTarget target = TouchTarget.obtain(child, pointerIdBits);target.next = mFirstTouchTarget;mFirstTouchTarget = target;return target;
}
注意到在 addTouchEvent 方法内部将 mFirstTouchTarget 字段赋值成 TouchTarget.obtain(child, pointerIdBits),TouchTarget 也就是完整事件序列的接收者 child,子 View 接收分发后 mFirstTouchTarget != null,回看前面注释 2 的拦截决策,假设子 View 没有设置标志位且 ViewGroup 没有重写 onInterceptTouchEvent (默认返回 false),序列后续每次触发的事件也会进入 if 块从而获取 onInterceptTouchEvent 的返回值,若 onInterceptTouchEvent 返回 true,则后续事件来到 else 块,intercepted == true 且不会再次调用 onInterceptTouchEvent 方法 ,这样也就体现了事件分发结论的 2 和 4 条。
最后看注释 4 的自己处理逻辑,dispatchTransformedTouchEvent 调用 super.dispatchTouchEvent 方法,事件接下来交由 View 处理
public boolean dispatchTouchEvent(MotionEvent event) {boolean result = false;...if (onFilterTouchEventForSecurity(event)) {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;}...return result;
}
View.dispatchTouchEvent 方法先调用 onFilterTouchEventForSecurity 进行安全检查,再判断是否注册 mOnTouchListener 及 View 是否 ENABLED 可见,最后会调用注册 onTouchListener 的 onTouch 方法,判断返回值,如果前面的判断和调用均 false,则会进入 onTouchEvent,所以可以得到优先级 onTouchListener > onTouchEvent。
