文章目录
事件分发
从何说起?
学习一个知识一般都是有顺序的,有些时候正确的学习顺序能起到事半功倍的效果。那这里我打算从View的事件分发开始说起。
为什么是从View的事件分发开始说起呢?因为它是在一般意义上的事件分发“终点”,也就是最后环节。我们这里先不考虑一层一层传递下来的复杂情况,只从View 本身如何分发事件说起,这是一个较为简单,不需要考虑各种复杂的情况,这也是最基础的部分。当理解了这部分基础的内容,我再对ViewGroup进行拆解,会更容易理解一些。
以一个什么框架去学习?
学习事件分发,我希望以下面一个框架去学习:
- 事件从何而来?
- 事件有哪些类型?
- 事件的结果有什么影响?
- 我能利用这个过程做些什么?或者实现些什么?
可以总结为:我是谁?我从哪里来?我要到哪里去?
重点
关于事件
事件是由一系列事件组成的事件流。
从ACTION_DOWN为起点,中间经历N个ACTION_MOVE,以ACTION_UP或ACTION_CANCEL为结束。
一般我们讨论事件的传递,不止讨论单一事件,而是考虑,而是整个事件流。
事件分发相关的方法
在事件分发过程中,总共有3个方法需要我们注意:
- dispatchTouchEvent
- onInterceptTouchEvent (只有ViewGroup才有此方法)
- onTouchEvent
这里先总览一下这三个方法都要做些什么:
dispatchTouchEvent
负责决定由谁来处理此次事件
onTouchEvent
决定了事件的如何处理此次事件。
onInterceptTouchEvent
决定了在ViewGroup是否要继续往子节点(可能为View,也可能为ViewGroup)继续分发事件。
所有的事件分发,都是dispatchTouchEvent为相对起点,在该方法中,决定是否拦截,或者是否触发onTouchEvent or OnTouchListener。
接下来,我先对View 的事件分发进行学习。
View的事件分发
先明确一下框架中提到的问题:
- 事件从何而来? ViewGroup,传递到当前View的dispatchTouchEvent。
- 事件有哪些类型?DOWN、MOVE、UP、CANCEL
- 事件的结果有什么影响?
事件的结果其实就是当前View的dispatchTouchEvent方法的返回值,它会决定父布局是否继续找寻下一个可处理事件的View。它的返回值含义是:我(当前View)是否要对当前事件进行处理。 - 我能利用这个过程中做些什么?
- 首先看Android原生做了些什么?
1. 处理点击事件:一次按下抬起,记为一次点击事件。(我们设置的OnClickListener是在这种情况下被触发)
2. 处理长按事件:当一次按下时长超过一个阈值,则触发长按事件。(LongClickListener 是在这种情况下被触发) - Button用来做了什么?
1. 按下的时候触发波纹动画效果、按下后背景颜色切换等
2. 抬起时触发点击事件,触发对应行为。 - 我可以做些什么?
1. 在dispatchTouchEvent时,我可以直接返回false,不进行点击事件处理
2. 在OnTouchEvent时,我可以根据当前触摸位置改变我自己View的位置
3. 在OnTouchEvent时,我可以根据按下时间长短,自己制造个 OnFiveSecondClickListener
4. 我可以设置一个OnTouchListener,返回false,里边做一下自己的操作,但是不接管原本的事件处理,比如当View被触摸时,我降低另一个View的透明度,但又不想影响原本的View点击行为,长按行为。
5. 等等等等,你可以任意想象,只要是与触摸有关的,你想实现的,都可以通过这个两个方法搞一搞。
View 的事件分发源码分析
// 事件分发的入口,从ViewGroup调用到此处。
public boolean dispatchTouchEvent(MotionEvent event) {
// 此处剔除掉次要代码
// 定于用于存储此次事件的结果
boolean result = false;
// 获取当前事件的类型
final int actionMasked = event.getActionMasked();
// 如果是按下事件
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 因为按下事件是事件传递的起点,所以此处,出现「按下」事件,则需要停止NestedScroll行为
stopNestedScroll();
}
// 判断是否可以处理此次事件,主要是判断当前View所处Window的状态,是否在展示中
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED &&
// 此方法处理,由鼠标 输入时的拖拽滑块行为,
// 如果返回值为true,则返回true,由鼠标拖拽处理后面行为
handleScrollBarDragging(event)) {
result = true;
}
// 获取是否设置了OnTouchListener
ListenerInfo li = mListenerInfo;
// 满足三个条件则 确认消费此次事件:
// 1. 设置了 OnTouchListener
// 2. 当前View 状态为Enable
// 3. OnTouchListener.onTouchEvent 返回值为 true
// 由以上可知,只要设置了OnTouchListener
// OnTouchListener的onTouchEvent一定会执行,无论返回true、false
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 当 onTouchListener 为null 或 OnTouchListener.onTouchEvent返回值为false
// 执行自身的 onTouchEvent()
if (!result && onTouchEvent(event)) {
result = true;
}
}
// 如果 此次事件为抬起、或者取消,或 (按下事件,但是自身没有处理此次事件)
// 则结束NestedScroll
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
// 返回自身处理结果
// 这个返回值会影响什么?
// 如果返回值为 true,则认为当前View消费此次事件,父View不会继续往下分发。
// 返回值为 true,则认为当前View不处理此次事件,父View会继续将事件传递给下一个View处理
return result;
}
// 由上边的 dispatchTouchEvent 可知,当OnTouchListener为空,或者OnTouchListener.onTouch()返回
// 值为false时,会调用自身的 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();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
// 根据当前View设置的Flag返回是否消费事件
// 1. 是否是Enable
// 2. Disable的时候是否可点击
if ((viewFlags & ENABLED_MASK) == DISABLED
&& (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
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 (mTouchDelegate != null) {
// 且代理的onTouchEvent返回值为true,则返回消费事件
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// 在View中,如果clickable 为false,则会直接返回 不消费事件
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.
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();
}
// 尝试将点击行为通过Handler发送到主线程, 如果发送失败,则调用内部点击事件
if (!post(mPerformClick)) {
// 在View 内部执行点击方法:调用OnClickListener的onClick方法
performClickInternal();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
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)) {
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);
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
break;
// 如果接收到了CANCEL 事件
// 如 ViewParent在某个时机拦截了事件,事件将不会继续传递给子View,会先给子View传递一个Cancel事件
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);
}
final int motionClassification = event.getClassification();
final boolean ambiguousGesture =
motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
int touchSlop = mTouchSlop;
if (ambiguousGesture && hasPendingLongPressCallback()) {
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()
* mAmbiguousGestureMultiplier);
// Subtract the time already spent
delay -= event.getEventTime() - event.getDownTime();
checkForLongClick(
delay,
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
touchSlop *= mAmbiguousGestureMultiplier;
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, touchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
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类型的组件,接收到一个事件,是用dispatchTouchEvent开始执行。
然后分情况进行事件分发:
- 若自身设置了OnTouchListener,则执行OnTouchListener的onTouch方法。
- 根据OnTouchListener的OnTouch方法的返回值判断是否执行自身的onTouchEvent方法
- onTouch 返回值为false,则执行自身的onTouchEvent方法
- onTouch返回值为true,直接返回结果。
- 若自身没有设置OnTouchListener,则执行自身的onTouchEvent并返回结果。
- dispatchTouchEvent的返回结果会告诉父布局,当前View是否处理后续事件。为true,意味着当前View会处理此次事件,否则不处理当前事件。
- 在View中没有onInterceptTouchEvent方法,不做讨论。
ViewGroup的事件分发
还是先明确框架中的问题
- 事件从何而来?
- 也是从ViewGroup而来,传递当当前ViewGroup的dispatchTouchEvent方法中
- 当然最初的事件是从框架层,native层传递过来的,传递到DecorView,然后一级一级寻找能够处理当前点击事件的View
- 事件有哪些类型?DOWN、MOVE、UP、CANCEL
- 事件的结果有什么影响?
事件的结果其实就是当前View的dispatchTouchEvent方法的返回值,它会决定父布局是否继续找寻下一个可处理事件的View。它的返回值含义是:我(当前View)是否要对当前事件进行处理。 - 我能利用这个过程中做些什么?
- 首先看Android原生做了些什么?
1. 先明确,ViewGroup也是View,如果对ViewGroup进行点击事件、长按事件的操作也可以。
2. ViewGroup 的dispathTouchEvent中,寻找一个可以处理当前事件流的View
3. 在寻找能够处理当前事件流的子View之前,会先判断是否要自己处理系列事件,若是自己处理,就不会继续传递给子View了。(onInterceptTouchEvent 返回值为true,表示不继续给子View传递事件,但是否要自己处理,还是要看OnTouchListener 或者 onTouchEvent 的返回值)
4. 处理滑动相关行为(ScrollView,RecyclerView~)。 - 我们能做一些什么?
1. 可以处理滑动冲突,嵌套滚动
2. 可以决定让特定的子View处理事件
3. 可以通过拦截事件,做一些算法,来协调子View与当前View的行为。
ViewGroup事件分发源码分析
dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
// ......
// 标记此次事件是否被处理了,在方法的最后返回该值。
// 若该值为false,则相当于告知父布局,当前View以及子View都无法处理这次事件,交由父布局继续处理。
// 若该值为true,则相当于告知父布局,当前View或者子View中有“人”处理了系列事件,
// 父布局(一般情况下)不会再继续分发事件,而是交由当前处理事件的View进行处理
boolean handled = false;
// 处理此次事件是否在安全域内:主要是根据当前Window的状态来判断,大多是情况下,我们不需要考虑。
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 前面我们说过,每一次事件流都是由按下开始的,所以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.
// 每次一次事件流中,都会形成一个TouchTarget链表,保存了触摸事件的目标相关信息,后面我们专门说一下。
// 此处是将之前事件流中所创建的链表,传递Cancel事件,并清除触摸状态。
cancelAndClearTouchTargets(ev);
// 重置触摸状态,回收建立的TouchTarget链表
resetTouchState();
}
// Check for interception.
// 检查是否要拦截此次事件
final boolean intercepted;
// 按下事件,或者 之前建立过 FirstTouchTarget链表。
// 为什么要这么判断?
// 1. 按下事件是起点,所以可以判断是否拦截。
// 2. mFirstTouchTarget != null,证明当前ViewGroup中有可以处理事件的子View,
// 于是才有拦截的必要, 如果没有人可以处理事件,拦截它干嘛呢?所以此处我们知道了,
// onInterceptTouchEvent,不是任何时候都能执行的。
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 获取当前ViewGroup的 标志位:FLAG_DISALLOW_INTERCEPT 是否被置为1,
// disallowIntercept 标志可以动过调用ViewGroup.requestDisallowInterceptTouchEvent进行设置
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// 如果 没有被标记为 「不可以拦截事件」
if (!disallowIntercept) {
// 则执行onInterceptTouchEvent();
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
// 否则,拦截值为false。
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// 检查是否当前事件是「取消」类型
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
&& !isMouseEvent;
// 此处开始创建新的TouchTarget链表
TouchTarget newTouchTarget = null;
// 初始化是否分发至新的TouchTarget 的flag
boolean alreadyDispatchedToNewTouchTarget = false;
// 如果不是取消事件,并且没有被拦截,执行找寻TouchTarget 的行为。
if (!canceled && !intercepted) {
// If the event is targeting accessibility focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
// 首先判断 当前事件是不是被标记为:「无障碍焦点」“优先”,
// 如果被标记为「无障碍焦点」优先,则会先寻找带有 「无障碍焦点」子View
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
// 1. 当前事件 是按下事件
// 2. 当前事件是 多点模式下的按下事件
// 3. 当前事件是 悬停移动事件(比如鼠标放在按钮上但不按下时的移动)
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
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.
// 清除对应id下的 TouchTargets,如果是单点触摸,一般就是清除所有
removePointersFromTouchTargets(idBitsToAssign);
// 当前子节点的个数
final int childrenCount = mChildrenCount;
// 如果当前新的 TouchTarget为空,并且子View数目不为0
if (newTouchTarget == null && childrenCount != 0) {
// 获取当前事件的x坐标
final float x =
isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
// 获取当前事件的y坐标
final float y =
isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
// 创建一个 排序好的子View列表,我们在此处可以利用重写 buildTouchDispatchChildList 方法,
// 提供我们想要View处理Touch事件的优先级。
// 默认实现为根据Z值,返回排序好的子View列表,如果没有子View设置了Z值,此处返回的值为null
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
// 是否是自定义顺序
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
[循环开始] for (int i = childrenCount - 1; i >= 0; i--) {
// 获取当前 index对应的 child 的index
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
// 获取当前 index对应的child
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;
}
// 如果当前child无法接收事件: 不可见,或者动画为空
if (!child.canReceivePointerEvents()
// 当前按下点,不在View的范围内
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
// 继续寻找
continue;
}
// 在当前的TouchTarget链表中查找是否存在是当前child的TouchTarget
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;
}
// 清除状态
resetCancelNextUpFlag(child);
// 分发转换后的事件,如果结果为true
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();
// 创建新的TouchTarget
// 实际上是讲当前TouchTarget放置到了链表头。
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();
}
// 如果 没有找到新的TouchTarget, 并且TouchTarget链表头不为空, 重置多点触控状态
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
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.
// 如果没有找到能处理当前事件的目标,执行dispatchTransformedTouchEvent
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 predecessor = null;
TouchTarget target = mFirstTouchTarget;
// 如果Target 存在值,
while (target != null) {
// 获取下一个节点
final TouchTarget next = target.next;
// 如果已经分发到新节点了,并且 当前的target就是,那个新节点。
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
// 标记为已处理
handled = true;
} else {
// 否则,重置目标节点的状态
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 将cancel事件传递给那个目标
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
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) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
dispatchTransformedTouchEvent
// 在上边的dispatchTouchEvent中,会调用到当前方法。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
@Nullable 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);
// 如果 未传递child,则证明事件交给自己处理。
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
// 否则调用 子View的 dispatchTouchEvent
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) {
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;
// 如果触摸模式并没有发生变化:触摸手指数没发生变化。满足以下条件
// 1. 并没有由单点触摸变换成多点触摸
// 2. 没有由多点触摸,切换出单点触摸
// 3. 没有增加或减少手指
if (newPointerIdBits == oldPointerIdBits) {
// 如果child 为空,或者 子View拥有变换矩阵
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
// 如果child为空,则调用自己的dispatchTouchEvent。具体实现在View中
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
// 否则,调用子View 的 dispatchTouchEvent
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
// 返回处理结果
return handled;
}
// 转换事件
transformedEvent = MotionEvent.obtain(event);
} else {
// 因为触摸点数发生变化,通过split转换事件
transformedEvent = event.split(newPointerIdBits);
}
// 分发转换后的事件。基本上与上面相同。
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()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
onInterceptTouchEvent
//ViewGroup中的 onInterceptTouchEvent 实现很简单,需要同时满足以下条件
// 1. 在ActionDown时
// 2. 事件源是鼠标
// 3. 按钮按下
// 4. 在滚动条上
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
ViewGroup中的其他设计
TouchTarget
结构
private static final class TouchTarget {
// 最大回收数
private static final int MAX_RECYCLED = 32;
// 锁
private static final Object sRecycleLock = new Object[0];
// 回收池
private static TouchTarget sRecycleBin;
// 已回收的数目
private static int sRecycledCount;
public static final int ALL_POINTER_IDS = -1; // all ones
// 目标View
@UnsupportedAppUsage
public View child;
// 触控点数标志
public int pointerIdBits;
// 下一个触摸目标节点
public TouchTarget next;
}
设计思路
- 属于单向链表结构
- 回收池机制,方便内存复用
其实与Message的设计思路很像,你说是不是?
DisallowIntercept
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
在ViewGroup中,提供了requestDisallowInterceptTouchEvent
方法,调用该方法后,此次事件流传递过程中,它将不再拦截,但是当此次事件流结束后,下一次事件流,依然会做拦截。
总结
- ViewGroup中分发事件时,先判断是否要拦截事件,如果拦截事件,则交由自己处理。
- ViewGroup未拦截事件,ViewGroup寻找能够处理事件的子View。其实只要点击的位置在对应的子View的实际范围内,就会调用其dispatchTouchEvent,然后根据其返回值,决定是否继续寻找下一个可处理的子View。
- 如果没有找到可以处理此次事件的子View,会将事件交由自己处理。如果自己仍然返回了false,则事件会继续交会给当前ViewGroup的父布局进行处理。