View事件分发(二)
事件分发的源码解析
Activity对点击事件的分发过程
点击事件用MotionEvent
表示,当一个点击操作发生时,事件最先传递给当前Activity
,由Activity
的dispatchTouchEvent
进行事件分发,而具体的工作是由Activity
内部的Window
来完成的。Window
会将事件传递荷藕DecorView
,DecorView
一般就是当前界面的一层容器(即setContentView
所设置的View的父容器),通过Activity.getDecorView()
获取,先从Activity
的dispatchTouchEvent
开始分析。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
分析以上代码,首先代码将交给Activity
所附属的Window
进行分发,如果返回true,那么整个事件循环就结束了。额如果返回了false,就代表事件没人处理,即所有View的onTouchEvent
都返回了false,那么Activity
的onTouchEvent
就会被调用。
接下来看Window
是如何将事件传递给ViewGroup
的,在Android源码中,Window
是一个抽象类,而Window
的superDispatchTouchEvent
方法也是个抽象方法。
Android源码中,Window
有且仅有一个实现类PhoneWindow
,以下是PhoneWindow
对superDispatchTouchEvent
的实现。
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
由源码可知,PhoneWindow
将事件直接传递给了DecorView
。
DecorView
通过((ViewGroup) (getWindow().getDecorView().findViewById(android.R.id.content))).getChildAt(0)
可以获取Activity
所设置的View,而上面PhoneWindow
的mDector
就是Activity.getWindow().getDecorView()
所返回的View是它的一个子View。
目前事件传递到了DecorView
这里,由于DecorView
继承FrameLayout
且是父View,所以最终事件都会传递给View,即事件肯定会传递到View。
DecorView对事件的分发过程
点击事件到达DecorView
后,会调用ViewGroup
的dispatchTouchEvent
方法。接下来:如果DecorView
的onInterceptTouchEvent
返回true,则事件由ViewGroup
处理,这时,如果ViewGroup
的mOnTouchListenr
被设置,则onTouch
会被调用,否则onTouchEvent
会被设置。假如两者都提供的话,onTouch
会屏蔽掉onTouchEvent
。在onTouchEvent
中,如果设置了mOnClickListener
,则onClick
会被调用。
如果顶级ViewGroup
不拦截事件,那么事件会传递给它所在的点击事件链上的子View,这时,子View的dispatchTouchEvent
会被调用。
上述过程中,点击事件已经从顶级View传递到下一层View,接下来的传递过程和顶级View是一致的,如此循环,完成整个事件的分发。
ViewGroup对事件的分发过程
首先要看的是ViewGroup
对点击事件的分发过程,主要事件在ViewGroup
的dispatchTouchEvent
方法中,由于这个方法比较长,所以需要分段说明,以下是"View是否拦截点击事件“部分的代码。
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
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.
intercepted = true;
}
从以上代码可知,ViewGroup
在以下两种情况下会判断是否要拦截当前事件。
- 事件类型为
ACTION_DOWN
mFirstTouchTarget != null
事件类型为ACTION_DOWN
时,肯定会进行拦截。而mFirstTouchTarget != null
的原因是,当事件由ViewGroup
的子元素成功处理时,mFirstTouchTarget
会被赋值并指向子元素,即当ViewGroup
不拦截事件并将事件交由给子元素处理时,mFirstTouchTarget != null
成立。而反过来,一旦事件由当前ViewGroup
拦截时,mFirstTouchTarget != null
不成立,并且当ACTION_MOVE
和ACTION_DOWN
事件到来时,由于actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null
为false,将导致ViewGroup
的onInterceptTouchEvent
不会再被调用,并且同一事件的其他序列将都默认交给它处理。
上面代码中有一种额外的情况,注意FLAG_DISALLOW_INTERCEPT
这个标识位,这个标识位是通过之前所说子View的requestDisallowInterceptTouchEvent
方法设置的。而FLAG_DISALLOW_INTERCEPT
一旦设置后,ViewGroup
将无法拦截除了ACTION_DOWN
以外的点击事件,而这里ACTION_DOWN
可以拦截的原因是因为ViewGroup
在分发事件时,处理ACTION_DOWN
的时候会重置FLAG_DISALLOW_INTERCEPT
这个标识位,将会导致子View设置的这个标识位无效。因此,ACTION_DOWN
事件总会让ViewGroup
调用onInterceptTouchEvent
方法来判断是否要拦截事件。以下是ViewGroup
重置状态的源码。
// 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);
resetTouchState();
}
这段源码的位置在之前源码的上面。这端逻辑在处理ACTION_DOWN
事件的时候将FLAG_DISALLOW_INTERCEPT
重置,所以子View的requestDisallowInterceptTouchEvent
方法并不能影响ViewGroup
对ACTION_DOWN
的处理。
结论
从以上的代码分析出,当ViewGroup
决定拦截事件后,那么后续的点击事件将会默认交给它处理并且不再调用它的onIntercept
方法。其中FLAG_DISALLOW_INTERCEPT
这个标识的作用是让ViewGroup
不再拦截事件,当前前提是ViewGroup
不拦截ACTION_DOWN
事件。
以上分析总结起来有两点。
-
onInterceptTouchEvent
不是每次事件都会被调用的。如果要在代码中提前处理所有的点击事件,可以选择dispatchTouchEvent
方法,只有这个方法确保每次都能被调用,而这个前提的保证条件是事件可以传递到当前的ViewGroup
。 -
FLAG_DISALLOW_INTERCEPT
标识位可以作为解决滑动冲突的一种思路。
ViewGroup不拦截时候,对子View的分发过程
当ViewGroup
不拦截事件的时候,事件会向下分发给它的子View进行处理。以下是源码实现。
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;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
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);
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);
}
以上的代码中,首先遍历这个ViewGroup
的子元素,然后判断子元素是否能接受到点击事件。这里是否可以接收到点击事件由两种情况来判定,子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内,如果子元素同时满足这两个条件,那么事件将传递给它处理。
其中,dispatchTransformedTouchEvent
实际上调用的是子元素的dispatchTouchEvent
方法,在其内部有一段内容,而又因为其中的子元素不为null,因此会直接调用子元素的dispatchTouchEvent
方法,这样事件就交由子元素处理,完成一轮事件的分发。
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
如果子元素的dispatchTouchEvent
返回true,那么mFirstTouchTarget
就会被赋值同时跳出for循环,代码如下。
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
以上代码完成了mFirstTouchTarget
的赋值并终止对子元素的遍历。而如果子元素的dispatchTouchEvnet
返回了false,ViewGroup
就会把事件分发给下一个元素(如果还有下一个元素的话)。
以上mFirstTouchTarget
的过程是在addTouchTarget
内部执行的。从其实现中可以看出,mFirstTarget
其实是一种单链表的结构。而mFirstTouchTarget
是否被赋值,将直接影响ViewGroup
内部的对事件的拦截策略,如果mFirstTouchTarget
为null,那么ViewGroup
就默认拦截接下来同一序列的所有点击事件。
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
遍历所有的子元素后事件都没被合适的处理有两种情况。
-
ViewGroup
没有子元素。 - 子元素处理了点击事件,但是在
dispatchTouchEvent
中返回了false,这一般是因为子元素在onTouchEvent
返回了false。在这两种情况下,ViewGroup
会自己处理点击事件。
上述情况的代码如下。
// 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);
}
这里第三个参数的child
,从前面的dispatchTransformedTouchEvent
实现可知,它会调用super.dispatchTouchEvent(event)
,而ViewGroup
的父类即View
类,所以这里就调用了View
的dispatchTouchEvent
方法,即点击事件交由View
类处理。
View类对点击事件的处理过程
View对点击事件处理过程比较简单,这里的View
不包含ViewGroup
,以下是dispatchTouchEvent
方法,如下所示。
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
// some code about accessibility
boolean result = false;
// some code
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)) {
result = true;
}
}
// some code
return result;
}
View
对点击事件的处理比较简单,因为View
是一个单独的元素,它没有子元素所以没必要向下传递事件,所以它只能直接处理事件。
从上面源码可以得知,首先会判断有没有设置OnTouchListener
,并且如果OnTouchListener
中的onTouch
方法返回true,按摩onTouchEvent
就不会被调用,所以得知OnTouchListener
的优先级高于onTouchEvent
,这样做的好处是方便在外界处理点击事件。
接着分析onTouchEvent
的实现。先看当View
处于不可用状态下的点击事件处理过程,代码如下。
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
从代码中得知,disable
状态下的View
照样会消耗点击事件。
接着,如果View
设置了代理,那么还会执行TouchDelegate
的onTouchEvent
方法,这个onTouchEvent
的工作机制和OnTouchListener
看起来相似,代码如下。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
接下来看onTouchEvent
对点击事件的具体处理,代码如下。
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
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;
}
从以上源码中可以得知,只要View
的CLICKABLE
和LONG_CLICKABLE
有一个为true,那么它就会消耗这个事件,即onTouchEvent
方法返回true,不管它是不是DISABLE
状态。而当ACTION_UP
事件产生后,会触发performClick
方法,这时如果View
设置了onClickListener
,那么performClick
方法内部会调用它的onClick
方法,代码如下。
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
View
的LONG_CLICKABLE
属性默认为false,而CLICKABLE
属性是否为false要看具体的View,总的来说,就是可点击的View的CLICKABLE
为true,不可点击的View为false,,比如Button
是可以点击的,但TextView
是不可点击的,而通过setClickable和setLongClickable可以分别改变View
的CLICKABLE
和LONG_CLICKABLE
属性。另外,setOnClickListener
和setOnLongClickListener
会自动设置以上两个属性为true。
到此,事件分发的源码实现已经分析完毕。