Android(安卓)事件分发
概念
在移动设备上,我们去做一些操作,无论是 Android 还是 IOS 其实在系统中是根据事件驱动的,用户通过屏幕与手机交互的时候,每一次的点击,长按,移动等都是一个事件。
而事件分发机制呢?他其实是因为 Android 每一个页面都是基于 Activity 进行实现的,一个 Activity 里面有若干个 View 以及若干个 ViewGroup 组成的,而事件分发机制就是某一个事件从屏幕传递给各个 View,由这个 View 来消费这个事件或者忽略这个事件,交与其他 View 进行消费的这个过程的控制。
事件分发的对象是什么呢?系统会把整个事件封装为 MotionEvent 对象,事件分发的过程就是 MotionEvent 对象分发的过程
事件的类型
结合我们的人为操作过程,事件的类型有下面四种:
- 按下(ACTION_DOWN)
- 移动(ACTION_MOVE)
- 抬起(ACTION_UP)
- 取消(ACTION_CANCEL)
所以说一个完整的事件序列是从手指按下屏幕开始,到手指离开屏幕为止所产生的一系列事件。也就是说一个完整的事件序列是以一个 ACTION_DOWN 事件开始,到一个 ACTION_UP 事件为止,中间有若干个 ACTION_MOVE 事件(当然可以没有)。
在同一个事件序列中,如果子 View / ViewGroup 没有消费该事件,那同一事件序列的后续事件就不会传递到该子 View / ViewGroup 中去。
事件分发
那 Android 中是怎样传递事件的呢?
接触屏幕 Activity Window DecorView ViewGroup View其实主要的对象就是 Activity,ViewGroup 以及 View。事件的分发就是对这一系列的传递的操作。接下来我们就围绕这三种主要的对象的事件分发来进行理解。
Activity
流程图
下面就是 Activity 事件分发的流程图:
源码分析
我们从流程图中可以知道当事件开始触发的时候会调用 dispatchTouchEvent
方法,那我们来看下对应的源码:
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev);}
从源码中我们可以知道当事件的类型是 DOWN 的时候,会执行 onUserInteraction
方法
public void onUserInteraction() {}
然后进入这个方法,我们可以发现在源码中该方法为空方法。所以说当我们需要监听按下手势的时候,重写 onUserInteraction
方法就可以达到监听的效果。
然后接着往下面看,我们就会发现会调用 Window 的 superDispatchTouchEvent
方法。假如消费了该事件的话,就会返回 true ,代表事件已被消费,否则调用 onTouchEvent
方法消费该事件。
public abstract boolean superDispatchTouchEvent(MotionEvent event);
观看源码后我们发现其实 superDispatchTouchEvent
方法是一个抽象方法。我们看 Window 类的注释会发现
* <p>The only existing implementation of this abstract class is* android.view.PhoneWindow, which you should instantiate when needing a* Window.
其实他有唯一的实现,就是 PhoneWindow 类,然后我们看下对应的实现类的方法:
@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event);}
其实可以发现调用的是 DecorView 的 superDispatchTouchEvent
方法,而 DecorView 呢?其实就是 Activity 顶层的View,也我们 setContentView 方法传递进来的 layout 就是添加到了这个 View 上面。
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event);}
然后我们就发现他其实调用了他的父类的 dispatchTouchEvent
方法,也就是 ViewGroup 的 dispatchTouchEvent
方法,那这个方法就一起在后面的 ViewGroup 里面记录。
接下来就是说假如 getWindow().superDispatchTouchEvent(ev)
返回了 true ,那就什么该事件已经被消费了,直接返回就行,如果返回的是 false ,就说明当前任何视图都没有处理这个事件,那我们就是要调用 Activity 的 onTouchEvent
方法去消费该事件,并且直接返回 onTouchEvent
方法的结果。
public boolean onTouchEvent(MotionEvent event) { if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; } return false;}
如果 mWindow.shouldCloseOnTouch(this, event)
返回结果为 true ,就将该 Activity finish掉,并且返回true,否则为 false。
/** @hide */public boolean shouldCloseOnTouch(Context context, MotionEvent event) { final boolean isOutside = event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event) || event.getAction() == MotionEvent.ACTION_OUTSIDE; if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) { return true; } return false;}
mCloseOnTouchOutside 只有当 Activity 是以 Dialog 方式进行实现的时候才会为 true 否则为 false
然后怎么确保有 View 呢?其实 peekDecorView
方法就是用来获取当前 Activity 的 DecorView 的
最后怎么保证是点击在 View 外部的呢,其实就是靠 isOutside 变量。所以我们就可以理解为什么之前会调用 finish
方法了。所以说只有在当 Activity 是以 Dialog 方式进行实现的时候,并且点击了 View 外部的空白才会将该 Activity 关闭,否则不做任何处理直接消费事件。
ViewGroup
流程图
上面说到了之后就会进入 ViewGroup 的 dispatchTouchEvent
方法,这个方法就是标志着事件已经到了 ViewGroup 这一层。然后呢?就是 onInterceptTouchEvent
方法,这个方法的意义就是是否拦截事件,假如返回结果为 true 的话,则代码该事件,当前的 ViewGroup 会拦截该事件,事件就不会再向下传递了。最后就是 onTouchEvent
方法了。这个方法在 ViewGroup 中没有实现,而是在 View 中进行实现的。这个方法就是用来当我们把事件拦截了以后,自己来处理这个事件重写的。
下面就是 ViewGroup 的事件分发流程图:
源码分析
我们首先就来看一下 ViewGroup 的 dispatchTouchEvent
方法,源码的行数较多,就不贴上来了,其实这个方法主要就是做了三件事:
- 判断是否需要拦截事件
- 在当前的 ViewGroup 中找到用户真正点击的 View
- 分发事件到 View 上
根据流程图我们可以发现从 onFilterTouchEventForSecurity
方法开始进入事件分发的过程
onFilterTouchEventForSecurity
方法做的就是一些安全策略的操作,主要的用处就是去判断这个 View 是不是可以被触摸,假如这个视图被其他视图遮挡,那就不会去处理这个事件。
public boolean onFilterTouchEventForSecurity(MotionEvent event) { //noinspection RedundantIfStatement if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0 && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) { // Window is obscured, drop this touch. return false; } return true;}
如果这个方法返回了 false,那就说明安全策略不通过,所以直接返回 false,否则的话再进行后面的事件分发。
然后我们就会对这个事件的类型进行判断,假如这个事件的类型是一个按下的操作的时候,就回去做一些初始化的操作,因为按下是一个事件系列的开始。
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();}
cancelAndClearTouchTargets
方法是用于取消和清除所有的触摸目标,然后通过 resetTouchState
方法来重置触摸状态。
然后就要开始检测当前的事件是否是需要拦截的,就是靠 intercepted 这个变量去记录的。
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;}
如果当前事件为按下事件或者是已经有处理改事件的子 View 的时候就要进入后面的判断,否则就为 true,表示的是事件被拦截,事件就不会再向下传递。disallowIntercept 变量的意义就是判断当前事件是否可以拦截,如果为 true 的话,就代表当前事件在这个 ViewGroup 是不允许被拦截的,如果为 false 代表这个事件是可以被拦截的,然后就要通过 onInterceptTouchEvent
方法来判断是否对事件拦截。
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;}
这几个判断条件都是什么呢?
- 判断当前事件是不是来自鼠标(因为一般我们使用都不会使用鼠标进行操作,所有一般来说,这个返回为 false)
- 当前事件是不是按下事件
- 判断当前我们是否按下鼠标左键(如果按下返回 true,否则返回 false)
- 判断当前触摸位置是不是在一个滚动条的上面(如果是的话返回为 true,否则为 false)
只有这四个条件都满足的话,我们才会去拦截这个事件。所以说一般情况下 onInterceptTouchEvent
方法都是返回 false,不去拦截该事件的。
接下来就是判断改事件是不是一个取消事件:
final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
然后再去判断该事件是不是作用与多个视图:
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
最后如果即通过了安全判断也不是取消事件以后就是 开始进入事件分发的逻辑。
if (actionMasked == MotionEvent.ACTION_DOWN|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE)
当事件的类型是按下或者是移动的时候进入事件的分发,首先系统就会清除之前触摸点的信息,然后判断当前触摸点是否大于 0,之后就去获取当前触摸点的坐标。并且获取到可以接受到该触摸事件的子 View 的集合 preorderedList,以及判断是否对自定义 View 绘制顺序有要求。
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();
然后就开始对这个子 View 列表进行遍历。然后通过索引获取到每一个子 View
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
getAndVerifyPreorderedIndex
方法里面就利用到了我们之前的 customOrder 变量,如果当前的 View 的绘制是有自定义顺序的话就要通过 getChildDrawingOrder
方法去获取,这个方法就是在我们自定义绘制顺序的时候需要重写的方法,否则索引就是列表的下标。
private int getAndVerifyPreorderedIndex(int childrenCount, int i, boolean customOrder) { final int childIndex; if (customOrder) { final int childIndex1 = getChildDrawingOrder(childrenCount, i); if (childIndex1 >= childrenCount) { throw new IndexOutOfBoundsException("getChildDrawingOrder() " + "returned invalid index " + childIndex1 + " (child count is " + childrenCount + ")"); } childIndex = childIndex1; } else { childIndex = i; } return childIndex;}
然后就是通过 getAndVerifyPreorderedView
方法去获取对应的 View。
private static View getAndVerifyPreorderedView(ArrayList<View> preorderedList, View[] children, int childIndex) { final View child; if (preorderedList != null) { child = preorderedList.get(childIndex); if (child == null) { throw new RuntimeException("Invalid preorderedList contained null child at index " + childIndex); } } else { child = children[childIndex]; } return child;}
然后就是判断这个 View 能否接受到触摸事件以及当前的触摸事件是不是在这个 View 范围之内。
if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue;}
如果两个方法都返回了 true,就说明这个 View 就可以处理该事件。然后就要获取当前 View 的触摸对象。
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);
如果该 View 已经有对应的触摸对象的话直接退出循环即可,否则的话就表示该 View 还没有对应的触摸事件,然后就要去判断,该 View 有没有设置不接收触摸事件的标志位,如果有的话就清除这个标志。
然后就是最主要的一个方法了,dispatchTransformedTouchEvent
方法里面讲述了一个事件是如何从一个 ViewGroup 传递到一个具体的 View 中是如何过度的。
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;}
首先我们会判断该事件是否为一个取消事件,如果是取消事件的话,就要去判断是否有 View 处理,如果 child 为 null 则直接用 ViewGroup 父类的 dispatchTouchEvent
方法处理,否则调用 View 的方法,最后直接返回结果。
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;}
如果不是取消事件的话,就会去获取一个新的指针位,如果指针位为 0 的话,直接返回 false
// Calculate the number of pointers to deliver.final int oldPointerIdBits = event.getPointerIdBits();final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;// If for some reason we ended up in an inconsistent state where it looks like we// might produce a motion event with no pointers in it, then drop the event.if (newPointerIdBits == 0) { return false;}
然后呢就要去判断原有的指针位和新的指针位是不是一样的,如果是一样的就要去判断子视图是否存在,如果没有的话还是调用父类的 dispatchTouchEvent
方法处理,否则就要去计算子 View 的偏移量,然后调用子 View 的 dispatchTouchEvent
方法处理。
// If the number of pointers is the same and we don't need to perform any fancy// irreversible transformations, then we can reuse the motion event for this// dispatch as long as we are careful to revert any changes we make.// Otherwise we need to make a copy.final MotionEvent transformedEvent;if (newPointerIdBits == oldPointerIdBits) { if (child == null || child.hasIdentityMatrix()) { if (child == null) { handled = super.dispatchTouchEvent(event); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; event.offsetLocation(offsetX, offsetY); handled = child.dispatchTouchEvent(event); event.offsetLocation(-offsetX, -offsetY); } return handled; } transformedEvent = MotionEvent.obtain(event);} else { transformedEvent = event.split(newPointerIdBits);}
如果前面的两个条件都不满足,没有返回值的话,就要创建出一个 MotionEvent 类,然后再去判断子 View 是否为空,如果为空的话还是调用父类的 dispatchTouchEvent
方法处理,否则就要去计算子 View 的偏移量,然后调用子 View 的 dispatchTouchEvent
方法处理。最后释放相关资源。
// 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);}// Done.transformedEvent.recycle();
然后如果 dispatchTransformedTouchEvent
方法返回了 true 的话,就代表了已经传递到了子 View 的 dispatchTouchEvent
方法了,也就代表了该事件已经被消费了,所以就可以直接结束。
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;}
这样我们就解决了当前事件不为取消事件以及子 View 允许事件传递的情况。然后就要看通用的情况了。
// 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);}
在这个时候 mFirstTouchTarget 还是为 null 的话,就说明没有子 View 去执行这个事件,就要通过 dispatchTransformedTouchEvent
方法去消费事件获取返回值,我们就可以发现他的 View 的传值为 null,在之前讲的 dispatchTransformedTouchEvent
方法里面,就会交与他的父类去执行 dispatchTouchEvent
方法。
如果不为空的话就会遍历整个链表,假如在之前已经处理了的话,他就会直接返回 true,否则的话就会去重新分发事件。
else { // Dispatch to touch targets, excluding the new touch target if we already // dispatched to it. Cancel touch targets if necessary. TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; }}
View
流程图
最后就到了最后一步 View 了。Android 中事件在 View 里面会怎么进行处理呢?首先和前面是一样的,就是 dispatchTouchEvent
方法,这个方法就是标志着事件已经到了 View 这一层。然后呢?就是 onTouchEvent
方法了。这个方法里面就是 Android 系统处理触摸事件的相关逻辑。
下面就是 View 的事件分发流程图:
源码分析
首先判断该 View 是否有可相应焦点,如果没有的话直接返回 false。
// If the event should be handled by accessibility focus first.if (event.isTargetAccessibilityFocus()) { // We don't have focus or no virtual descendant has it, do not handle the event. if (!isAccessibilityFocusedViewOrHost()) { return false; } // We have focus and got the event, then use normal event dispatch. event.setTargetAccessibilityFocus(false);}
然后对事件的类型进行判断,如果当前的事件为按下事件的话,如果存在视图滚动效果的话就要立刻停止滚动
final int actionMasked = event.getActionMasked();if (actionMasked == MotionEvent.ACTION_DOWN) { // Defensive cleanup for new gesture stopNestedScroll();}
之后就开始进入真正的事件分发的过程了,首先和 ViewGroup 是一样的,通过 onFilterTouchEventForSecurity
方法来进行事件的安全判断。这块的逻辑和 View Group 中是一样的,如果不符合的话就返回 false ,如果符合的话就要接下来进行事件的处理了。
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;}
首先就是判断当前的操控方式是不是为鼠标操作,如果为鼠标操作的话,返回值就可以直接为 ture 了,表示消费了该事件。然后就要检查是否有触摸事件的监听,然后就要调用 listener 的 onTouch
方法,如果结果返回的是 true,则我们的返回结果也为 true。如果还是不去消费该事件的话,就要调用 View 的 onTouchEvent
方法,根据返回的结果来进行返回。
然后就要进入 View 的 onTouchEvent
方法里面来看了
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;}
首先获取到点击的位置坐标,View 的标志位,事件类型以及可点击状态,然后去判断该 View 是否处于一个禁用状态的话,返回结果就为可点击的状态,这样就可以说明,当 View 是处于一个禁用状态的话,如果是可点击的,也会去消费这个事件,但是因为是直接返回的,所以说不会去多事件有所响应。
然后就会去判断有没有设置触摸的代理。
if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; }}
如果有的话就要通过代理的 onTouchEvent
方法去获取结果,如果能够消费事件的话就直接返回 true,否则继续后面的事件处理。
最后就是去判断是否为一个可点击的状态或者在标记位上 TOOLTIP 位为 1 的话,就会开始根据事件类型的不同做不同的处理,然后返回 true,表示事件已经被消费,否则返回结构为 false。
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: ... case MotionEvent.ACTION_DOWN: ... case MotionEvent.ACTION_CANCEL: ... case MotionEvent.ACTION_MOVE: ... } return true; } return false; }
更多相关文章
- android 触摸事件、点击事件的区别
- j2me与android的区别
- 关于android连续点击出现多个Activity界面的解决方法
- Android位图操作
- Android(安卓)Hook 机制之简单实战
- [置顶] Android类加载之PathClassLoader和DexClassLoader
- Android(安卓)Service生命周期及用法!
- 浅谈Java中Collections.sort对List排序的两种方法
- Python list sort方法的具体使用