ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] # 基础知识 ## Activity、PhoneWindow、DecorView ![](https://img.kancloud.cn/27/2a/272a20ae7493e435306de73c220ff3c6_281x418.png) ## 事件MotionEvent MotionEvent负责处理报告所有类型的设备输入事件,如手指触摸、鼠标、触控笔、轨迹球等。在实际开发中我们主要关注手指触摸事件即可。 事件|说明 ---|--- ACTION_DOWN|手指初次接触到屏幕时触发 ACTION_MOVE|手指在屏幕上滑动时触发,可多次触发 ACTION_UP|手指离开屏幕时触发 ACTION_CANCEL|事件被上层拦截时触发 ACTION_OUTSIDE|手指不在控件区域时触发 ## Input系统 本段出自[http://gityuan.com/2015/09/19/android-touch/](http://gityuan.com/2015/09/19/android-touch/) 当手指触摸到屏幕时,屏幕硬件一行行不断地扫描每个像素点,获取到触摸事件后,从底层产生中断上报。再通过native层调用Java层InputEventReceiver中的dispatchInputEvent方法。经过层层调用,交由Activity的dispatchTouchEvent方法来处理。 ![](https://img.kancloud.cn/89/44/89445a5d29b5bc8313ee33c566a145ba_529x749.png) # Activity 的事件分发 ## DecorView的分发 首先从上图中DecorView的dispatchTouchEvent看起: ```java // DecorView.java public boolean dispatchTouchEvent(MotionEvent ev) { final Window.Callback cb = mWindow.getCallback(); return cb != null && !mWindow.isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev); } ``` Activity在创建Window对象时,会为Window对象设置回调接口,也就是Activity自己,这样Window在收到外界的状态改变时,就会回调Activity的相应方法。 上面代码中cb指Window.Callback,Activity实现了此接口,因此接下来调用Activity的dispatchTouchEvent: ```java // Activty.java public boolean dispatchTouchEvent(MotionEvent ev) { // 捕获用户正在和设备交互,为了更方便的管理通知栏的通知,暂不重要 if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } // 事件分发给 Window if (getWindow().superDispatchTouchEvent(ev)) { return true; } // 当 PhoneWindow 里的所有View都不消费事件时(多用于处理落在Window范围外部的触摸事件) return onTouchEvent(ev); } ``` 主要的代码在Window的事件分发,一起来看看: ## PhoneWindow 的分发 先来看看 PhoneWindow 的分发: ```java // PhoneWindow.java public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); } ``` 可以看到,直接调用自身 DecorView 的 superDispatchTouchEvent 方法: ```java // DecorView.java public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); } ``` 而 DecorView 又直接调用了父类 ViewGroup 的 dispatchTouchEvent 方法。DecorView是一个FrameLayout,触摸事件在这一步传递给了根 ViewGroup。 ## PhoneWindow不处理时,Activity 的处理 当 PhoneWindow 不消费事件,也就是当前 Activity 的所有 View 都不消费事件时,事件会回传给 Activity,调用 onTouchEvent 方法: ```java public boolean onTouchEvent(MotionEvent event) { if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; } return false; } ``` mWindow 为 PhoneWindow 对象,在 PhoneWindow 代码中未找到 shouldCloseOnTouch 方法,来看看其父类 Window 中的该方法: ```java public boolean shouldCloseOnTouch(Context context, MotionEvent event) { if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event) && peekDecorView() != null) { return true; } return false; } ``` 当设置了 mCloseOnTouchOutside 为 true,并且为 MotionEvent.ACTION_DOWN 事件,手指在 Activity 界面以外,且当前 Activity 包含子 View 时,会返回 true,当前 Activity 会被 finish。 下面来仔细看看 ViewGroup 的事件分发。 # ViewGroup 的事件分发 ## 概述 **分发事件** 方法:dispatchTouchEvent() 结果:返回值为true代表事件被当前View消费了,为false代表没被消费 1、由于Android视图结构为树状结构,所以触摸事件都是从rootView开始依次向下进行传递,称之为事件分发。 2、在没有拦截的情况下,事件最终将会分发给手指触摸区域的子 View,子 View 重叠时分发给最上层的子 View **拦截事件** 方法:onInterceptTouchEvent() 1、事件在分发的过程中,可以被中途某个ViewGroup进行拦截,停止向下进行分发传递。 2、ViewGroup拦截后,直接调用其onTouchEvent方法看是否需要消费使用。 ![](https://img.kancloud.cn/1c/5f/1c5fda29d08c3337720fd95824fd94a9_387x454.png) **使用事件** 方法:onTouchEvent(MotionEvent event) 定义:拿到MotionEvent实例,可以获取到事件内容并使用 结果:onTouchEvent返回值为true时代表消费了事件,上层View不会再接收事件回传;返回值为false时代表没消费事件,可能只是使用了事件的内容,但没消费,事件会继续进行回传。 事件被消费,就意味着事件信息传递终止 1、如果事件没有被拦截,会按视图层次结构依次往下进行传递,直到传递到最底层的View,也就是最终命中的View。 2、命中的View拿到事件后,会决定是否消费,如果消费了就没上层View什么事了。 3、如果命中的View没有消费事件,事件会回传,依次调用上层View的onTouchEvent方法,看上层View是否需要使用事件做些什么。 ![](https://img.kancloud.cn/06/28/0628cdfce78768557dd3af59e55bc594_394x580.png) ## ViewGroup 的事件分发机制伪代码: 这一段伪代码来源于 GcsSloop 大神的[文章](http://www.gcssloop.com/customview/dispatch-touchevent-source),很有嚼劲。理解了这段代码基本对于 Android 事件分发机制就有了宏观上的把握了。配合吴小龙同学的这篇 [Android 事件传递机制分析](http://wuxiaolong.me/2015/12/19/MotionEvent/)服用效果更佳! ```java // 返回值代表是否将事件消费掉 public boolean dispatchTouchEvent(MotionEvent event) { // 默认状态为未消费过 boolean result = false; // 如果没有拦截(ViewGroup一般不会进行拦截,可点击的ViewGroup除外,下文源码部分有分析) if (!onInterceptTouchEvent(event)) { // 则交给childView result = child.dispatchTouchEvent(event); } // 如果事件没有被childView消费 if (!result) { // 则调用自身 onTouchEvent() result = onTouchEvent(event); } // 返回事件消费状态 return result; } ``` ## ViewGroup 的事件分发机制源码 ```java public boolean dispatchTouchEvent(MotionEvent ev) { //... final boolean intercepted; //这里首先对是否拦截此事件做了很多判断 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { //... intercepted = onInterceptTouchEvent(ev); } else { intercepted = true; } //... // 如果不拦截才会往下走进行分发 if (!canceled && !intercepted) { final ArrayList<View> preorderedList = buildTouchDispatchChildList(); final View[] children = mChildren; // 查找可以处理本次事件的childView for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex); //... // 将事件交给childView if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { //... } } if (mFirstTouchTarget == null) { // 此处会调用super.dispatchTouchEvent() handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { //... } return handled; } ``` 其中交给childView处理的dispatchTransformedTouchEvent方法源码如下: ```java private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; //... if (child == null) { handled = super.dispatchTouchEvent(transformedEvent); } else { //调用childView的dispatchTouchEvent方法 handled = child.dispatchTouchEvent(transformedEvent); } return handled; } ``` 最后来看看刚刚 onInterceptTouchEvent 方法的默认实现: ```java 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; } ``` 可以看到,会先检查事件来源。当为鼠标事件,且当前为 ACTION_DOWN,且鼠标左键按下时,默认返回 true;大部分情况即非鼠标事件时,onInterceptTouchEvent 都是返回 false。 # View 的事件分发 在事件向下分发的过程中,如果中间没有ViewGroup拦截事件,事件会一直传递到最底层的子View。调用View的dispatchTouchEvent方法。 ## 分发顺序 View 可以注册很多事件监听器,如:单击事件(onClick)、长按事件(onLongClick)、触摸事件(onTouch),并且View自身也有 onTouchEvent 方法。这些方法由 dispatchTouchEvent() 方法进行分发。 事件的调度顺序为:onTouchListener > onTouchEvent > onLongClickListener > onClickListener ## View 的事件分发机制伪代码 ```java // 返回值代表是否将事件消费掉 public boolean dispatchTouchEvent(MotionEvent event) { if(mOnTouchListener.onTouch(this, event)) { return true; } else if (onTouchEvent(event)) { return true; } return false; } ``` onClickListener 和 onLongClickListener 在 onTouchEvent 中调用。 ## View 的事件分发机制源码: ```java public boolean dispatchTouchEvent(MotionEvent event) { //... if (onFilterTouchEventForSecurity(event)) { ListenerInfo li = mListenerInfo; // 回调mOnTouchListener接口 if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } // 调用onTouchEvent方法(在里面会回调onLongClickListener、onClickListener) if (!result && onTouchEvent(event)) { result = true; } } //... return result; } public boolean onTouchEvent(MotionEvent event) { // 判断View是否可点击 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: //... // 执行单击事件 performClick(); break; case MotionEvent.ACTION_DOWN: //... // 检测长按,并执行长按事件 checkForLongClick(0, x, y); break; } // 只要是可点击的,就会返回true,也就是当前View就会消费事件 return true; } return false; } ``` ## 点击事件 View的onClick事件需要同时接收到ACTION_DOWN和ACTION_UP才能触发,如果这两个事件被分发给了不同的View,点击事件就不会被触发。 View只有消费了ACTION_DOWN事件,才能接收到后续的事件,并且后续事件传递来时,不会再传递给其他View,除非在途中被其他View拦截。如果后续事件确实被拦截,当前View会收到一个ACTION_CANCEL的事件,表示事件结束,不会有后续事件。 一次触摸流程中产生的事件应被同一 View 消费,全部接收或者全部拒绝。 ## 注意要点 * 只要给 View 注册了 onClickListener、onLongClickListener、OnContextClickListener 其中的任何一个监听器或者设置了 android:clickable=”true” 就代表这个 View 是可点击的。 * 可点击的 View 就会消费事件,不可点击的 View 不会消费事件 * ViewGroup 和 ChildView 同时注册了点击事件监听器时,事件优先给 ChildView 消费 * 同一次点击事件只能被一个 View 消费,防止事件混乱 # 总结 对于ViewGroup来说: * 用户触摸事件从rootView依次向下进行传递。在没有拦截的情况下,事件会依次传递到最底部子View * 事件分发过程中,可被中途某个ViewGroup进行拦截,停止向下进行分发传递。ViewGroup拦截后直接调用其onTouchEvent看是否需要消费使用,如不消费事件往上进行回传 * 如果事件没被拦截顺利传递给最底层的View,命中的View会决定是否消费,消费了就没上层View的事情了 * 子View如果不消费事件,事件会进行回传,每个层级的ViewGroup都可以拿到事件并决定是否消费 * 如果事件一直没有被消费,最后会回传给 Activity,如果 Activity 也不需要就被抛弃 对于子View来说: * 只要注册了onClickListener、onLongClickListener、onContextClickListener或设置了clickable="true",就代表时可点击的 * 可点击的View就会消费事件,不可点击的View不会消费事件 * 当ChildView重叠时,一般会分配给显示在最上面的ChildView # 补充 关于 ViewGroup 对于触摸事件的分发流程图,可参考下面这几幅图,[来源](https://blog.csdn.net/xyz_lmn/article/details/12517911) 1、都不拦截且都不消费的情况 ![](https://img.kancloud.cn/a2/5c/a25cc0c7a0f9b0ab399d0aa2bc5af348_571x672.png) 2、View 消费了事件,不再进行回传 ![](https://img.kancloud.cn/e0/06/e00648dce27442722a03c1414ee50771_685x562.png) 3、View 消费了 ACTION_DOWN ![](https://img.kancloud.cn/7c/8b/7c8bfac03d3ff75081ba338c65fc0e87_732x693.png) View 消费了 ACTION_DOWN,但后续的 ACTION_MOVE 和 ACTION_UP 被上层拦截了,会给它个ACTION_CANCEL ![](https://img.kancloud.cn/08/34/083446b32dfa4eef6f8c5b303e578275_715x628.png) 4、上层一开始就拦截了事件,此时子 View 对于事件无感 ![](https://img.kancloud.cn/d1/ff/d1ff3576cc05d6c3a32cbe079b36c3ac_619x628.png) # 参考文档: [安卓自定义View进阶-事件分发机制详解](http://www.gcssloop.com/customview/dispatch-touchevent-source)