Android 事件分发机制
2016-02-04 14:31
453 查看
事件序列 : 在屏幕上点击的瞬间会产生 ACTION_DOWN 事件,紧跟着的滑动的 ACTION_MOVE 事件 ,到最后的 ACTION_DOWN 为一个完整的事件序列
传递顺序 : 当一个事件产生后,将由顺序 Activity -> Window -> DecorView -> ViewGroup -> View 逐层分发, 在获得一个事件后,先判断是否拦截,如果拦截,则该事件序列之后的事件不需要判断是否拦截,直接交由它处理,如果不拦截,则分发给子view,子View 重复该分发的步骤并返回是否处理,如果子view不处理,则自己处理该事件,如果它不处理,即onTouchEvent 返回 false 则将事件抛至上层处理,直到Activity
事件拦截 : 如果一个 ViewGroup中的 onInterceptTouchEvent 返回 true ,表示当前ViewGroup 要拦截当前事件,则该ViewGroup的onTouchEvent将会调用;如果不拦截,将会把事件分发给子view.
注意拦截ACION_DOWN与其他ACTION的差异:
- 第1种情况:如果ACTION_DOWN的事件没有被拦截,顺利找到了TargetView,那么后续的MOVE与UP都能够下发。如果后续的MOVE与UP下发时还有继续拦截的话,事件只能传递到拦截层,并且发出ACTION_CANCEL。
- 第2种情况:如果ACITON_DOWN的事件下发时被拦截,导致没有找到TargetView,那么后续的MOVE与UP都无法向下派发了,在Activity层就终止了传递。
view 处理事件: 如果一个View需要处理事件时,如果设置了onTouchListener ,将首先调用onTouchListener 的 onTouch 方法,如果返回true ,则该view的onTouchEvent 将不会调用,如果返回false,则进入 onTouchEvent事件,在onTouchEvent 方法中,如果设置了 clickable或者long clickable属性,则默认返回true,在此方法中,如果有onClickListener 将调用 onClickListener的onClick事件:
- 如果为一个view设置了onClickListener 将会自动设置 clickable为true 。
- Button 默认 clickable ,ImageView 为false
顺序 onTouch -> onTouchEvent -> onClick
几个方法
如果一个View处理某事件,但是没有消费ACTION_DOWN事件,则之后的事件都不会交给它处理
如果View不消耗除ACTION_DOWN之外的事件,则该事件会消失,父容器的 onTouchEvent 不会调用,且该序列的其它事件该view可以收到,消失的事件最终会传递给Activity处理
ViewGroup默认不拦截任何事件
ViewGroup决定拦截任何事件,则序列之后的事件都不会传递给子view,且不需要再判断是否拦截
View 不能拦截,即没有 onInterceptTouchEvent方法
View的onTouchEvent默认为消耗事件,除非是不可点击的
View的enable属性不影响onTouchEvent的值
如果子View处理了ACTION_DOWN事件,而ViewGroup要拦截ACTION_MOVE事件,则会向子View发送ACTION_CANCEL事件,并将状态重置,例如mFristTouchTarget置为null
以下源码解析来自Android Deeper(00) - Touch事件分发响应机制
传递顺序 : 当一个事件产生后,将由顺序 Activity -> Window -> DecorView -> ViewGroup -> View 逐层分发, 在获得一个事件后,先判断是否拦截,如果拦截,则该事件序列之后的事件不需要判断是否拦截,直接交由它处理,如果不拦截,则分发给子view,子View 重复该分发的步骤并返回是否处理,如果子view不处理,则自己处理该事件,如果它不处理,即onTouchEvent 返回 false 则将事件抛至上层处理,直到Activity
事件拦截 : 如果一个 ViewGroup中的 onInterceptTouchEvent 返回 true ,表示当前ViewGroup 要拦截当前事件,则该ViewGroup的onTouchEvent将会调用;如果不拦截,将会把事件分发给子view.
注意拦截ACION_DOWN与其他ACTION的差异:
- 第1种情况:如果ACTION_DOWN的事件没有被拦截,顺利找到了TargetView,那么后续的MOVE与UP都能够下发。如果后续的MOVE与UP下发时还有继续拦截的话,事件只能传递到拦截层,并且发出ACTION_CANCEL。
- 第2种情况:如果ACITON_DOWN的事件下发时被拦截,导致没有找到TargetView,那么后续的MOVE与UP都无法向下派发了,在Activity层就终止了传递。
view 处理事件: 如果一个View需要处理事件时,如果设置了onTouchListener ,将首先调用onTouchListener 的 onTouch 方法,如果返回true ,则该view的onTouchEvent 将不会调用,如果返回false,则进入 onTouchEvent事件,在onTouchEvent 方法中,如果设置了 clickable或者long clickable属性,则默认返回true,在此方法中,如果有onClickListener 将调用 onClickListener的onClick事件:
- 如果为一个view设置了onClickListener 将会自动设置 clickable为true 。
- Button 默认 clickable ,ImageView 为false
顺序 onTouch -> onTouchEvent -> onClick
几个方法
public boolean dispatchTouchEvent(MotionEvent event); //用于事件分发,如果事件能到达View,此方法必然会调用 public boolean onInterceptTouchEvent(MotionEvent event); //在上述方法内部调用,用来判断是否拦截事件,此方法保证一定调用 public boolean onTouchEvent(MotionEvent event); //在dispatchTouchEvent中调用,用来处理事件,返回是否消耗当前事件
几个经验总结
一旦一个元素决定拦截事件,则该事件序列之后的事件直接交由它处理,不需要再判断是否拦截,即onInterceptTouchEvent 不再调用且拦截后子View将收不到该事件如果一个View处理某事件,但是没有消费ACTION_DOWN事件,则之后的事件都不会交给它处理
如果View不消耗除ACTION_DOWN之外的事件,则该事件会消失,父容器的 onTouchEvent 不会调用,且该序列的其它事件该view可以收到,消失的事件最终会传递给Activity处理
ViewGroup默认不拦截任何事件
ViewGroup决定拦截任何事件,则序列之后的事件都不会传递给子view,且不需要再判断是否拦截
View 不能拦截,即没有 onInterceptTouchEvent方法
View的onTouchEvent默认为消耗事件,除非是不可点击的
View的enable属性不影响onTouchEvent的值
如果子View处理了ACTION_DOWN事件,而ViewGroup要拦截ACTION_MOVE事件,则会向子View发送ACTION_CANCEL事件,并将状态重置,例如mFristTouchTarget置为null
以下源码解析来自Android Deeper(00) - Touch事件分发响应机制
@Override public boolean dispatchTouchEvent(MotionEvent ev) { ...... boolean handled = false; ...... final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // 1)处理初始的ACTION_DOWN if (actionMasked == MotionEvent.ACTION_DOWN) { // 把ACTION_DOWN作为一个Touch手势的始点,清除之前的手势状态。 cancelAndClearTouchTargets(ev); //清除前一个手势,*关键操作:mFirstTouchTarget重置为null* resetTouchState(); //重置Touch状态标识 } // 2)检查是否会被拦截 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 是ACTION_DOWN的事件,或者mFirstTouchTarget不为null(已经找到能够接收touch事件的目标组件) final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // 判断禁止拦截的FLAG,因为requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法可以禁止执行是否需要拦截的判断 if (!disallowIntercept) { // 禁止拦截的FLAG为false,说明可以执行拦截判断,则执行此ViewGroup的onInterceptTouchEvent方法 intercepted = onInterceptTouchEvent(ev); // 此方法默认返回false,如果想修改默认的行为,需要override此方法,修改返回值。 ev.setAction(action); } else { // 禁止拦截的FLAG为ture,说明没有必要去执行是否需要拦截了,这个事件是无法拦截的,能够顺利通过,所以设置拦截变量为false intercepted = false; } } else { // 当不是ACTION_DOWN事件并且mFirstTouchTarget为null(意味着没有touch的目标组件)时,这个ViewGroup应该继续执行拦截的操作。 intercepted = true; } // 通过前面的逻辑处理,得到了是否需要进行拦截的变量值 final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; if (!canceled && !intercepted) { // 不是ACTION_CANCEL并且拦截变量为false if (actionMasked == MotionEvent.ACTION_DOWN) { // 在ACTION_DOWN时去寻找这次DOWN事件新出现的TouchTarget final int actionIndex = ev.getActionIndex(); // always 0 for down ..... final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { // 根据触摸的坐标寻找能够接收这个事件的子组件 final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); final View[] children = mChildren; // 逆序遍历所有子组件 for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = i; final View child = children[childIndex]; // 寻找可接收这个事件并且组件区域内包含点击坐标的子View if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { continue; } newTouchTarget = getTouchTarget(child); // 找到了符合条件的子组件,赋值给newTouchTarget ...... // 把ACTION_DOWN事件传递给子组件进行处理 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 如果此子ViewGroup消费了这个touch事件 mLastTouchDownTime = ev.getDownTime(); mLastTouchDownIndex = childIndex; mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); // 则为mFirstTouchTarget赋值为newTouchTarget,此子组件成为新的touch事件的起点 newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } } } ...... } } // 经过前面的ACTION_DOWN的处理,有两种情况。 if (mFirstTouchTarget == null) { // 情况1:(mFirstTouchTarget为null) 没有找到能够消费touch事件的子组件或者是touch事件被拦截了, // 那么在ViewGroup的dispatchTransformedTouchEvent方法里面,处理Touch事件则和普通View一样, // 自己无法消费,调用super.dispatchOnTouchEvent()把事件回递给父ViewGroup进行处理 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // 情况2:(mFirstTouchTarget!=null) 找到了能够消费touch事件的子组件,那么后续的touch事件都可以传递到子View TouchTarget target = mFirstTouchTarget; // (这里为了理解简单,省略了一个Target List的概念,有需要的同学再查看源码) while (target != null) { if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { // 如果前面利用ACTION_DOWN事件寻找符合接收条件的子组件的同时消费掉了ACTION_DOWN事件,这里直接返回true handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; // 对于非ACTION_DOWN事件,则继续传递给目标子组件进行处理(注意这里的非ACTION_DOWN事件已经不需要再判断是否拦截) if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { // 如果target子组件进行处理,符合某些条件的话,会传递ACTION_CANCEL给target子组件 // 条件是:如果ACTION_DOWN时没有被拦截,而后面的touch事件被拦截,则需要发送ACTION_CANCEL给target子组件 handled = true; } ...... } } } if (canceled || actionMasked == MotionEvent.ACTION_UP) { // 如果是ACTION_CANCEL或者ACTION_UP,重置Touch状态标识,mFirstTouchTarget赋值为null,后面的Touch事件都无法派发给子View resetTouchState(); } ...... return handled; }
相关文章推荐
- Android防止按钮两次点击
- android studio cmd $ANDROID_HOME is not defined
- 【Android】3.12 兴趣点( POI)搜索功能
- android 入门 001 (界面布局)
- Android仿知乎图片墙
- Android使用自定义控件HorizontalScrollView打造史上最简单的侧滑菜单
- Inside of Jemalloc
- 【Android】3.11 地理编码功能
- ANDROID_HOME on Mac OS X
- Andriod界面设计适配和Android Studio中的资源
- fragment+viewpage设置不预加载页面
- 【Android】3.10 热力图功能
- Android 中 startActivityForResult和setResult详解
- android:configChanges属性描述
- Android关闭输入法键盘
- ViewHolder写法
- Android 自定义组合控件
- 【Android】3.9 覆盖物功能
- 解决Android键盘出来把底部按钮顶上来的方法
- 进入activity 不自动弹键盘