我们都知道,用户与app进行交互都是通过activity来进行的,而我们平时在activity中设置的view是怎么接收到用户交互的事件呢,activity与view又有怎么样的层级关系呢?我们来看一个很简单的页面:
图一
图二
图三
从三张图我们可知,我们在activity中通过setContentView设置的view就是图一中的ContentViews部分,它的根布局是FrameLayout,也是图二中的红色区域。图三中的红色区域就是图一中的TitleView。知道了activity与view的层级关系后,那么事件是怎么一步步传到view中的呢?
首先来看Activity中的实现:
/**
* 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);
}
大概意思就是,如果Window的superDispatchTouchEvent返回true,表示Activity将该事件传递给Window来处理,如果返回false,则Activity自行处理该事件,Activity的onTouchEvent将会被调用,
/**
* Called when a touch screen event was not handled by any of the views * under it. This is most useful to process touch events that happen * outside of your window bounds, where there is no view to receive it. * * @param event The touch screen event being processed.
* * @return Return true if you have consumed the event, false if you haven't.
* The default implementation always returns false. */ public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
下面再来看看Window的superDispatchTouchEvent返回true的情况,由于Window的实现类为PhoneWindow,
@Override public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
有此可知调用了mDecor,mDecor就是DecorView,是根View,继承至FrameLayout
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
关于点击事件如何在View中进行分发,点击事件到达顶级view(一般是一个ViewGroup),会调用ViewGroup的dispatchTouchEvent方法,当ACTION_DOWN事件发生后,如果ViewGroup选择拦截该事件,那么接下来的ACTION_MOVE和ACTION_UP等这一事件序列的所有事件都不能被传递给ViewGroup的子View,都交给了ViewGroup进行处理,所以说,ViewGroup不能拦截ACTION_DOWN事件,否则子view将接收不到任何事件。ViewGroup不拦截ACTION_DOWN事件,则交给子View进行出来,接着会调用子view的dispatchTouchEvent事件,如果子view的dispatchTouchEvent返回true表示消费该事件,否则表示不消费该事件(会将该事件又返回给父ViewGroup进行处理)。
[图片上传失败...(image-e9c0ad-1624893293481)]
如果事件交由ViewGroup进行处理,执行ViewGroup的超类View的dispatchTouchEvent方法,这时如果ViewGroup的mOnTouchListener被设置了,就会执行OnTouchListener的onTouch方法,如果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;
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;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
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();
}
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:
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;
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);
}
// 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;
}
return true;
}
return false;
}
onTouchEvent会判断该ViewGroup是否是可点击的,也就是是否设置了mOnClickListener,如果设置了表示消费该事件,onClick将会被调用。否则也就表示不消费该事件,该事件还是会被抛给当前的父View进行处理。当ViewGroup不拦截此事件,会交给子View进行处理,然后再一次执行dispatchTouchEvent方法。到此为止,事件已经从顶级View传递给了下一层的View,接下来的传递过程和顶级View是一致的,如此循环,完成整个事件的分发。
所以我们接着看ViewGroup的dispatchTouchEvent,
[图片上传失败...(image-72149b-1624893293479)]
当发生ACTION_DOWN事件时,cancelAndClearTouchTargets会将mFirstTouchTarget重置为null,resetTouchState会将mGroupFlags置为0。
从上面代码我们可以看出,ViewGroup在如下两种情况下会判断是否要拦截当前事件:事件类型为ACTION_DOWN或者mFirstTouchTarget != null,ACTION_DOWM 好理解,那么mFirstTouchTarget是什么呢,由后面的代码可知,mFirstTouchTarget在ViewGroup将事件传递给子view时,mFirstTouchTarget会被赋值并被指向子元素。
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;
}
/**
* Adds a touch target for specified child to the beginning of the list. * Assumes the target child is not already present. */ private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
也就是当ViewGroup不拦截事件并将事件交由子元素处理时,mFirstTouchTarget != null。反过来,一旦ViewGroup需要拦截此事件,mFirstTouchTarget != null就不成立了。那么当ACTION_UP和ACTION_MOVE事件到来时,actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null就不成立了,将导致ViewGroup的onInterceptTouchEvent不会被调用,intercepted为true,并且同一事件序列的其他事件也默认交给ViewGroup处理。
当然,这里有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceptTouchEvent方法设置的,
/**
* Called when a child does not want this parent and its ancestors to * intercept touch events with * {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}.
* * This parent should pass this call onto its parents. This parent must obey
* this request for the duration of the touch (that is, only clear the flag * after this parent has received an up or a cancel.
** @param disallowIntercept True if the child does not want the parent to
* intercept touch events. */ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept);
一般用于子View中。FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的点击事件。为什么说出了ACTION_DOWN以外其他点击事件,是因为发生ACTION_DOWN事件时,会重置mGroupFlags标记位,还会置mFirstTouchTarget为null,将导致子View设置的标记位失效。因此当发生ACTION_DOWN事件时,ViewGroup总是询问自己是否要拦截事件,从上面代码就可以看出。当ViewGroup决定拦截事件时,后续这一事件序列里的所有事件都默认交给ViewGroup自己处理并且不再调用onInterceptTouchEvent方法。设置mGroupFlags为FLAG_DISALLOW_INTERCEPT的作用就是让ViewGroup不再拦截事件,前提时ACTION_DOWN事件没有被ViewGroup拦截。