本篇内容主要讲解“Android事件分发机制是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Android事件分发机制是什么”吧!
事件分发原因
Android 中页面上的 View 是以树型结构显示的,View 会重叠在一起,当我们点击的地方有多个 View 可以响应的时候,这个点击事件应该给谁,为了解决这个问题就需要一个事件分发机制
事件分发对象
Touch 事件,即将每一个 Touch 事件(MotionEvent)传递给 View,至于最终这个事件有没有处理看接收事件者的逻辑而定
当用户触摸屏幕的时候,就会产生 Touch 事件(Touch 事件被封装成 MotionEvent 对象),其主要分为如下几种
MotionEvent.ACTION_DOWN:使用手指点击屏幕这一瞬间,产生该事件,是所有事件的开始
MotionEvent.ACTION_MOVE:使用手指在屏幕滑动的时候产生该事件
MotionEvent.ACTION_CANCLE:非人为原因结束当前事件
MotionEvent.ACTION_UP:手指离开屏幕一瞬间产生该事件
一次完整的 Touch 事件,是从用户手指触摸屏幕(伴随着一次 ACTIONDOWN 事件)到用户手指离开屏幕(伴随着一次 ACTIONUP 事件)这一过程,整个过程如下
ACTIONDOWN(一次) --> ACTIONMOVE(N 次) --> ACTION_UP(一次)
事件分发方法
dispatchTouchEvent(MotionEvent ev) :从方法名也能看出它的作用是对事件进行分发;当一个事件由底层驱动检测到了之后,会进行上报,最终会交由 Activity 的该方法处理,来决定是自己消费还是继续传递下去
onInterceptTouchEvent(MotionEvent ev) :当一个事件分发到 ViewGroup 后,它可以决定是否对该事件进行拦截,该方法只有 ViewGroup 拥有
onTouchEvent(MotionEvent event) :这是事件分发流程的最后一个方法了,即是否消费该次事件
事件分发参与者
Activity:包含 ViewGroup 和 View
ViewGroup:包含 ViewGroup 和 View
View:并不包含其它 View,只有自己
事件分发流向一般是 Activity --> ViewGroup --> … --> View
注意:
子 View 可以通过 requestDisallowInterceptTouchEvent 方法干预父 View 的事件分发过程(ACTION_DOWN 事件除外),而这就是我们处理滑动冲突常用的关键方法
如果 View 设置了 onTouchListener,在重写的 onTouch 方法中返回 true,那么它的 onTouchEvent 方法不会被调用,因为在 View 的 dispatchTouchEvent 中 onTouch 优先于 onTouchEvent 执行;onClick 方法也不会被调用,因为 onClick 是在 onTouchEvent 中回调的
事件分发流程
当手指触摸屏幕后,底层 Input 驱动从/dev/input/路径下读写以 event[NUMBER]为名的硬件输入设备节点获取事件(可以通过 adb shell getevent 查看你的设备下的节点,Android 也是从这些节点获取这些原始数据再封装后提供给开发者使用;如果做游戏开发可能就直接获取这些原始数据自己处理了),经过一系列调用后传递到了 DecorView 的 dispatchTouchEvent 方法
在 DecorView 中,会通过 Window 的内部接口 Callback,将事件继续传递,因为 Activity 实现了该接口,故事件分发到 Activity;Activity 获取到事件后,在 dispatchTouchEvent 方法中先将事件分发到该 Activity 所在的 window,实际类型是 PhoneWindow,这个 window 又将事件交给它的顶级 view 即 DecorView 处理
DecorView 是 FrameLayout 的子类,即 ViewGroup 的子类,自己没有处理,只是继续将事件交由 ViewGroup 处理;就这样一个事件就从 Activity 转到了 ViewGroup
ViewGroup 在 dispatchTouchEvent 方法进行分发,如果自己的 onInterceptTouchEvent 方法拦截此次事件,就把事件交给自身的 onTouchEvent 方法处理;反之遍历自己的子 View,继续将事件分发下去,只要有一个子 View 消费了这个事件,那就停止遍历
事件会传递到子 View 的 dispatchTouchEvent 方法,如果给子 View 注册了 OnTouchListener,且返回 true,那事件分发就到此结束;反之就会继续将事件传递到子 View 的 onTouchEvent 方法
子 View 会在 ACTION_UP 事件中回调 View 的 onClick 监听,如果子 View 没有消费此次事件,就会按照分发流程反过来传递回去到 Activity;如果到了 Activity 还没人消费(包括 Activity 自己),那就会销毁这个事件
事件分发源码
以下源码基于 API24
对应上面的流程,当有 Touch 事件后,步骤如下
DecorView.dispatchTouchEvent
@Override public boolean dispatchTouchEvent(MotionEvent ev) { final Window.Callback cb = mWindow.getCallback(); return cb != null && !mWindow.isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev); }
此处的 cb 指的是 window 内部的 Callback 接口,Activity 实现了这个接口,接下来进入 Activity
Activity.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
这个方法就是 Activity 用来处理触摸屏事件,我们可以重写这个方法,并返回 true/false,这样在事件分发到 window 前就能进行拦截,Activity 内的 ViewGroup 或者 View 将收不到事件
一个触摸屏事件都是以 ACTION_DOWN 开始,那就肯定会进入 onUserInteraction()方法
public void onUserInteraction() { }
这是一个空方法,它的调用时机如下: 当一个按键事件,触摸屏事件或者 trackball 事件分发到 Activity 的时候,它就会被调用;如果你希望在 Activity 正在运行的时候了解用户和设备用某种方式交互,可以重写这个方法;不过需要注意的是这个方法只响应 touch-down 这种触摸手势,不会响应接下来的 touch-move 和 touch-up
与这个方法相对应的一个方法就是onUserLeaveHint,它同样也是一个空方法,它的调用时机如下:
当在用户操作的情况下 Activity 进入后台,这个方法会作为 Activity 生命周期的一部分被调用;比如,用户按下 home 键,当前 Activity 就会进入后台,它就会被调用,并且是在 onPause 之前调用;但是比如有电话打进来了导致 Activity 被动进入后台,这个方法就不会被调用
接下来进入第二个 if 语句
getWindow().superDispatchTouchEvent
通过 getWindow()获取到的是一个 Window 对象,但是它是在 Activity 的 attach 方法中进行实例化,实际类型是 PhoneWindow,也是在这里实现了 Callback 接口
final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config, String referrer, IVoiceInteractor voiceInteractor, Window window) { ...... mWindow = new PhoneWindow(this, window); mWindow.setCallback(this); ...... }
这里就转到 PhoneWindow,如下
PhoneWindow.superDispatchTouchEvent
//这是窗口的顶层视图 private DecorView mDecor @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
DecorView .superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }
DecorView 是 FrameLayout 的子类,FrameLayout 又是 ViewGroup 的子类,这里就会走到 ViewGroup
ViewGroup.dispatchTouchEvent
@Override public boolean dispatchTouchEvent(MotionEvent ev) { //用于调试目的的一致性验证程序 if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(ev, 1); } // If the event targets the accessibility focused view and this is it, start // normal event dispatch. Maybe a descendant is what will handle the click. if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) { ev.setTargetAccessibilityFocus(false); } //这个变量用于标记事件是否被消费 boolean handled = false; //根据应用安全策略过滤触摸事件 if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // 处理 initial down 发生后的初始化操作 if (actionMasked == MotionEvent.ACTION_DOWN) { // 新的 ACTION_DOWN 事件来了,需要取消并清除之前的 touch Targets //清空掉 mFirstTouchTarget cancelAndClearTouchTargets(ev); //重置触摸状态 resetTouchState(); } //标记是否拦截事件 final boolean intercepted; // 当 ACTION_DOWN 来了或者已经发生过 ACTION_DOWN,并且将 mFirstTouchTarget 赋值 就检测 ViewGroup 是否需要拦截事件. //只有发生过 ACTION_DOWN 事件,mFirstTouchTarget != null if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { //子 View 可以通过调用父 View 的 requestDisallowInterceptTouchEvent 方法设置 mGroupFlags 值 //以此告诉父 View 是否拦截事件 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; //如果子 view 没有告诉父 View 别拦截事件,那父 View 就判断自己是否需要拦截事件 if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // 重新恢复 action 以防被改变了 } else { //这里表明子 View 告诉父 View 不要拦截事件 intercepted = false; } } else { //当 mFirstTouchTarget=null(没有子 View 被分配处理),且不是 initial down 事件时(事件已经初始化过了),ViewGroup 继续拦截触摸 //继续设置为 true intercepted = true; } // 如果当前事件是 ACTION_CANCEL,或者 view.mPrivateFlags 被设置了 PFLAG_CANCEL_NEXT_UP_EVENT //那么当前事件就取消了 final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; //split 表示当前的 ViewGroup 是不是支持分割 MotionEvent 到不同的 View 当中 final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; //新的 TouchTarget TouchTarget newTouchTarget = null; //是否把事件分发给了新的 TouchTarget boolean alreadyDispatchedToNewTouchTarget = false; //不取消事件,同时不拦截事件才进入该区域 if (!canceled && !intercepted) { //把事件分发给所有的子视图,寻找可以获取焦点的视图 View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null; //如果是这三种事件就得遍历子 View 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; // 对于这个 PointerId 清空更早的 touch targets removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount; //如果当前 ViewGroup 有子 View 且 newTouchTarget=null if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); // 在视图里从前到后扫描一遍获取可以接收事件的子 View final ArrayList<View> preorderedList = buildTouchDispatchChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; //遍历所有子 View,找到一个来接收事件 for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); //如果当前子 View 没有获取焦点,则跳过这个子 View if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } //如果当前子 View 不可见且没有播放动画 或者 不在触摸点范围内,跳过这个子 View if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } //如果在触摸目标列表找到了与该子 View 对应的 TouchTarget,说明这个 view 正在接收事件,不需要再遍历,直接退出 newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { newTouchTarget.pointerIdBits |= idBitsToAssign; break; } resetCancelNextUpFlag(child); //子 view 处于触摸位置,就将事件分发给子 View,如果该子 View 返回 true,说明消费了这个事件,就跳出遍历 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 获取 TouchDown 的时间点 mLastTouchDownTime = ev.getDownTime(); // 获取 TouchDown 的 Index 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; } //获取 TouchDown 的 x,y 坐标 mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); //添加到触摸目标列表 同时给 mFirstTouchTarget 赋值 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) { // 到这里说明没有子 View 接收事件,那就把最近一次的触摸目标赋值给 newTouchTarget newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } } // mFirstTouchTarget 赋值是在通过 addTouchTarget 方法获取的; // 只有处理 ACTION_DOWN 事件,才会进入 addTouchTarget 方法。 // 这也正是当 View 没有消费 ACTION_DOWN 事件,则不会接收其他 MOVE,UP 等事件的原因 if (mFirstTouchTarget == null) { // 那就只能 ViewGroup 自己处理事件了 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // 到这里就说明有子 View 接收了 ACTION_DOWN 事件,那后续的 move up 等事件就继续分发给这个触摸目标 TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { //如果 view.mPrivateFlags 被设置了 PFLAG_CANCEL_NEXT_UP_EVENT 或者事件被 ViewGroup 拦截了 //那子 View 需要取消事件 final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; //继续分发事件给子 View if (dispatchTransformedTouchEvent(ev, cancelChild,