作者:积木zz
这次说下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 {//isViewisConsume = 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();}
}
当某一层viewGroup的onInterceptTouchEvent被调用,则代表当前层级要消费事件。如果它的onTouchListener被设置了的话,则onTouch会被调用,如果onTouch的返回值返回true,则onTouchEvent不会被调用。如果返回false或者没有设置onTouchListener,则会继续调用onTouchEvent。而onClick方法则是设置了onClickListener则会被正常调用。
这里用一张流程图总结下:
源码分析
一个触摸事件,首先是传到Activity层级,然后传到根view,通过一层层的viewgroup最终到底最里面一层的view,我们来一层层解析
Activity(dispatchTouchEvent)
直接上代码
//Activity.javapublic 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.javaprivate DecorView mDecor;@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) {return mDecor.superDispatchTouchEvent(event);}//DecorView.javapublic boolean superDispatchTouchEvent(MotionEvent event) {return super.dispatchTouchEvent(event);}
这里可以看到,依次经过了PhoneWindow到达了DecorView,DecorView是activity的根view,也是setcontentView所设置的view的父view,它是继承自FrameLayout。所以这里super.dispatchTouchEvent(event)方法,其实就是走到了viewgroup的dispatchTouchEvent
方法。
ViewGroup(dispatchTouchEvent)
@Overridepublic 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 indexfor (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 gesturestopNestedScroll();}if (onFilterTouchEventForSecurity(event)) {if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {result = true;}//noinspection SimplifiableIfStatementListenerInfo 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 checkremoveLongPressCallback();// Only perform take click actions if we were in the pressed stateif (!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 @Overridepublic 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 @Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {return false;} else {return true;}}//子view.java@Overridepublic 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方法中来,那么这时候要保证方法中一定是要拦截的。
Android 学习笔录
Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap