通过本文了解Android中触摸事件在视图层次中的分发流程以及多点触控时系统的处理方式。通过分析源码的方式初步建立UI系统中处理事件的基本思路,为以后自己设计UI系统时做参考。同时也为开发Android自定义控件时处理复杂事件提供理论基础。
前置条件: 有一定的Android开发经验
源码版本: android 10
在开始之前先思考几个问题:
- 事件在视图层级中分派流程是怎样的
- 什么时机确认事件被某一View处理
- 如何记录事件应该分派到哪一个View
- 多点触控时如何处理事件分派
- 什么情况下View会收到ACTION_CANCLE事件
Touch事件分发流程
如图所示
Android中Activity和Dialog都有与之关联的Window对象(通常是PhoneWindow),而Window对象的根View是DecorView,
DecorView继承至FrameLayout,同样是一个ViewGroup(也是View),不过它的dispatchTouchEvent方法被重写。
重写后的代码如下,不再直接将事件分发到子View而是先调用Window.Callback的dispatchTouchEvent方法,这里Window.Callback实际被Activity实现,
因此在Activity中重写dispatchTouchEvent方法将能监听或者拦截事件,这比自己写的代码中所有控件的事件处理都早。
DecorView - dispatchTouchEvent 源码分析
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();//Window.Callback被Activity实现
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
在Activity的dispatchTouchEvent方法中会调用Window(实际是PhoneWindow)的superDispatchTouchEvent,
在此方法中又会调用DecorView的super.dispatchTouchEvent方法(也就是ViewGroup的对应方法),事件就此进入view层次结构进行传递。
Activity - dispatchTouchEvent 源码分析
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {//getWindow()返回的实际为PhoneWindow,PhoneWindow的superDispatchTouchEvent方法会调用DecorView的super.dispatchTouchEvent方法
return true;
}
return onTouchEvent(ev);
}
之所以要经过Activity绕一圈,可以看做是为了让Activity参与到事件分派过程中来
View - dispatchTouchEvent处理流程
ViewGroup当有子View时会调用View的dispatchTouchEvent,当没有子View时会调用自身的未复写的dispatchTouchEvent(因为ViewGroup本身也是View,也就是ViewGroup调用super.dispatchTouchEvent)
如果View设置了OnTouchListener将先调用OnTouchListener的onTouch,当OnTouchListener返回值为false时,
才调用onTouchEvent做事件处理。
public boolean dispatchTouchEvent(MotionEvent event) {
//...省略不相关代码
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
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)) {//调用自身的onTouchEvent事件
result = true;
}
}
//...省略不相关代码
return result;
}
View - onTouchEvent 处理流程
View 的 onTouchEvent 主要职责是实现长按事件和点击事件以及控件(按下、抬起)等状态更新
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 ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
//当事件为ACTION_UP时并且当前View状态为pressed则更新View状态
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
//...省略无关代码
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
//不可点击时移除相应事件
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
//由于prepressed设置为true,当前还处于识别是否为滑动的时间窗口内,还未将View设置为pressed状态
//先设置按钮为按下状态,给一定的延时后再设置为false,让用户能够看到点击效果
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
//长按事件未调用则调用点击事件
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
//延时一定时间,再设置取消pressed,确保点击状态变化能被看到
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) {
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
break;
}
if (performButtonActionOnTouchDown(event)) {
//注意performButtonActionOnTouchDown这是一个非常有意思的方法调用,后面将展开分析它的实现
//通过该方法的分析将有助于我们略微了解PFLAG_CANCEL_NEXT_UP_EVENT标志的用法,该标志在ViewGroup的
//dispatchTouchEvent方法中将多次作为判读依据,从字面上看PFLAG_CANCEL_NEXT_UP_EVENT意思好像是
//取消下一次UP事件,取消下一次UP事件,当前View将收到一个ACTION_CANCLE事件
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
//如果在可滑动容器内部,就给一个短暂的时间用以识别是否为滑动事件
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);//设置状态为pressed
//开始检测长按事件
//检测长按时事件,通过延时实现,当前延时到达时判断当前状态是否为pressed
//当判断还是pressed则调用performLongClick
//在performLongClick将会调用LongClickListener 如果LongClickListener未处理事则调用showContextMenu
//若performLongClick返回true,则设置 mHasPerformedLongPress = true
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
break;
case MotionEvent.ACTION_CANCEL:
//取消相应标志位及长按检测
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
//关与event Classification 的判断在Android10.0之前没有此判断,可能是Android10.0开始对事件本身也进行分类(普通按屏幕、劲按了屏幕等)
final int motionClassification = event.getClassification();
final boolean ambiguousGesture =
motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
int touchSlop = mTouchSlop;
if (ambiguousGesture && hasPendingLongPressCallback()) {
//事件意图不明确时乘上相应的因子,增加容错性。测试下来这个分支通常不会执行
//因为event.getClassification()返回通常是CLASSIFICATION_NONE
final float ambiguousMultiplier =
ViewConfiguration.getAmbiguousGestureMultiplier();
if (!pointInView(x, y, touchSlop)) {
// The default action here is to cancel long press. But instead, we
// just extend the timeout here, in case the classification
// stays ambiguous.
removeLongPressCallback();
long delay = (long) (ViewConfiguration.getLongPressTimeout()
* ambiguousMultiplier);
// Subtract the time already spent
delay -= event.getEventTime() - event.getDownTime();
checkForLongClick(
delay,
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
touchSlop *= ambiguousMultiplier;
}
// Be lenient about moving outside of buttons
//如果当前触点已经移动到控件外面,则取消相关事件检测(比如我们按下Button后,移动手指到Button外面,将不会响应点击事件)
if (!pointInView(x, y, touchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
//取消了click事件,在抬起手指(ACTION_UP分支中)要判断
//(mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed,为真才时才可能调用点击事件监听回调函数
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
//如果为CLASSIFICATION_DEEP_PRESS则马上调用长按事件,类似于IOS的3D Touch
final boolean deepPress =
motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
if (deepPress && hasPendingLongPressCallback()) {
// process the long click action immediately
removeLongPressCallback();
checkForLongClick(
0 /* send immediately */,
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
}
break;
}
return true;
}
return false;
}
View - performButtonActionOnTouchDown 源码分析
protected boolean performButtonActionOnTouchDown(MotionEvent event) {
//如果是鼠标并且是鼠标右键事件将显示contextMenu,并设置PFLAG_CANCEL_NEXT_UP_EVENT,表示该View不在接收ACTION_UP事件,会收到ACTION_CANCLE事件
if (event.isFromSource(InputDevice.SOURCE_MOUSE) &&
(event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
showContextMenu(event.getX(), event.getY());
mPrivateFlags |= PFLAG_CANCEL_NEXT_UP_EVENT;
return true;
}
return false;
}
ViewGroup 事件分派流程分析
ViewGroup 重写了View的dispatchTouchEvent方法,该方法将事件分发给相应的子控件进行处理。如果没有子控件处理,则交给ViewGrop本身的onTouchEvent方法处理。注意判断子控件是否处理事件是根据手指按下时(发生ACTION_DOWN事件)调用子View的dispatchTouchEvent的返回值是否为true来确定的。
因为子View的dispatchTouchEvent又会调用自己的onTouchEvent方法,所以onTouchEvent中返回true时,能够接受到后续事件,否则收不到后续事件。
ViewGroup - dispatchTouchEvent 事件分发流程
-
ViewGroup的dispatchTouchEvent方法在处理ACTION_DOWN事件时,发现某个子View的dispatchTouchEvent返回值为true则将该子view及touch事件的PointerId(即是哪个手指)通过方法addTouchTarget加入TouchTarget链表。
-
发生后续事件如ACTION_MOVE时,将TouchTarget链表中的元素依次取出判断是否有相同的PointerId,如果有的话就调用TouchTarget关联的view的dispatchTouchEvent,
如果PointerId完全不等则返回。这里涉及多点触控,Android2.3之前实际不支持多点触控,看源码在Android4.0开始就有相关处理多点触控的逻辑了。
ViewGroup - onInterceptTouchEvent 分析
ViewGroup中的onInterceptTouchEvent方法用于判断当前ViewGroup是否要拦截事件,也就是ViewGroup是否要自己处理事件,不分派到子View
-
onInterceptTouchEvent 在dispatchTouchEvent中被调用时,返回值为true表示拦截事件,否则不拦截
-
一旦onInterceptTouchEvent返回true,后续事件到来时如ACTION_MOVE,将不再调用onInterceptTouchEvent方法
-
如果ViewGroup不拦截事件也就是onInterceptTouchEvent始终返回false,则后续事件都会调用onInterceptTouchEvent做判断
ViewGroup - dispatchTouchEvent源码分析
dispatchTouchEvent方法中涉及多点触控情况的处理,引入了TouchTarget链表,让我们先来分析一下TouchTarget的源码
TouchTarget 数据结构
TouchTarget对象采用链表的数据结构,用next指向下一个TouchTarget对象,使用了对象池缓存TouchTarget对象,避免频繁创建对象,TouchTarget对象持有一个View对象和pointerId的信息(pointerIdBits 用一个整形值的位来记录)
private static final class TouchTarget {
private static final int MAX_RECYCLED = 32;
private static final Object sRecycleLock = new Object[0];//神奇!,采用长度为0的数组
private static TouchTarget sRecycleBin;//回收的对象池链表头
private static int sRecycledCount;//回收链表中对象的个数
public static final int ALL_POINTER_IDS = -1; // all ones
// The touched child view.
@UnsupportedAppUsage
public View child;
// The combined bit mask of pointer ids for all pointers captured by the target.
public int pointerIdBits;
// The next target in the target list.
public TouchTarget next;
@UnsupportedAppUsage
private TouchTarget() {
}
//如果回收链表中有对象可用就取出第一个元素返回,否则新创建一个TouchTarget对象
public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
if (child == null) {
throw new IllegalArgumentException("child must be non-null");
}
final TouchTarget target;
synchronized (sRecycleLock) {
if (sRecycleBin == null) {
target = new TouchTarget();
} else {
//摘下链表头,并更新链表头,计数减去1
target = sRecycleBin;
sRecycleBin = target.next;
sRecycledCount--;
target.next = null;
}
}
target.child = child;
target.pointerIdBits = pointerIdBits;
return target;
}
//如果回收队列中对象个数小于MAX_RECYCLED,则将对象放回回收链表的头部,更新回收链表的头指针为当前对象
public void recycle() {
if (child == null) {
throw new IllegalStateException("already recycled once");
}
synchronized (sRecycleLock) {
if (sRecycledCount < MAX_RECYCLED) {
next = sRecycleBin;
sRecycleBin = this;
sRecycledCount += 1;
} else {
next = null;
}
child = null;
}
}
}
正式开始
手指按下时,在ViewGroup的dispatchTouchEvent中,将处理事件的子View和事件的PointerId构造为TouchTarget对象保存到TouchTarget链表中,
后续事件根据事件的PointerId查找TouchTarget链表,找到处理事件的View进行事件分派。
下面对多个手指按在同一View上和不同View上两种情况进行讨论:
情形一:两个手指先后按在同一控件上
第一个手指按下,在TouchTarget链表中添加一个节点。
第二个手指按下时根据按下的位置找到目标View,并到TouchTarget链表中根据目标View查找到TouchTarget对象,将新的PointerId添加到TouchTarget对象的pointerIdBits字段中(进行位与)。
情形二:两个手指先后按在不同控件上
第一个手指按下,在TouchTarget链表中添加一个节点。
第二个手指按下时根据按下的位置找到相应的View,并到TouchTarget链表中根据View查找,此时必定查找不到TouchTarget,因为手指按在不同控件上。根据PointerId和View创建新的TouchTarget对象并加入链表。而此时的事件必定为ACTION_POINTER_DOWN。在事件最终分派到View上时该事件将被转换为ACTION_DOWN
对于上面讨论的第二中情形,系统也提供了FLAG_SPLIT_MOTION_EVENTS标志位,对应的开关函数为View.setMotionEventSplittingEnabled来控制是否允许将第二个手指的事件分派到不同控件上。如果该标志被清除,那第二个手指的事件将分派到第一个手指所按的View上。
详细见代码注释
public boolean dispatchTouchEvent(MotionEvent ev) {
//...省略无关代码
boolean handled = false;
//...省略无关代码
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev); //ACTION_DOWN开启新的事件处理,清空上一次的相关记录值,主要是TouchTarget链表
resetTouchState();//重置Touch状态,如清除mGroupFlags中禁止事件拦截标志位,mPrivateFlags中取消下一次UP事件标志位
}
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {//如果是ACTION_DOWN事件或事件已经有子控件处理时进入该if块
//FLAG_DISALLOW_INTERCEPT 标志为由requestDisallowInterceptTouchEvent方法设置,通常是由子view调用,禁止父控件拦截事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);//ViewGroup是否拦截事件
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.
//事件不是ACTION_DOWN并且没有子view处理事件,则ViewGroup拦截事件
intercepted = true;
}
//...省略无关代码
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
//split 用于判断多个手指落在不同的View上,不同的View上分别接受事件处理,
//例如,手指一先按在view1上,然后手指二按下view2:
//如果split 为true ,则在view2上将收到ACTON_DOWN事件而不是ACTION_POINTER_DOWN,是经过dispatchTransformedTouchEvent转换的结果
//如果split 为false 则,手指二的按下还是被认为是发生在view1上,view1收到事件ACTION_POINTER_DOWN
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
//...忽略无关代码
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//该段代码用于判读处理事件的子View,并将其添加到TargetTouch链表中
final int actionIndex = ev.getActionIndex(); // always 0 for down
//当前split为true的时候将PointerId存储到idBitsToAssign的相应为中
//否则,将idBitsToAssign的所有位设置为1
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
//将包含该PointerId的TouchTarget的对应位清除,清除该位后当TouchTarget的pointerIdBits
//为0时从TouchTarget链中将其移除
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//遍历子View,根据当前坐标和子View找到处理事件的View
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//getTouchTarget到TouchTarget链表中查找该view
//若找到则将新的pointerId记录到TouchTarget的pointerIdBits中
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
//在TouchTarget链表中未找到,则说明是需要产生新的TouchTarget
//通过dispatchTransformedTouchEvent分派事件到子View
//然后将子View和PointerId构造为TouchTarget对象并添加到TouchTarget链表中
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
//当前事件没有子View处理,则将pointerId设置到最早接收事件的View上(也就是认为该点落在此View上),此处英文注释好像跟代码逻辑不匹配
//每一次添加TouchTarget对象都是添加到链表头部
//此处的代码却是找到链表的最后一个元素将PointerId添加到该TouchTarget上
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
//没有子View处理则,则将事件交给自己处理,后面分析dispatchTransformedTouchEvent将会看到
//当第3个参数child为null时,并且第4个参数为ALL_POINTER_IDS时将调用super.dispatchTouchEvent
//也即是交给ViewGroup自己处理
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
// 下面代码遍历TouchTarget链表,对每一个TouchTarget对象调用dispatchTransformedTouchEvent方法
// 在dispatchTransformedTouchEvent中将会判断PointerId是否包含此次事件的PointerId,若包含
// 将事件分发到TouchTarget关联的View上
//若此时ViewGroup拦截了事件即intercepted为true或当前事件的子view取消下一次ACTON_UP事件标志位被设置
//则向所有接受事件的子View 分发 ACTION_CANCEL事件并将对应的TouchTarget从链表中移除
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {//分派事件到子View
handled = true;
}
if (cancelChild) {//如果是cancel则将对应的TouchTarget从链表中移除
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//进入此处表明ViewGroup结束事件接收,重置Touch相关的状态,如清除TouchTarget链表等
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
//如果允许多个子view同时接收事件并且为ACTION_POINTER_UP事件则将TouchTarget的存在的PointerId的位清除掉
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
//...省略无关代码
return handled;
}
ViewGroup - dispatchTransformedTouchEvent源码分析
根据dispatchTouchEvent代码分析中发现真正将事件分发到子View是调用dispatchTransformedTouchEvent完成的。从方法名字上可以看出该方法主要变换事件和分发事件。
根据参数值的不同进行不同的逻辑处理:
- 参数cancel为true或者当前event为ACTION_CANCEL,child不为null时将调用child的dispatchTouchEvent分派ACTION_CANCEL事件
- 参数cancle为true或者当前event为ACTION_CANCEL,child为null是将调用super.dispatchTouchEvent分派事件到当前ViewGroup处理
- 不为前面两种情况则判断当前事件的PointerId集合与TouchTarget的PointerId集合是否相交或包含
- 不包含则返回,即两个集合位与为0
- 如果pointerId集合包含于TouchTarget的PointerId集合,则将事件分发到当前View或子View
- 如果部分包含则调用MotionEvent的split方法转换事件(在多点触控下会发生),如split新产生的TouchEvent有新的PointerId且只包含一个点,如果事件是ACTION_POINTER_DOWN,则将事件ACTION_POINTER_DOWN转换为ACTION_DOWN,否则转换为ACTION_UP
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 {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {//当前事件没有PointerId对应返回
return false;
}
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {//当前事件的PointerId集合包含于TouchTarget的PointerId集合,分发事件
if (child == null || child.hasIdentityMatrix()) {//当child为null或者child的变换矩阵为单位矩阵
if (child == null) {
//child为null将事件分发给自身处理
handled = super.dispatchTouchEvent(event);
} else {
//将事件分发给子View,此处做了坐标转换,让坐标相对子view
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);//拷贝事件
} else {
transformedEvent = event.split(newPointerIdBits);//转换为新的事件
}
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
//子View的变换矩阵不是单位矩阵时将事件的点变换为为当前子View实际的坐标
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
总结
对与本文开头的问题做出回答
1、事件在视图层级中分派流程是怎样的
答:由ViewGroup的dispatchTouchEvent根据记录的PointerId与View的对应关系分发给子View或者当前ViewGroup自己处理
2、什么时机确认事件被某一View处理
答:在ACTION_DOWN时,View的dispatchTouchEvent方法返回值为true的情况下认为事件被该View处理
3、如何记录事件应该分派到哪一个View
答:在ACTION_DOWN时将接收事件的View和PointerId封装为TouchTarget保存到链表中,下一次事件发生时则遍历链表根据PointerId找到View,进行事件分派
4、多点触控时如何处理事件分派
答:多个手指落在不同的View上时,会封装多个对应的TouchTarget对象放入链表,并将事件转换后分派给子View,例如将ACTION_POINTER_DOWN转换为ACTION_DOWN。
5、什么情况下View会收到ACTION_CANCLE事件
答:两种情况。
情形一:子View在接收事件的过程中(如ACTION_MOVE),事件被父控件拦截。
情形二:当前ViewGroup或者子View设置了取消下一次抬起事件(即设置了PFLAG_CANCEL_NEXT_UP_EVENT)