有人问你,能详细介绍一下事件传递机制吗?
事件传递过程中涉及到的主要类有:Activity,ViewGroup,View,主要方法有:dispatchTouchEvent, onTouchEvent, onInterceptTouchEvent, requestDisallowInterceptTouchEvent,涉及到的事件主要有:ACTION_DOWN, ACTION_MOVE, ACTION_UP
一个事件最先到达Activity,再到ViewGroup,最后到View,如果有人消费了事件,则事件不会往回传递了,否则事件会回到Activity,呈一个U行传递。如果ViewGroup的onInterceptTouchEvent返回true,则会拦截事件,事件不会传递到view,View也可以通过调用requestDisallowInterceptTouchEvent禁止ViewGroup拦截事件,了解事件传递对解决滑动冲突非常有帮助。
但这里有太多的疑问了,比如:
1、Activity的事件是谁传给它的?
2、Down, Move,Up分别是怎么传递的?
3、Fragment在这个过程中起到什么作用了吗?
4、Activity的事件是怎么给到View的?
5、窗口的层级关系,window,ViewRootImpl,View什么关系,怎么分层?
6、滑动冲突产生的原因,怎么解决?
咱们先详细了解整个过程,再一一解答这些疑问。
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
Activity的dispatchTouchEvent,目前我们知道的事件处理是从这里开始的。
咱们都知道Activity的mWindow其实是PhoneWindow,所以看PhoneWindow的superDispatchTouchEvent
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
DecorView其实是一个ViewGroup,它是一个根ViewGroup,是在setContentView时创建的,平时我们使用的android.R.id.content就被添加到DecorView中,而咱们自己写的布局是加在content上的。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
所以,DecorView的superDispatchTouchEvent最终会调用ViewGroup的dispatchTouchEvent方法。
下面咱们看一下dispatchTouchEvent的代码:
第一部分
// 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;
}
上边的代码用来判断是否拦截事件,如果不是Down事件并且touch target为空,则拦截事件,事件由自己处理。mFirstTouchTarget != null后边会详细说。
@Override
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);
}
}
咱们可以调用requestDisallowInterceptTouchEvent来避免ViewGroup拦截事件。
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;
}
你可以重写onInterceptTouchEvent来决定是否拦截事件。
Down 事件到来时,如果此方法返回true,则mFirstTouchTarget不会被赋值,那么后续的事件都会被拦截,onInterceptTouchEvent只会被回调一次。
下面继续看dispatchTouchEvent的代码。
接下来分两种情况:ViewGroup拦截和不拦截。
先看不拦截的情况
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;
}
// 1、判断子View是否满足条件
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);
// 2、事件分发给目标子view
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();
}
上边的代码遍历ViewGroup的子view,查找处理事件的子View。
主要分为两部分
1、判断子view是否能够接收事件
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
/**
* Returns true if a child view can receive pointer events.
* @hide
*/
private static boolean canViewReceivePointerEvents(@NonNull View child) {
return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null;
}
判断子view是否正在播放动画,如果是则跳过。
/**
* Returns true if a child view contains the specified point when transformed
* into its coordinate space.
* Child must not be null.
* @hide
*/
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempPoint();
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, child);
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
判断点击事件是否落在子view的范围内。
如果点击事件落在子view范围内,且没有动画正在执行,则可接收事件,继续看第二部分。
2、事件分发给子view
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;
}
方法dispatchTransformedTouchEvent中如果参数child为空,则调用ViewGroup的dispatchTouchEvent方法处理事件,如果child不空,则调用child的dispatchTouchEvent方法处理事件。
如果child的处理当前事件,则mFirstTouchTarget会被赋值,并跳出循环,否则继续查找下一个满足条件的子view。如果没找到,它会自己处理事件。
从上边拦截部分的讲解可知,如果mFirstTouchTarget为空,则ViewGroup会拦截接下来的全部事件。
到这里事件就由父view传到了子view。
再看拦截的情况
如果ViewGroup拦截事件,则mFirstTouchTarget为空
// 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);
}
dispatchTransformedTouchEvent方法传child为空,则调用super#dispatchTouchEvent,其实就是View#dispatchTouchEvent方法。
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
以上就是ViewGroup部分的事件处理流程,接下来看View部分的分析
View部分的逻辑比较简单
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;
}
}
在dispatchTouchEvent方法中会先判断View是否设置了OnTouchListener,如果设置了并且onTouch返回true,则会屏蔽View的onTouchEvent回调,否则执行onTouchEvent
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) {
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;
}
首先判断当前组件是否可点击,如果是可点击的,即使现在是disable状态,事件仍然会被消费。
如果组件不是clickable的,那事件不会被消费。
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) {
checkForLongClick(0, x, y);
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(0, x, y);
}
break;
在DOWN事件中主要是设置按压状态,开始长按倒计时等操作。
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
MOVE时主要检查手指当前是否仍在View中。
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();
}
if (!post(mPerformClick)) {
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;
在Up事件中,主要是状态的设置和操作的执行,如果长按事件已经执行,则不在执行click操作,否则执行click处理。
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
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);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
执行点击操作的逻辑也比较简单。
到这里事件分发基本流程就结束了。
下面总结一下:
Down事件下发之后,在ViewGroup会先进行判断是否拦截,如果拦截,则后续的Move, Up事件均会被拦截,事件走它自己的处理逻辑。如果不拦截,则事件达到子view中,如果子view是可点击的,则会消耗事件。后续的Move,Up事件中ViewGroup仍然可决定是否拦截。如果子view不处理,则回到ViewGroup处理,如果ViewGroup不处理,继续回到父ViewGroup处理,直到Activity。
如果子view在Down事件返回false,那么后续的MOVE和UP均无法收到。
Move和Up事件的分发和Down事件类似,这里不再展开分析。
1、Activity的事件是谁传给它的?
image.png
参考:https://juejin.cn/post/6918272111152726024
2、Down, Move,Up分别是怎么传递的?
image.png
图片来源 http://gityuan.com/2015/09/19/android-touch/
3、Fragment在这个过程中起到什么作用了吗?
Fragment在事件分发过程中没起作用,Fragment的布局也就是在onCreateView中创建的View会被添加到Fragment在Activity的容器View中,这样就正常被添加到View tree中,可以正常接受分发的事件。
// 1、FragmentManager
f.mContainer = container;
// 这里调用onCreateView给mView赋值
f.performCreateView(f.performGetLayoutInflater(
f.mSavedFragmentState), container, f.mSavedFragmentState);
if (f.mView != null) {
f.mInnerView = f.mView;
f.mView.setSaveFromParentEnabled(false);
if (container != null) {
container.addView(f.mView);
}
// 2、FragmentController
/**
* Attaches the host to the FragmentManager for this controller. The host must be
* attached before the FragmentManager can be used to manage Fragments.
*/
public void attachHost(Fragment parent) {
mHost.mFragmentManager.attachController(
mHost, mHost /*container*/, parent);
}
// 3、Activity中创建
final FragmentController mFragments = FragmentController.createController(new HostCallbacks());
4、Activity的事件是怎么给到View的?
这个上边已经说到了,Activity -> PhoneWindow -> DecorView -> ViewGroup->View
5、窗口的层级关系,window,ViewRootImpl,DecorView什么关系,怎么分层?
在调用WindowManager#addView时会创建ViewRootImpl,这里addView其实是添加的DecorView,并使DecorView和ViewRootImpl产生关联。
6、滑动冲突产生的原因,怎么解决?
滑动冲突的场景:
1、外部滑动和内部滑动方向不一致。
2、外部滑动和内部滑动方向一致。
3、1和2两种情况的嵌套。
解决方法:外部拦截法(推荐)和内部拦截法
外部拦截法:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器处理事件) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
return intercepted;
}
ACTION_DOWN事件不能拦截,否则子view收不到后续的MOVE和UP事件了。
内部拦截法需要父View和子View配合修改
// 父View 修改部分
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
}
return true;
}
父View默认拦截除ACTION_DOWN之外的全部事件。因为ACTION_DOWN不受requestDisallowInterceptTouchEvent的控制。
// 子View修改部分
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要处理事件) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
子View通过requestDisallowInterceptTouchEvent禁止父View拦截事件。如果父View需要处理事件,则父View拦截事件。
参考:《Android开发艺术探索》等








网友评论