Android支持多指触控,一个完整的事件序列:
DOWN ... MOVE ... POINTER_DOWN ... MOVE .... POINTER_UP ... MOVE .... UP 从 DOWN -> POINTER_DOWN 称为焦点事件,在MotionEvent中用PointerId描述
5.1. ViewGroup 的事件分发
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
// TouchTarget 描述一个正在响应事件的 View
// 一个 View 可以响应多个焦点事件
// mFirstTouchTarget 为链表头
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
boolean handled = false;
// 过滤不安全的事件
if (onFilterTouchEventForSecurity(ev)) {
// 获取事件的类型
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 若为 ACTION_DOWN, 则说明是一个全新的事件序列
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev); // 清空所有 TouchTarget
resetTouchState(); // 清空所有触摸状态
}
// 1. 处理事件拦截
final boolean intercepted;
// 1.1 若为初始事件 或 当前容器存在子 View 正在消费事件, 则尝试拦截
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 1.1.1 判断当前 ViewGroup 的 Flag 中是否设置了 不允许拦截事件
// 通过 ViewGroup.requestDisallowInterceptTouchEvent 进行设置, 常用于内部拦截法, 由子 View 控制容器的行为
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 若允许拦截, 则调用 onInterceptTouchEvent, 尝试进行拦截
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
// 不允许拦截, 则直接将标记为置为 false
intercepted = false;
}
} else {
// 1.2 若非初始事件, 并且没有子 View 响应事件中的焦点, 则自己拦截下来由自己处理
// 自己处理不代表一定能消费, 这点要区分开来
intercepted = true;
}
......
// 判断当前容器是否被取消了响应事件
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
......
// 判断是否需要拆分 MotionEvent
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null; // 描述响应的目标
boolean alreadyDispatchedToNewTouchTarget = false;// 描述这个事件是否分发给了新的 TouchTarget
if (!canceled && !intercepted) {
......
// 2. 若为 ACTION_DOWN 或者 ACTION_POINTER_DOWN, 则说明当前事件序列出现了一个新的焦点, 则找寻该焦点的处理者
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 2.1 获取焦点的索引(第一个手指的 down 为 0, 第二个手指的 down 为 1)
final int actionIndex = ev.getActionIndex(); // always 0 for down
// 将焦点的索引序号映射成二进制位, 用于后续保存在 TouchTarget 的 pointerIdBits 中
// 0 -> 1, 1 -> 10, 2 -> 100
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS;
// 清理 TouchTarget 中对历史遗留的 idBitsToAssign 的缓存
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
// 2.2 获取事件在当前容器中的相对位置
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// 获取当前容器前序遍历的 View 序列(即层级从高到低排序)
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
// 2.3 遍历子 View, 找寻可以响应事件的目标
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
// 通过索引找到子孩子实例
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
......
// 2.3.1 判断这个子 View 是否在响应事件的区域
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
......
// 若不在可响应的区域, 则继续遍历下一个子 View
continue;
}
// 2.3.2 判断获取这个子 View, 是否已经响应了序列中的一个焦点
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// 若这个 View 已经响应了一个焦点, 则在它的 TouchTarget.pointerIdBits 添加新焦点的索引
newTouchTarget.pointerIdBits |= idBitsToAssign;
// 则直接结束查找, 直接进行后续的分发操作
break;
}
......
// 2.3.3 调用 dispatchTransformedTouchEvent 尝试将这个焦点分发给这个 View 处理
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
......
// 子 View 成功的消费了这个事件, 则将这个 View 封装成 TouchTarget 链到表头
newTouchTarget = addTouchTarget(child, idBitsToAssign);
// 表示这个事件在找寻新的响应目标时已经消费了
alreadyDispatchedToNewTouchTarget = true;
break;
}
......
}
......
}
// 2.4 若没有找到可以响应的子 View, 则交由最早的 TouchTarget 处理
if (newTouchTarget == null && mFirstTouchTarget != null) {
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
// 在其 pointerIdBits 保存焦点的 ID
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// 3. 执行事件分发
// 3.1 没有任何子 View 可以响应事件序列, 则交由自己处理
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 3.2 若存在子 View 响应事件序列, 则将这个事件分发下去
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// 3.2.1 alreadyDispatchedToNewTouchTarget 为 true 并且 target 为我们上面新找到的响应目标时, 跳过这次分发
// 因为在查找焦点处理者的过程中, 已经分发给这个 View 了
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// 3.2.2 处理还未消耗这个事件的子 View
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;// 判断是否需要给 View 发送 CANCEL 事件, view 设置了 PFLAG_CANCEL_NEXT_UP_EVENT 这个 Flag, 或者这个事件被该容器拦截了, 那么将会给子 View 发送 Cancel 事件
// 3.2.2 调用 dispatchTransformedTouchEvent 将事件分发给子 View
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
// 3.2.3 若给这个 View 分发了 cancel 事件, 则说明它已经无法响应事件序列的焦点了, 因此将它从响应链表中移除
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// 4. 清理失效的 TouchTarget
// 4.1 若事件序列取消 或 整个事件 UP 了, 则移除所有的 TouchTarget
if (canceled || actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
// 4.2 若事件序列的一个焦点 UP 了, 则移除响应这个焦点的 TouchTarget
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
......
return handled;
}
}
总结一下:
- 拦截事件
- 若为 起始事件 ACTION_DOWN/ACTION_POINTER_DOWN 或 存在子View正在消费事件,则调用onInterceptTouchEvent尝试拦截
- 若设置了FLAG_DISALLOW_INTERCEPT 则不进行拦截操作,直接分发给子View
- 若非 起始事件 并且没有子View响应事件,则进行拦截操作
- 若为 起始事件 ACTION_DOWN/ACTION_POINTER_DOWN 或 存在子View正在消费事件,则调用onInterceptTouchEvent尝试拦截
- 寻找起始事件的处理者
- 通过 canViewReceivePointerEvents 和 isTransformedTouchPointInView 找寻区域内的子View
- 若子View已经响应了一个焦点事件序列,则将这个事件序列的id添加到这个View对应的TouchTarget的pointerIdBits中
- 若子View当前未响应事件,则调用dispatchTransformedTouchEvent尝试让子View处理
- 处理成功,则将这个View构建成TouchTarget保存起来
- 若无处理者,则交由mFirstTouchTarget处理
- 通过 canViewReceivePointerEvents 和 isTransformedTouchPointInView 找寻区域内的子View
- 执行事件分发
- 若无响应目标,则ViewGroup自行处理
- 若存在响应目标,则遍历TouchTarget链表,将事件分发给所有的TouchTarget
当产生一个新的事件焦点时,如何找到响应它的子View呢?
5.1.1. 寻找事件的响应目标
在ViewGoup的dispatchTouchEvent中,我们知道判断一个View是否可以响应事件主要有两个方法:canViewReceivePointerEvents 和 isTransformedTouchPointInView
canViewReceivePointerEvents : 根据View 的状态判断是否可以接受事件
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
private static boolean canViewReceivePointerEvents(@NonNull View child) {
// Condition1: View 为 Visible
// Condition2: 当前 View 设置了动画
return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null;
}
}
isTransformedTouchPointInView : 判断事件的相对坐标是否落在子View的宽高之内
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
// 1. 获取一个坐标数组, 数据为事件在当前 ViewGroup 中的相对坐标 x, y 值
final float[] point = getTempPoint();
point[0] = x;
point[1] = y;
// 2. 调用了 transformPointToViewLocal, 将坐标转为 Child 的相对坐标
transformPointToViewLocal(point, child);
// 3. 调用了 View.pointInView 判断坐标是否落在了 View 中
final boolean isInView = child.pointInView(point[0], point[1]);
// 若在子 View 中, 则尝试输出到 outLocalPoint 中, dispatchTouchEvent 中传入的为 null
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
public void transformPointToViewLocal(float[] point, View child) {
// 2.1 将 point 转为 View 的相对坐标
point[0] += mScrollX - child.mLeft;
point[1] += mScrollY - child.mTop;
// 2.2 若 View 设置了 Matrix 变化, 则通过 Matrix 来映射这个坐标
if (!child.hasIdentityMatrix()) {
child.getInverseMatrix().mapPoints(point);
}
}
}
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
final boolean pointInView(float localX, float localY) {
return pointInView(localX, localY, 0);
}
public boolean pointInView(float localX, float localY, float slop) {
// 很简单, 即判断是否落在 View 的区域内, 小于 View 的宽度和高度
// slop 为当前 Android 系统能够识别出来的手指区域的大小
return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
localY < ((mBottom - mTop) + slop);
}
}
在transformPointToViewLocal方法中,会调用 child.getInverseMatrix().mapPoints(point) 来映射一次坐标,为什么呢?
- 因为我们在执行属性动画的时候,有的时候会进行View的transition ,scale 等操作,这些操作并不会改变View的原始坐标,但会改变它内部的RenderNode的Matrix,进行坐标映射,是为了让View 在变化后的区域依旧可以响应事件流,这就是为什么属性动画后View依旧可以响应点击事件的原因。
在子View判断完是否可以响应事件后,看一下ViewGroup事件流是如何分发的?
5.2. 将事件分发给子View
在ViewGroup.dispatchTouchEvent中,我们知道最后它会遍历TouchTarget链表,逐个调用dispatchTransformedTouchEvent方法,将一个事件分发给该事件所在序列的所有焦点处理者,是怎么做到的?
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// 获取事件的动作
final int oldAction = event.getAction();
// 1. 处理 Cancel 操作
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
// 1.1 将事件的 Action 强行改为 ACTION_CANCEL
event.setAction(MotionEvent.ACTION_CANCEL);
// 1.2 ACTION_CANCEL 的分发操作
if (child == null) {
// 1.2.1 自己处理
handled = super.dispatchTouchEvent(event);
} else {
// 1.2.2 分发给子 View
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// 2. 判断这个 child 是否可以响应该事件
// 2.1 获取这个事件所在序列的所有焦点
final int oldPointerIdBits = event.getPointerIdBits();
// 2.2 desiredPointerIdBits 描述这个 child 能够处理的焦点
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// 2.3 若他们之间没有交集, 则说明出现了异常情况, 这个 child 所响应的并非是这个事件序列中的焦点
if (newPointerIdBits == 0) {
return false;
}
// 3. 将事件分发给子 View
final MotionEvent transformedEvent;
// 3.1 若这个子 View 能够处理这个事件序列的所有焦点, 则直接进行分发操作
if (newPointerIdBits == oldPointerIdBits) {
// 若不存子 View, 或者子 View 设置 Matrix 变幻, 则走下面的分支
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
// 3.1.1 自己处理
handled = super.dispatchTouchEvent(event);
} else {
// 3.1.2 分发给子 View
......
handled = child.dispatchTouchEvent(event);
......
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
// 3.2 若这个子 View 只能够处理事件序列中部分的焦点, 则调用 MotionEvent.split 进行焦点分割
transformedEvent = event.split(newPointerIdBits);
}
if (child == null) {
// 3.2.1 不存在子 View 则自己处理
handled = super.dispatchTouchEvent(transformedEvent);
} else {
// 3.2.2 将进行焦点分割后的事件, 分发给子 View
......
handled = child.dispatchTouchEvent(transformedEvent);
}
......
return handled;
}
}
来看一下TouchTarget的实现
// 遍历mFirstTouchTarget链表,检测每个节点的child是不是方法参数中的child
// 参数中的child是要分派触摸事件的子View
private TouchTarget getTouchTarget(View child) {
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
if (target.child == child) {
return target;
}
}
return null;
}
private static final class TouchTarget {
public View child;
// 可以理解为当前已按下的手指数
public int pointerIdBits;
public TouchTarget next;
......
}
在dispatchTransformedTouchEvent返回了true以后,会调用addTouchTarget方法
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
第一次分派事件给子View之后,会把子View和pointerIdBits记录到mFirstTouchTarget中。 当新手指按下时,会先从链表mFirstTouchTarget中查找有没有(该子View)的节点
- 有的话更新这个节点的pointerIdBits,也就是刷新当前手指按下的数量
- 没有的话,说明这个子View还没有手指按下过
在dispatchTransformedTouchEvent中检测到是多指按下时,会把事件拆分,最后分派拆分(调用split方法把ACTION_POINTER_DOWN改为ACTION_DOWN)后的事件给子View。分派事件后,又把这个子View和手指数量记录到链表中。
可以看到,只要View能够处理当前MotionEvent所在事件序列中的一个焦点,便会分发给它,分发之前会调用MotionEvent.split将事件分割成为View能够处理的焦点事件,并将其分发下去。
5.2.0.1. MotionEvent事件
当两个及以上的手指触摸屏幕时,会产生多个触摸事件传递给ViewGroup,改MotionEvent中除了 存储事件类型和坐标位置等信息外,还会保存一组触摸点信息。当触摸点落在ViewGroup的不同child上时,需要对MotionEvent进行事件拆分,再将拆分后的事件派发给对应的child。
MotionEvent的action为int型,高8位存储触摸点索引集合,低8位才是存储动作类型。
可以看出,索引值是会相对变化的,而ID值保持不变。
MotionEvent.split 方法主要根据出入的idBits调整事件的Action
- 触摸点3按下时,ViewGroup会收到 ACTION_POINTER_DOWN 事件,该触摸点是ChildB感兴趣的,对于ChildB来说是一个全新的事件序列,因为派发给childB时,需要将类型调整ACTION_DOWN。而对于childA 来说,并不是它感兴趣的,因此在派发给childA时,调整为ACTION_MOVE 。
- 当触摸点2抬起时,ViewGroup会收到 ACTION_POINTER_UP 事件,改事件是childA感兴趣的,但是childA上仍有触摸点1,因此派发给childA 的事件类型依旧是ACTION_DOWN_UP。而派发给childB时,调整为ACTION_MOVE。
事件拆分是为了在多点触摸情况下更准确的将事件传递给子View,在派发过程中,ViewGroup不会原样把MotionEvent派发给子View,而是根据落于子View上的触摸点,调整MotionEvent中的事件类型和触摸点信息后生成新的MotionEvent副本,再用这个MotionEvent副本派发给对应的子View。
E/TAG: --------------------dispatch begin----------------------
E/TAG: ViewGroup PointerId is: 0, eventRawX 531.7157, eventRawY 747.5201
E/TAG: Child PointerId is: 1, eventRawX 467.7539, eventRawY 1387.9688
E/TAG: Child PointerId is: 0, eventRawX 531.7157, eventRawY 747.5201
E/TAG: --------------------dispatch begin----------------------
E/TAG: ViewGroup PointerId is: 0, eventRawX 534.09283, eventRawY 744.18854
E/TAG: Child PointerId is: 1, eventRawX 467.7539, eventRawY 1387.9688
E/TAG: Child PointerId is: 0, eventRawX 534.09283, eventRawY 744.18854
从日志可以看出,ViewGroup中的一个事件的确会分发给这个事件序列所有的焦点处理者,可以看到MotionEvent.split方法,不仅仅分割了焦点,并且还巧妙的将触摸事件转换到了焦点处理View对应的区域,Google这里处理的原因可能是为了保证触摸过程中事件的连续性,以实现更好的交互效果。
5.3. View的事件分发
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
public boolean dispatchTouchEvent(MotionEvent event) {
......
boolean result = false;
......
// 获取 Mask 后的 action
final int actionMasked = event.getActionMasked();
// 若为 DOWN, 则停止 Scroll 操作
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
// 处理事件
if (onFilterTouchEventForSecurity(event)) {
// 1. 若是拖拽滚动条, 则优先将事件交给 handleScrollBarDragging 方法消耗
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
// 2. 若 ListenerInfo 中设置了 OnTouchListener, 则尝试将其交给 mOnTouchListener 消耗
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 3. 若上面的操作没有将 result 位置 true, 则交给 onTouchEvent 消耗
if (!result && onTouchEvent(event)) {
result = true;
}
}
......
return result;
}
}
View的dispatchTouchEvent主要操作:
- 优先将事件交给 ScrollBar 处理
- 成功消费则将 result = true
- 次优先将事件交给OnTouchListener处理
- 成功消费则将 result = true
- 最后将事件交给onTouchEvent处理
5.3.1. View 的 OnTouchEvent 处理事件
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
// 判断当前 View 是否是可点击的
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
......
// 若设置了代理, 则交由代理处理
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// 执行事件处理
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
// 1. 处理按压事件
case MotionEvent.ACTION_DOWN:
......
// 处理 View 在可滑动容器中的按压事件
if (isInScrollingContainer) {
......
} else {
// 处理在非滑动容器中的按压事件
// 1.1 设置为 Pressed 状态
setPressed(true, x, y);
// 1.2 尝试添加一个长按事件
checkForLongClick(0, x, y);
}
break;
// 2. 处理移动事件
case MotionEvent.ACTION_MOVE:
......
// 处理若移动到了 View 之外的情况
if (!pointInView(x, y, mTouchSlop)) {
// 移除 TapCallback
removeTapCallback();
// 移除长按的 Callback
removeLongPressCallback();
// 清除按压状态
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
......
}
break;
// 3. 处理 UP 事件
case MotionEvent.ACTION_UP:
// 3.1 若置为不可点击了, 则移除相关回调, 重置相关 Flag
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
......
break;
}
// 3.2 判断 UP 时, View 是否处于按压状态
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
......
// 3.2.1 若没有触发长按事件, 则处理点击事件
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();// 移除长按回调
// 处理点击事件
if (!focusTaken) {
// 创建一个事件处理器
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
// 发送到 MessageQueue 中执行
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
// 清除按压状态
......
}
......
break;
// 4. 处理取消事件
case MotionEvent.ACTION_CANCEL:
// 移除回调, 重置标记位
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
return true;
}
return false;
}
}
onTouchEvent 事件处理:
- ACTION_DOWN
- 将View设置为按压状态
- 添加了一个长按监听器
- ACTION_MOVE
- 若坐标在View的范围之外,则移除相关回调,清除按压状态
- ACTION_UP
- 若长按事件没有相应,则处理View的点击事件
- 移除按压状态
- ACTION_CANCEL
- 移除相关回调,清除按压状态
5.4. View事件处理总结
5.4.1. ViewGroup 事件分发总结
- 拦截事件
- 若为 起始事件 ACTION_DOWN/ACTION_POINTER_DOWN 或 存在子View正在消费事件,则调用onInterceptTouchEvent尝试拦截
- 若设置了FLAG_DISALLOW_INTERCEPT 则不进行拦截操作,直接分发给子View
- 若非 起始事件 并且没有子View响应事件,则进行拦截操作
- 若为 起始事件 ACTION_DOWN/ACTION_POINTER_DOWN 或 存在子View正在消费事件,则调用onInterceptTouchEvent尝试拦截
- 寻找起始事件的处理者
- 通过 canViewReceivePointerEvents 和 isTransformedTouchPointInView 找寻区域内的子View
- 若子View已经响应了一个焦点事件序列,则将这个事件序列的id添加到这个View对应的TouchTarget的pointerIdBits中
- 若子View当前未响应事件,则调用dispatchTransformedTouchEvent尝试让子View处理
- 处理成功,则将这个View构建成TouchTarget保存起来
- 若无处理者,则交由mFirstTouchTarget处理
- 通过 canViewReceivePointerEvents 和 isTransformedTouchPointInView 找寻区域内的子View
- 执行事件分发
- 若无响应目标,则ViewGroup自行处理
- 若存在响应目标,则遍历TouchTarget链表,将事件分发给所有的TouchTarget
5.4.2. View 事件分发总结
- 优先将事件交给 ScrollBar 处理
- 成功消费则将 result = true
- 次优先将事件交给OnTouchListener处理
- 成功消费则将 result = true
- 最后将事件交给onTouchEvent处
属性动画不会改变View真正位置,它改变的是View硬件渲染结点 mRenderNode的矩阵数据
- RenderNode是View硬件渲染机制中用来捕获View渲染动作的
- ViewGroup的dispatchTouchTarget在寻找事件位置所在区域的子View时,会计算View矩阵映射后的坐标,因此事件分发天然就支持属性动画变换的
为什么ViewGroup会将一个事件分发给所有的TouchTarget,只分发给响应位置的TouchTarget不行?
- 当一个手指按在屏幕上进行滑动时,另一个手指也按上去,此时他们都是连续的触摸事件
- 但事件分发一次只能分发一个事件,为了保证所有View响应的连续性,分发时会调用MotionEvent.split方法,巧妙的将触摸事件转换到了焦点处理View对应的区域,以实现更好的交互效果。