原文链接:https://juejin.im/post/5eb3e0…
这次说下Android中的事件散发机制
从开始点击屏幕开始,就会产生从Activity开始到decorview始终到最里层的view一连串事件传递。每一层view或者viewgroup都会首先调用它的dispatchTouchEvent办法,而后判断是否就在以后一层生产掉事件
view的事件散发
首先上一段伪代码,是在书上看到的,也是我感觉总结的最好的
public boolean dispatchTouchEvent(MotionEvent event) { boolean isConsume = false; if (isViewGroup) { if (onInterceptTouchEvent(event)) { isConsume = onTouchEvent(event); } else { isConsume = child.dispatchTouchEvent(event); } } else { //isView isConsume = onTouchEvent(event); } return isConsume; }
复制代码如果以后是viewgroup层级,就会判断 onInterceptTouchEvent 是否为true,如果为true,则代表事件要生产在这一层级,不再往下传递。接着便执行以后 viewgroup 的onTouchEvent办法。如果onInterceptTouchEvent为false,则代表事件持续传递到下一层级的 dispatchTouchEvent办法,接着一样的代码逻辑,始终到最外面一层的view。
ok,还没完哦,到最外面一层就会间接执行onTouchEvent办法,这时候,view有没有权力回绝生产事件呢? 按情理view作为最底层的,应该是没有发言权才对。然而呢,秉着偏心公正准则,view也是能够回绝的,能够在onTouchEvent办法返回false,示意他不想生产这个事件。那么这个事件又会怎么解决呢?见上面一段伪代码:
public void handleTouchEvent(MotionEvent event) { if (!onTouchEvent(event)) { getParent.onTouchEvent(event); } }
复制代码如果view的onTouchEvent办法返回false,那么它的父容器的onTouchEvent又会被调用,如果父容器的onTouchEvent又返回false,则又交给上一级。始终到最上层,也就是Activity的onTouchEvent被调用。
至此,生产流程结束
然而,对于onTouch,onTouchEvent和onClick又是怎么样的调用关系呢?
那就再来一段伪代码:
public void consumeEvent(MotionEvent event) { if (setOnTouchListener) { onTouch(); if (!onTouch()) { onTouchEvent(event); } } else { onTouchEvent(event); } if (setOnClickListener) { onClick(); } }
复制代码当某一层view的onInterceptTouchEvent被调用,则代表以后层级要生产事件。如果它的onTouchListener被设置了的话,则onTouch会被调用,如果onTouch的返回值返回true,则onTouchEvent不会被调用。如果返回false或者没有设置onTouchListener,则会持续调用onTouchEvent。而onClick办法则是设置了onClickListener则会被失常调用。
这里用一张流程图总结下:
<figcaption style=”margin: 10px 0px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; line-height: inherit; text-align: center; color: rgb(153, 153, 153); font-size: 0.7em;”>在这里插入图片形容</figcaption>
源码剖析
一个触摸事件,首先是传到Activity层级,而后传到根view,通过一层层的viewgroup最终到底最外面一层的view,咱们来一层层解析
Activity(dispatchTouchEvent)
间接上代码
//Activity.java public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); } public void onUserInteraction() { }
复制代码这里能够看到,onUserInteraction办法是空的,次要是调用了getWindow().superDispatchTouchEvent(ev)办法,返回true,就代表事件生产了。返回false,就代表上层没人解决,那就间接到了activity的onTouchEvent办法,这点跟之前的生产传递也是吻合的。
持续看看superDispatchTouchEvent办法,而后就走到了PhoneWindow的superDispatchTouchEvent办法,以及DecorView的superDispatchTouchEvent,看看代码:
//PhoneWindow.java private DecorView mDecor; @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); } //DecorView.java public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }
复制代码这里能够看到,顺次通过了PhoneWindow达到了DecorView,DecorView是activity的根view,也是setcontentView所设置的view的父view,它是继承自FrameLayout。所以这里super.dispatchTouchEvent(event)办法,其实就是走到了viewgroup的dispatchTouchEvent 办法。
ViewGroup(dispatchTouchEvent)
@Override public boolean dispatchTouchEvent(MotionEvent ev) { if (onFilterTouchEventForSecurity(ev)) { // Check for interception,示意是否拦挡的字段 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { //FLAG_DISALLOW_INTERCEPT标记是通过requestDisallowInterceptTouchEvent设置 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; } //mFirstTouchTarget赋值 while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { } else { if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } continue; } } } }
复制代码这里截取了局部要害的代码,首先是两个条件
- actionMasked == MotionEvent.ACTION_DOWN
- mFirstTouchTarget != null
如果满足了其中一个条件才会持续走上来,执行onInterceptTouchEvent办法等,否则就间接intercepted = true,示意拦挡。
第一个条件很显著,就是示意以后事件位按下事件(ACTION_DOWN)
第二个条件是个字段,依据上面的代码能够得悉,当前面有view生产掉事件的时候,这个mFirstTouchTarget字段就会赋值,否则就为空。
所以什么意思呢,当ACTION_DOWN事件时候,肯定会执行到前面代码。当其余事件来的时候,要看以后viewgroup是否生产了事件,如果以后viewgroup曾经生产了事件,没传到子view,那么mFirstTouchTarget字段就为空,所以就不会执行到前面的代码,就间接生产掉所有事件了。
这就合乎了之前的所说的一种机制:
某个view一旦开始拦挡,那么后续事件就全副就给它解决了,也不会执行onInterceptTouchEvent办法了
然而,两个条件满足了一个,就能执行到onInterceptTouchEvent了吗?不肯定,这里看到还有一个判断条件:disallowIntercept。这个字段是由requestDisallowInterceptTouchEvent办法设置的,前面咱们会讲到,次要用于滑动抵触,意思就是子view通知你不想让你拦挡,那么你就不拦挡了,间接返回false。
ok,持续看源码,之前的内容咱们理解到,如果viewgroup不拦挡事件,应该会传递给子view,那在哪里传的呢?持续看看dispatchTouchEvent的代码:
if (!canceled && !intercepted) { final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { 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 (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } 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); } if (preorderedList != null) preorderedList.clear(); } } }
复制代码这里能够看到,进行了一个子view的便当,其中,如果满足两个条件中的一个,就跳出。否则就执行dispatchTransformedTouchEvent办法。先看看这两个条件:
- !child.canReceivePointerEvents()
- !isTransformedTouchPointInView(x, y, child, null)
看名字是看不出啥了,间接看代码吧:
protected boolean canReceivePointerEvents() { return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null; } 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; }
复制代码哦,原来是这个意思。canReceivePointerEvents办法就代表view是不是能够承受点击事件,比方是不是在播放动画。而isTransformedTouchPointInView办法代表点击事件的坐标是不是在这个view的区域下面。
ok,如果条件都满足,就执行到dispatchTransformedTouchEvent办法了:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; // Canceling motions is a special case. We don't need to perform any transformations // or filtering. The important part is the action, not the contents. final int oldAction = event.getAction(); if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; } }
复制代码这个办法大家应该都猜到了,其实就是执行了child.dispatchTouchEvent(event)。也就是下一层view的dispatchTouchEvent办法呗,开始事件的层级传递。
View(dispatchTouchEvent)
到view 层级的时候,天然就执行的view的dispatchTouchEvent,上代码
public boolean dispatchTouchEvent(MotionEvent event) { boolean result = false; if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } final int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { // Defensive cleanup for new gesture stopNestedScroll(); } 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; } } if (!result && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } // Clean up after nested scrolls if this is the end of a gesture; // also cancel it if we tried an ACTION_DOWN but we didn't want the rest // of the gesture. if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && !result)) { stopNestedScroll(); } return result; }
复制代码这里能够看到,首先会判断li.mOnTouchListener != null,如果不为空,就会执行onTouch办法。
依据onTouch办法返回的后果,如果为false,result就为false,那么onTouchEvent才会执行。这个逻辑也是合乎咱们之前说的传递形式。
最初咱们再看看view的onTouchEvent都做了什么事:
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { 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 (!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(); } } } } mIgnoreNextUpEvent = false; break; } return true; }
复制代码从代码能够得悉,如果设置了CLICKABLE或者LONG_CLICKABLE,那么这个view就会生产事件,并且执行performClickInternal办法,而后执行到performClick办法。这个performClick办法大家应该都很相熟,就是触发点击的办法,其实外部就是执行了onClick办法。
private boolean performClickInternal() { notifyAutofillManagerOnClick(); return performClick(); } 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; } return result; }
复制代码至此,源代码也看的差不多了,外部其实有很多细节,这里也就不一一阐明了,大家有空能够去钻研下。
事件散发的利用(requestDisallowInterceptTouchEvent)
那既然学会了事件散发机制,咱们理论工作中会怎么利用呢?其实最常见的就是解决滑动抵触的问题。个别有两种解决办法:
- 一种是内部拦挡:从父view端解决,依据状况决定事件是否散发到子view
- 一种是外部拦挡:从子view端解决,依据状况决定是否阻止父view进行拦挡,其中的要害就是requestDisallowInterceptTouchEvent办法。
第一种办法,其实就是在onInterceptTouchEvnet办法外面进行判断返回true还是返回false。
第二种办法,就是用到了requestDisallowInterceptTouchEvent办法,这个办法的意思就是让父view不要去拦挡事件了,在dispatchTouchEvent办法外面就有这个标记位:FLAG_DISALLOW_INTERCEPT,如果disallowIntercept字段为true,就不会去执行onInterceptTouchEvent办法,而是返回false,不拦挡事件。
上代码:
//内部拦截法:父view.java @Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercepted = false; //父view拦挡条件 boolean parentCanIntercept; switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: intercepted = false; break; case MotionEvent.ACTION_MOVE: if (parentCanIntercept) { intercepted = true; } else { intercepted = false; } break; case MotionEvent.ACTION_UP: intercepted = false; break; } return intercepted; }
复制代码内部拦挡很简略,就是判断条件,而后决定是否进行拦挡。
//父view.java @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { return false; } else { return true; } } //子view.java @Override public boolean dispatchTouchEvent(MotionEvent event) { //父view拦挡条件 boolean parentCanIntercept; switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: if (parentCanIntercept) { getParent().requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: break; } return super.dispatchTouchEvent(event); }
复制代码感觉外部拦挡有点简单呀,还要重写父view的办法,这里剖析下,为什么要去这么写:
- 父view ACTION_DOWN的时候,不能拦挡,因为如果拦挡,那么后续事件也就跟子view无关了
- 父view 其余事件的时候,要返回true,示意拦挡。因为onInterceptTouchEvent办法的调用是被FLAG_DISALLOW_INTERCEPT标记位所管制,所以子view须要父view拦挡的时候,才会走到这个onInterceptTouchEvent办法中来,那么这时候要保障办法中肯定是要拦挡的。
至此,事件的散发机制也就说的差不多了。
文末
您的点赞珍藏就是对我最大的激励!
欢迎关注搞代码gaodaima网,分享Android干货,交换Android技术。
对文章有何见解,或者有何技术问题,欢送在评论区一起留言探讨!