内容简介:基于 Android 28 源码分析所谓点击事件的事件分发,其实就是对首先我们需要介绍在点击事件分发过程中很重要的三个方法:
基于 Android 28 源码分析
所谓点击事件的事件分发,其实就是对 MotionEvent
事件的分发过程,即当一个 MotionEvent
产生了以后,系统需要把这个事件传递给一个具体的 View
,而这个传递的过程就是分发过程。
三个重要方法
首先我们需要介绍在点击事件分发过程中很重要的三个方法:
dispatchTouchEvent
用来进行事件的分发。如果事件能够传递给当前 View
,那么此方法一定会被调用,返回结果受当前 View
的 onTouchEvent
和 下级 View
的 dispatchTouchEvent
方法的影响,表示是否消耗当前事件。
onInterceptTouchEvent
在 dispatchTouchEvent
内部调用 ,用来判断是否拦截某个事件,如果当前 View
拦截了某个事件,那么在同一个事件序列当中, 此方法不会被再次调用
,返回结果表示是否拦截当前事件。
onTouchEvent
在 dispatchTouchEvent
内部调用 ,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一事件序列中,当前 View
无法再次接受到事件。
其实它们的关系可以用如下伪代码表示:
public boolean dispatchTouchEvent(MotionEvent ev) { if (onInterceptTouchEvent(ev)) { return onTouchEvent(ev); } return child.dispatchTouchEvent(ev); } 复制代码
对于一个根 ViewGroup
来说,点击事件产生后,首先会传递给它的 dispatchTouchEvent
方法,如果这个 ViewGroup
的 onInterceptTouchEvent
返回为 true
, 就表示它要拦截当前事件,接着事件就会交给该 ViewGroup
的 onTouchEvent
方法去处理。如果 onInterceptTouchEvent
返回为 false
,就表示它不拦截当前事件,这是当前事件就会传递给它的子元素,接着由子元素的 dispatchTouchEvent
来处理点击事件,如此反复直到事件被最终处理。
事件分发的源码分析
当一个点击事件发生后,它的传递过程遵循如下顺序: Activity
-> Window
-> View
, 即事件总是先传递给 Activity
, Activity
再传递给 Window
, 最后 Window
再传递给顶级 View
。 顶级 View
接受到事件后,就会按照事件分发机制去分发事件。
Activity 对点击事件的分发过程
// Activity.java public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); } 复制代码
分析上面的代码,点击事件用 MotionEvent
来表示,当一个点击操作发生时,由当前 Activity
的 dispatchTouchEvent
来进行事件分发,具体的工作由 Activity
内部的 Window
来完成的。如果返回 true
,整个事件循环就结束了,返回 false
意味着事件没人处理,所有 View
的 onTouchEvent
都返回了 false
, 那么 Activity
的 onTouchEvent
就会被调用。
Window 对点击事件的分发过程
接下来看 Window
是如何将事件传递给 ViewGroup
的。看源码会发现, Window
是个抽象类,而 Window
的 superDispatchTouchEvent
方法也是个抽象方法,因此必须找到 Window
的实现类才行。通过注释可以发现 Window
的唯一实现类是 PhoneWindow
,因此接下来看一下 PhoneWindow
是如何处理点击事件的。
// PhoneWindow.java @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); } 复制代码
PhoneWindow
将事件直接传递给了 DecorView
,我们知道通过 ((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)
这种方式就可以获取到 Activity
中所设置的 View
, 这个 mDecor
显然就是 getWindow().getDecorView()
返回的 View
,而我们通过 setContentView
设置的 View
是它的一个子 View
。由于 DecorView
继承子 FrameLayout
且是 父 View
,所以最终事件会传递给 View
。从这里开始,事件已经传递到顶级 View
了,即在 Activity
中通过 setContentView
所设置的 View
,
顶级 View
一般来说都是 ViewGroup
顶级 View
对点击事件的分发过程
首先看 ViewGroup
对点击事件的分发过程,其主要实现在 ViewGroup
的 dispatchTouchEvent
方法中,这个方法代码量很多,分段进行说明。
// ViewGroup.java @Override public boolean dispatchTouchEvent(MotionEvent ev) { ... // Check for interception. final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 判断是否要拦截当前事件 // 根据 FLAG_DISALLOW_INTERCEPT 标记位来判断是否要进行拦截 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; } ... } 复制代码
上面代码可以看出,当事件类型为 ACTION_DOWN
或者 mFirstTouchTarget != null
这两种情况下来判断是否要拦截当前事件。 ACTION_DOWN
事件容易理解,那么 mFirstTouchTarget != null
是什么意思呢? 这个从后面的代码逻辑可以看出来,
当事件由 ViewGroup
的子元素成功处理时, mFristTouchTarget
就会被赋值指向子元素
,那也就是说当事件是被当前 ViewGroup
拦截来处理而不交给子元素处理时, mFristTouchTarget == null
,那么当 ACTION_MOVE
和 ACTION_UP
事件到来时,由于 (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)
这个条件为 false
,将导致 ViewGroup
的 onInterceptTouchEvent
不会再被调用,
并且同一序列中的其他事件都会默认交给该 ViewGroup
来处理。
这里还有一种特殊情况,那就是 FLAG_DISALLOW_INTERCEPT
标记位,这个标记位是通过 requestDisallowInterceptTouchEvent
方法来设置的,一般用于子 View
中。 FLAG_DISALLOW_INTERCEPT
一旦设置后, ViewGroup
将无法拦截除了 ACTION_DOWN
以外的其他点击事件。为什么是除了 ACTION_DOWN
以外的事件呢? 这是因为 ViewGroup
在分发事件时,如果是 ACTION_DOWN
就会重置 FLAG_DISALLOW_INTERCEPT
这个标记位,将导致子 View
中设置的这个标记位无效。
// ViewGroup.java @Override public boolean dispatchTouchEvent(MotionEvent ev) { ... // 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(); // 重置 FLAG_DISALLOW_INTERCEPT 标记位 } // Check for interception. final boolean intercepted; ... } 复制代码
上面的代码中, ViewGroup
会在 ACTION_DOWN
事件到来时做重置状态的操作,而在 resetTouchState
方法中会对 FLAG_DISALLOW_INTERCEPT
进行重置,因此子 View
调用 requestDisallowInterceptTouchEvent
方法并不会影响 ViewGroup
对 ACTION_DOWN
事件的处理。
通过上面可以得出结论:当 ViewGroup
决定拦截事件后,那么后续的点击事件将会默认交给它处理并且不再调用它的 onInterceptTouchEvent
方法。
所以 onIntecepterTouchEvent
不是每次事件都会被调用的,如果我们想提前处理所有的点击事件,要选择 dispatchTouchEvent
方法,只有这个方法能保证每次都会被调用,当然前提是事件能够传递到当前的 ViewGroup
中。
接着来看 ViewGroup
不拦截事件的时候,事件会向下分发交由它的子 View
进行处理
// ViewGroup.java @Override public boolean dispatchTouchEvent(MotionEvent ev) { ... if (!canceled && !intercepted) { // If the event is targeting accessibility focus we give it to the // view that has accessibility focus and if it does not handle it // we clear the flag and dispatch the event to all children as usual. // We are looking up the accessibility focused host to avoid keeping // state since these events are very rare. View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null; if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { final int actionIndex = ev.getActionIndex(); // always 0 for down final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; // Clean up earlier touch targets for this pointer id in case they // have become out of sync. removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); // Find a child that can receive the event. // Scan children from front to back. final ArrayList<View> preorderedList = buildTouchDispatchChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { // 遍历 ViewGroup 的所有子元素 判断子元素是否能够接受到点击事件 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)) { // 实际调用的就是子元素的 dispatchTouchEvent 方法 // 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(); // mFirstTouchTarget 被赋值并且跳出 for 循环 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(); } if (newTouchTarget == null && mFirstTouchTarget != null) { // Did not find a child to receive the event. // Assign the pointer to the least recently added target. newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } } ... } 复制代码
上面代码的逻辑是,首先遍历 ViewGroup
的所有子元素,然后判断子元素是否能够接受到点击事件。 是否能够接受点击事件主要由两点来衡量:
- 子元素是否在播动画
- 点击事件的坐标是否落在子元素的区域内
如果子元素满足这两个条件,那么事件就会传递给它来处理。传递由 dispatchTransformedTouchEvent
方法来完成
// ViewGroup.java private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; ... // Perform any necessary transformations and dispatch. if (child == null) { handled = super.dispatchTouchEvent(transformedEvent); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; transformedEvent.offsetLocation(offsetX, offsetY); if (! child.hasIdentityMatrix()) { transformedEvent.transform(child.getInverseMatrix()); } handled = child.dispatchTouchEvent(transformedEvent); } ... return handled } 复制代码
可以发现如果 child
传递的不是 null
,它会直接调用子元素的 dispatchTouchEvent
方法,这样事件就交由子元素处理了,从而完成了一轮事件的分发。
如果子元素的 dispatchTouchEvent
返回 true
,那么上文提到的 mFirstTouchTarget
就会被赋值同时跳出 for
循环, mFirstTouchTarget
真正的赋值过程是由 addTouchTarget
函数完成的。
// ViewGroup.java private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; } 复制代码
通过代码可以看出, mFirstTouchTarget
是一种单链表数据结构。 mFirstTouchTarget
是否被赋值,将直接影响到 ViewGroup
对事件的拦截策略,如果 mFirstTouchTarget
为 null
,那么 ViewGroup
就默认拦截接下来同一序列中所有的点击事件,这点上文已经分析过。
如果遍历所有的子元素后事件都没有被合适的处理,这包含两种情况:
-
ViewGroup
没有子元素 -
子元素处理了点击事件,但是在
dispatchTouchEvent
中返回了false
,这一般是因为子元素在onTouchEvent
中返回了false
在以上两种情况下, ViewGroup
会自己处理点击事件
// ViewGroup.java @Override public boolean dispatchTouchEvent(MotionEvent ev) { ... // 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
为 null
,从签名的分析可以知道,它会调用 super.dispatchTouchEvent(event)
,很显然,这里就转到了 View
的 dispatchTouchEvent
方法中,即点击事件开始交由 View
来处理。
View 对点击事件的处理过程
// View.java public boolean dispatchTouchEvent(MotionEvent event) { ... 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; } } ... return result; } 复制代码
View
对点击事件的处理过程就比较简单了,因为 View
(不包含 ViewGroup
)是一个单独的元素,它没有子元素因此无法向下传递事件,所以只能自己处理事件。上面的源码可以看出 View
首先会判断有没有设置 onTouchListener
,如果 onTouchListener
中的 onTouchListener
方法返回 true
,那么 onTouchEvent
就不会被调用,可见
onTouchListener
的优先级高于 onTouchEvent
,这样做的好处是方便在外界处理点击事件。
// View.java public boolean onTouchEvent(MotionEvent event) { ... final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; if ((viewFlags & ENABLED_MASK) == DISABLED) { // 不可用状态下的 View 照样会消耗点击事件 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; } ... // 只要 View 的 CLICKABLE, LONG_CLICKABLE, CONTEXT_CLICKABLE 和 TOOLTIP 有一个为 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)) { 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; ... } return true; } return false; } 复制代码
上面代码中,只要 View
的 CLICKABLE
, LONG_CLICKABLE
, CONTEXT_CLICKABLE
和 TOOLTIP
有一个为 true
就会消耗这个事件。 即 onTouchEvent
方法返回 true
,不管它是不是 DISABLE
状态。然后就是当 ACTION_UP
事件发生时,会触发 performClickInternal
方法,最终调用 performClick
方法。
// View.java 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; } 复制代码
上述代码可知,如果 View
设置了 OnClickListener
那么 performClick
方法内部就会调用它的 onClick
方法
以上所述就是小编给大家介绍的《基于源码分析 Android View 事件分发机制》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Android事件分发源码归纳
- View的事件分发(二)源码分析
- View的事件分发(三)源码分析
- Android事件分发机制[-View-] 源码级
- Laravel Queue——消息队列任务与分发源码剖析
- Android读书笔记--从源码角度剖析View事件分发机制
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。