Android 事件分发机制源码解析,最新 Android 通用流行框架大全
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)) {
handled = true;
}
...
}
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);
}
}
return handled;
}
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
// 注意传参 child 为 null 时,调用的是自己的 dispatchTouchEvent
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 默认不拦截事件
return false;
}
这个方法比较长,只要把握住主要脉络,修枝剪叶后还是非常清晰的:
(1) 判断事件是够需要被 ViewGroup 拦截
首先会根据mGroupFlags
判断是否可以执行onInterceptTouchEvent
方法,它的值可以通过requestDisallowInterceptTouchEvent
方法设置:
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) {
// 层层向上传递,告知所有父 View 不拦截事件
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
所以我们在处理某些滑动冲突场景时,可以从子 View 中调用父 View 的requestDisallowInterceptTouchEvent
方法,阻止父 View 拦截事件。
如果 view 没有设置FLAG_DISALLOW_INTERCEPT
,就可以进入 onInterceptTouchEvent 方法,判断是否应该被自己拦截,ViewGroup 的 onInterceptTouchEvent 直接返回了 false,即默认是不拦截事件的,ViewGroup 的子类可以重写这个方法,内部判断拦截逻辑。
**注意:**只有当事件类型是ACTION_DOWN
或者 mFirstTouchTarget 不为空时,才会走是否需要拦截事件这一判断,如果事件是ACTION_DOWN
的后续事件(如ACTION_MOVE
、ACTION_UP
等),且在传递ACTION_DOWN
事件过程中没有找到目标子 View 时,事件将会直接被拦截,交给 ViewGroup 自己处理。mFirstTouchTarget 的赋值会在下一节提到。
(2) 遍历所有子 View,逐个分发事件:
执行遍历分发的条件是:当前事件是ACTION_DOWN
、ACTION_POINTER_DOWN
或者ACTION_HOVER_MOVE
三种类型中的一个(后两种用的比较少,暂且忽略)。所以,如果事件是ACTION_DOWN
的后续事件,如ACTION_UP
事件,将不会进入遍历流程!
进入遍历流程后,拿到一个子 View,首先会判断触摸点是不是在子 View 范围内,如果不是直接跳过该子 View;否则通过dispatchTransformedTouchEvent
方法,间接调用child.dispatchTouchEvent
达到传递的目的;
如果dispatchTransformedTouchEvent
返回 true,即事件被子 View 消费,就会把 mFirstTouchTarget 设置为 child,即不为 null,并将 alreadyDispatchedToNewTouchTarget 设置为 true,然后跳出循环,事件不再继续传递给其他子 View。
可以理解为,这一步的主要作用是,在事件的开始,即传递ACTION_DOWN
事件过程中,找到一个需要消费事件的子 View,我们可以称之为目标子View
,执行第一次事件传递,并把 mFirstTouchTarget 设置为这个目标子 View
(3) 将事件交给 ViewGroup 自己或者目标子 View 处理
经过上面一步后,如果 mFirstTouchTarget 仍然为空,说明没有任何一个子 View 消费事件,将同样会调用 dispatchTransformedTouchEvent,但此时这个方法的View child
参数为 null,所以调用的其实是super.dispatchTouchEvent(event)
,即事件交给 ViewGroup 自己处理。ViewGroup 是 View 的子 View,所以事件将会使用 View 的 dispatchTouchEvent(event)方法判断是否消费事件。
反之,如果 mFirstTouchTarget 不为 null,说明上一次事件传递时,找到了需要处理事件的目标子 View,此时,ACTION_DOWN
的后续事件,如ACTION_UP
等事件,都会传递至 mFirstTouchTarget 中保存的目标子 View 中。这里面还有一个小细节,如果在上一节遍历过程中已经把本次事件传递给子 View,alreadyDispatchedToNewTouchTarget 的值会被设置为 true,代码会判断 alreadyDispatchedToNewTouchTarget 的值,避免做重复分发。
小结:dispatchTouchEvent 方法首先判断事件是否需要被拦截,如果需要拦截会调用
onInterceptTouchEvent
,若该方法返回 true,事件由 ViewGroup 自己处理,不在继续传递。若事件未被拦截,将先遍历找出一个目标子 View,后续事件也将交由目标子 View 处理。若没有目标子 View,事件由 ViewGroup 自己处理。此外,如果一个子 View 没有消费
ACTION_DOWN
类型的事件,那么事件将会被另一个子 View 或者 ViewGroup 自己消费,之后的事件都只会传递给目标子 View(mFirstTouchTarget)或者 ViewGroup 自身。简单来说,就是如果一个 View 没有消费ACTION_DOWN
事件,后续事件也不会传递进来。
View
现在回头看上一节的第 2、3 步,不管是对子 View 分发事件,还是将事件分发给 ViewGroup 自身,最后都殊途同归,调用到了 View 的dispatchTouchEvent
,这就是我们这一节分析的目标。
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (onFilterTouchEventForSecurity(event)) {
// 判断事件是否先交给 ouTouch 方法处理
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
// onTouch 未消费事件,传给 onTouchEvent
if (onTouchEvent(event)) {
return true;
}
}
...
return false;
}
代码量不多,主要做了三件事:
若 View 设置了 OnTouchListener,且处于 enable 状态时,会先调用 mOnTouchListener 的 onTouch 方法
若 onTouch 返回 false,事件传递给
onTouchEvent
方法继续处理若最后 onTouchEvent 也没有消费这个事件,将返回 false,告知上层 parent 将事件给其他兄弟 View
这样,我们的分析转到了 View 的onTouchEvent
方法:
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PRESSED) != 0) {
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
// 如果一个 View 处于 DISABLED 状态,但是 CLICKABLE 或者 LONG_CLICKABLE 的话,这个 View 仍然能消费事件
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
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.
mPrivateFlags |= PRESSED;
refreshDrawableState();
}
if (!mHasPerformedLongPress) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only per
form 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)) {
performClick();
}
}
}
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();
}
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
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 |= PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
mPrivateFlags |= PRESSED;
refreshDrawableState();
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
return true;
}
return false;
}
评论