一,View的事件分发
先概述下 View事件分分流程:View.dispatchEvent->View.setOnTouchListener->View.onTouchEvent,接下来详细分析各个流程:
1. dispatchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
//****************省略部分代码******************
boolean result = false;
//****************省略部分代码******************
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 如果是ACTION_DOWN(一个事件序列的开始),则清除之前的手势
stopNestedScroll();
}
//一些安全策略上的过滤
if (onFilterTouchEventForSecurity(event)) {
//View的状态是 enabled 的,并且可以像处理ScrollBar拖曳事件一样处理,则result = true
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//如是mOnTouchListener的onTouch方法能处理(返回true),则result = true
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//如果以上都没能处理这个事件,则看当前 View的onTouchEvent能否处理
if (!result && onTouchEvent(event)) {
result = true;
}
}
//****************省略部分代码******************
return result;
}
其中mOnTouchListener就是我们调用setOnTouchListener方法设置上去的,也就是说如果OnTouchListener的 OnTouch方法返回ture,那么事件的处理就结束了,否则就交给onTouchEvent方法处理了;
2. onTouchEvent(event)
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的状态是 disable 的
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
//如果一个状态为 disable 的View,但它却是 clickable 也是可以消费事件的,只是没有什么反映。
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
//如果设置了TouchDelegate,并且TouchDelegate消费了该事件,则返回true(即:消费该事件)
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
//进入了switch,则一定会返回true
switch (action) {
case MotionEvent.ACTION_UP:
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();
}
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();
}
mIgnoreNextUpEvent = false;
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 |= 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(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
重点在 switch 模块,代码比较长,我们将代码按执行顺序拆解开来:
2.1 MotionEvent.ACTION_DOWN
case MotionEvent.ACTION_DOWN:
//设置处理长按事件的标志位为 false
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
//这里是判断当前 View是不是在一个可以滚动的ViewGroup中
boolean isInScrollingContainer = isInScrollingContainer();
//如果是在一个可以滚动的 ViewGroup 中,则延时处理 press的回调,因为可能是一个滚动
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
//延时一定时间(100ms),CheckForTap是一个 Runnable对象
//ViewConfiguration.getTapTimeout()=100ms ,代表着如果在这时间间隔内没有移动,则是一个轻触事件,否则是一个滚动事件
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
2.2 CheckForTap
private final class CheckForTap implements Runnable {
public float x;
public float y;
@Override
public void run() {
mPrivateFlags &= ~PFLAG_PREPRESSED;
setPressed(true, x, y);
checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
}
}
最后无论 View 是否在一个可以滚动的 ViewGroup 中,ACTION_DOWN 时都会调用setPressed 与 checkForLongClick 方法,setPressed 方法主要是更新些状态的标志位以及刷新背景,而checkForLongClick负责处理长按事件,如果支持长按事件的响应并设置了mOnLongClickListener调用OnLongClickListener的onLongClick方法,接着看 ACTION_MOVE 事件
2.3 MotionEvent.ACTION_MOVE
case MotionEvent.ACTION_MOVE:
//刷新背景
drawableHotspotChanged(x, y);
// 如果移出当前View
if (!pointInView(x, y, mTouchSlop)) {
//取消之前ACTION_DOWN中设置的所有回调
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
removeLongPressCallback();
setPressed(false);
}
}
break;
2.4 MotionEvent.ACTION_UP
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
//*****************省略部分代码*******************
//如是长按事件没有发生或者发生了但返回 false
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 移除 ACTION_DOWN时设置的长按回调
removeLongPressCallback();
// 如是获取了焦点
if (!focusTaken) {
//下面是执行我们熟悉的onClick事件
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();
}
mIgnoreNextUpEvent = false;
break;
至此,Veiw的事件分发机制分析完毕。接下来分析一下 ViewGroup 的事件分发机制。
二. ViewGroup的事件分发机制
ViewGroup 继承自 View ,但它多了一个onInterceptTouchEvent的过程,大致流程如下:dispatchTouchEvent ->onInterceptTouchEvent->子 View, 照例,先看一下dispatchTouchEvent方法
1. dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
//**********************省略部分代码**********************
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
//当为ACTION_DOWN事件时,重置所有的 Touch 状态,这里可能会影响上一个事件序列的up 或者 cancel事件,从而可能导致 App丢帧或者 ANR
cancelAndClearTouchTargets(ev);
resetTouchState();
}
final boolean intercepted;
//注意这里的判断条件ACTION_DOWN,mFirstTouchTarget代表者事件处理者
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//disallowIntercept 代表着是否允许拦截,
//disallowIntercept 可以通过 ViewGroup.requestDisallowInterceptTouchEvent(boolean disallowIntercept )设置
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//如果允许拦截,则调用onInterceptTouchEvent(ev)判断是否拦截
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
//如果不是 ACTION_DOWN事件,并且在 ACTION_DOWN 事件中没找到处理者(mFirstTouchTarget == null)则对事件进行拦截
intercepted = true;
}
//**********************省略部分代码**********************
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
//注意这里的判断条件,ACTION_DOWN
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;
//**********************省略部分代码**********************
//下面是重点,对每一个子 View进行循环,如果当前点击事件在子 Veiw的区域内,则调用子 View 的dispatchTouchEvent进行事件的处理。
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;
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;
}
//判断是当前子 View能否接收事件以及点击区域是否在当前子 View的范围内
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
//**********************省略部分代码**********************
//如果子 View可以处理当前事件,即子 View的dispatchTouchEvent方法返回 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();
//这里会给mFirstTouchTarget赋新值
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.
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.
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;
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)) {
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);
}
}
//**********************省略部分代码**********************
return handled;
}
2.onInterceptTouchEvent
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;
}
这个函数比较简单,但有以下几点需要说明的:
1. 这个函数会接收 ACTION_DOWN事件;
2. ACTION_DOWN事件要么被 child view 的onTouchEvent处理,或者被当前的 onTouchEvent 处理,所以你实现的onTouchEvent应当返回 true,当返回 true时,事件序列中的其它事件都将由此onTouchEvent处理,onInterceptTouchEvent不会再被调用;
3. 如果onInterceptTouchEvent返回 false,之后所有的事件都将分发到这里来,然后再分发到相应的onTouchEvent中;
4. 如果onInterceptTouchEvent返回 true,你将无法接收到其余事件,所有事件都会直接交给当前的onTouchEvent处理,但是如果 onTouchEvent不处理 ACTION_DOWN事件,那么其余事件都不会交给它处理;
三,总结
- 在 View的事件分发中,dispatchTouchEvent负责了所有的分发工件,首先会分发给当前View的OnTouchListener,如果OnTouchListener不能够处理(返回flase)当前事件,那么就交给 OnTouchEvent 处理。在 OnTouchEvent 中,首先在 ACTION_DOWN中会设置一个longClick的延时回调,如果 longClick 回调执行了并且 LongClickListener处理了(返回 true)事件,那么事件就被消费掉了,否则在 ACTION_UP时,事件会交给 OnClickListener 处理;
- 在 ViewGroup 的事件分发中,还是 dispatchTouchEvent 负责所有的分发工作,不过多了一个onInterceptTouchEvent,由onInterceptTouchEvent决断是否拦截当前事件,如果不拦截的话,交给能处理的子 View处理。子 View的处理方式同 ViewGroup或者,层层向下,如果在 ACTION_DOWN 时拦截了事件,onInterceptTouchEvent就不会再被调用了,所有事件都会直接交给当前的onTouchEvent处理,但是如果 onTouchEvnet不处理 ACTION_DOWN事件,那么其余事件都不会交给它处理;
- 事件的分发是层层向下的,只要有一层拦截了,那么下面的就接收不到事件,而处理是层层向上的,只要有一层处理了事件,那么事件就结束了,否则向上分发;