Android|第3章 View的事件体系


文章目录

      • 一、View基础知识
        • 1. View 的位置参数
        • 2. MotionEvent 和 TouchSlop
          • 1)MotionEvent:在手指触摸后产生的一系列事件
          • 2)TouchSlop:是系统所能识别的被认为是滑动的最小距离
        • 3. VelocityTracker 、GestureDetector 和 Scroller
          • 1)VelocityTracker:速度追踪,追踪手指在滑动过程中的速度
          • 2)GestureDetector:手势检测,用于辅助用户的单击、滑动、长按、双击等行为
          • 3)Scroller:弹性滑动对象,用于实现View的弹性滑动
      • 二、View的滑动
        • 1. 使用scrollTo/scrollBy
        • 2. 使用动画
        • 3. 改变布局参数
        • 4. 各种滑动方式比对
      • 三、弹性滑动
        • 1. 使用Scroller
        • 2. 使用动画
        • 3. 使用延时策略
          • 1)Handler/postDelay
          • 2)Thread/sleep
      • 四、View的事件分发机制
        • 1. 传递规则
          • 1)三个主要方法
          • 2)事件传递顺序
          • 3)onTouchEvent中回调顺序
          • 4)一些结论
        • 2. 事件分发机制源码解析
          • 1)Activity 对点击事件的分发过程
          • 2)顶级View(ViewGroup)对事件的分发过程
          • 3)View对点击事件的处理过程
      • 五、View的滑动冲突
        • 1. 常见的滑动冲突场景
        • 2. 滑动冲突的处理规则
        • 3. 滑动冲突的解决方式(待深入学习)
          • 1)外部拦截法
          • 2)内部拦截法

一、View基础知识
1. View 的位置参数 left、right、top、bottom
View在平移的过程中,top和left表示的是原始左上角的位置信息,值不会发生改变,发生改变的是x、y、translationX和translationY这四个参数
2. MotionEvent 和 TouchSlop 1)MotionEvent:在手指触摸后产生的一系列事件
  • ACTION_DOWN
  • ACTION_MOVE
  • ACTION_UP
通过getX/getY 返回相对于当前VIew的左上角的x和y坐标
通过getRawX/getRawY 返回相对于屏幕左上角的x和y坐标
2)TouchSlop:是系统所能识别的被认为是滑动的最小距离 获取这个常量:ViewConfiguration.get(getContext()).getScaledTouchSlop()
3. VelocityTracker 、GestureDetector 和 Scroller 1)VelocityTracker:速度追踪,追踪手指在滑动过程中的速度
  • 在View 的onTouchEvent中追踪当前单击事件的速度
VelocityTracker velocityTracker = VelocityTracker.obtain(); VelocityTracker.addMovement(event);

  • 先计算速度再获取速度
VelocityTracker.computeCurrentVelocity(1000); //1000像素 int xVelocity = (int)velocityTracker.getXVelocity();

  • 速度指的是一段时间内手指所滑过的像素数
  • 不需要是用clear重置并回收
VelocityTracker.clear(); VelocityTracker.recycle();

2)GestureDetector:手势检测,用于辅助用户的单击、滑动、长按、双击等行为
  • 创建一个GestureDetector对象并实现OnGestureListener接口,还可以实现OnDoubleTaoListener 从而监听双击行为
GestureDetector mGestureDetector = new GestureDetector(this); mGestureDetector.setIsLongpressEnabled(false); //解决长按屏幕无法拖动现象

  • 接管目标View的onTouchEvent方法
boolean consume = mGestureDetector.onTouchEvent(event); return consume;

  • 做完上面两步,就可以有选择地实现OnGestureListener 和 OnDoubleTaoListener中的方法:onSingleTapUp(单击)、onFling(滑动)、onScroll(拖动)、onLongPress(长按)、onDoubleTap(双击)等
建议:监听滑动相关,建议自己在onTouchEvent中实现,双击行为,建议使用GestureDetector
3)Scroller:弹性滑动对象,用于实现View的弹性滑动 使用View的scrollTo/scrollBy 方法进行滑动时,其滑动效果是瞬间完成的,这时就可以用Scroller来实现有过渡效果的滑动
Scroller需要和View的computeScroll方法配合使用,典型代码如下:
Scroller scroller = new Scroller(mContext); // 缓慢滚动到指定位置 private void smoothScrollTo(int destX, int destY) { int scrollX = getScrollX(); int delta = destX - scrollX; // 1000ms内滑向destX,效果是慢慢滑动 scroller.starlScroll(scrollX, 0, delta, 0, 1000); invalidate(); }@override public void computeScroll() { if (scroller.computeScrollOffset) { scrollTo(scroller.getCurrX(), scroller.getCurrY()); postInvalidate(); } }

二、View的滑动
3种方式
1. 使用scrollTo/scrollBy
  • scrollBy 实际也是基于 scrollTo实现,前者基于当前位置相对滑动,后者基于所传递参数绝对滑动
  • mScrollX 总是等于View左边缘和View内容左边缘在水平方向的距离,可通过getScrollX获取
  • mScrollY 总是等于View上边缘和View内容上边缘在竖直方向的距离,可通过getScrollY获取
  • scrollBy 和 scrollTo 只能改变View内容的位置而不能改变View在布局中的位置
  • mScrollX 和 mScrollY 的单位为像素,右移上移为正,反之为负
2. 使用动画
  • View动画的 translationX 和 translationY,注意View动画是对View的影响做操作,并不能真正改变View的位置参数,包括宽/高,并且如果希望动画结束后状态保留还要设置 fillAfter为true
  • 属性动画,Android3.0以上才支持,真正改变属性
ObjectAnimator.ofFloat(targetView, "translationX", 0, 100).setDuration(100).start();

注意:使用View动画可能带来问题,平移后的View将无法响应点击事件,因为点击事件实在View的初始区域注册的
3. 改变布局参数 改变LayoutParams
MarginLayoutParams params = (MarginLaoutParams)mButton.getLayoutParams(); params.width += 100; params.laftMargin += 100; mButton.requestLayout();

4. 各种滑动方式比对
  • scrollTo/scrollBy:操作简单,适合对View内容的滑动
  • 动画:操作简单,适用于没有交互的View和实现复杂的动画效果
  • 改变布局参数:操作稍微复杂,适用于有交互的View
三、弹性滑动
1. 使用Scroller
Scroller scroller = new Scroller(mContext); // 缓慢滚动到指定位置 private void smoothScrollTo(int destX, int destY) { int scrollX = getScrollX(); int delta = destX - scrollX; // 1000ms内滑向destX,效果是慢慢滑动 scroller.starlScroll(scrollX, 0, delta, 0, 1000); invalidate(); }@override public void computeScroll() { if (scroller.computeScrollOffset) { scrollTo(scroller.getCurrX(), scroller.getCurrY()); postInvalidate(); } }

再看一下computeScrollOffset方法的实现:
public boolean computeScrollOffset() { int timePassed = (int)(AnimationUtils.currentAnimationTImeMills() - mStartTime); if (timePassed < mDuration) { switch (MODE) { case SCROLL_MODE: final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); mCurrX = mStartX + Math.round(x * mDeltaX); mCurrY = mStartY + MAth.round(x * mDeltaX); break; ··· } } return true; }

上面是Scroller的典型使用方法
工作原理:
  • 构造一个Scroller对象并调用它的 startScroll 方法时,Scroller内部其实什么也没做,只是保存了我们传递的几个参数;
  • 然后调用invalidate方法会导致VIew重绘;
  • 在VIew 的draw方法中又会去调用 computeScroll 方法,computeScroll方法在View中是一个空实现,因此需要我们自己去实现,computeScrollOffset 中,每一次重绘距滑动起始时间都会有一个时间间隔,通过这个时间间隔Scroller就可以得到当前VIew的滑动位置,computeScroll中我们去向Scroller获取当前的scrollX 和 scrollY,然后通过scrollTo滑动;
  • 接着调用postInvalidate 进行第二次重绘;
  • 这一次重绘和第一次一样,还是会导致computeScroll被调用,继续向Scroller获取当前的scrollX 和 scrollY并滑动,如此反复,直至滑动结束。
以上,View的每一次重绘都会导致VIew进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,同时整个过程对View没有丝毫引用,也没有内部计时器
2. 使用动画 基本实现:
ObjextAnimator.odFloat(targetView, "translationX", 0, 100).setDuration(100).start();

模仿Scroller实现:
final int StartX = 0; final int deltaX = 100; ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(1000); animator.addUppdateListener(new AnimatorUpdateListener() { @override public void onAnimatorUpdate(ValueAnimator animator) { float duration = animator.getAnimatedFraction(); mButton.scrollTo(startX + (int)(deltaX * fraction), 0); } })

上述实现本质上没有作用于任何对象,只是在1000ms内在动画的每一帧获取动画完成比例,根据这个比例计算出VIew要滑动的距离,针对VIew内容而非VIew本身
3. 使用延时策略 1)Handler/postDelay 大约1000ms内将VIew向左移动100像素
private static final int MESSAGE_SCROLL_TO = 1; private static final int FRAME_COUNT = 30; private static final int DELAYED_TIME = 30; private int mCount = 0; @SuppressLint("HandlerLeak") private Handler mHandler = new Handler() { public void handleMaeeage(Messaeg msg) { switch(msg.what) { case Message_SCROLL_TO: { mCount ++; if (mCount <= FRAME_COUNT) { float fraction = mCount / (float) FRAME_COUNT; int scrollX = (int)(fraction * 100); mButton.scrollTo(scrollX, 0); mHandler.sendEmptyMessageDelayd(MESSAGE_SCROLL_TO, DELAYED_TIME); } break; } default: break; } }; };

2)Thread/sleep 四、View的事件分发机制
参考:https://www.gcssloop.com/customview/dispatch-touchevent-theory
http://wuxiaolong.me/2015/12/19/MotionEvent/
1. 传递规则 1)三个主要方法
类型 相关方法 Activity VIewGroup View
事件分发 dispatchTouchEvent ?? ?? ??
事件拦截 onInterceptTouchEvent ? ?? ?
事件消费 onTouchEvent ?? ?? ??
  • Activity 作为原始的事件分发者,如果 Activity 拦截了事件会导致整个屏幕都无法响应事件,这肯定不是我们想要的效果。
  • View最为事件传递的最末端,要么消费掉事件,要么不处理进行回传,根本没必要进行事件拦截。
三个事件关系伪代码:
public Boolean dispatchTouchEvent(MotionEvent ev) { boolean consume = false; if (onInterceptTouchEvent(ev)) { consume = onTouchEvent(ev); } else { consume = child.dispatchTouchEvent(ev); } return consume; }

2)事件传递顺序 事件收集之后最先传递给 Activity, 然后依次向下传递,大致如下:
Activity -> PhoneWindow -> DecorView -> ViewGroup -> ... -> View

如果最后分发到View,如果这个View也没有处理事件怎么办,那么这个事件会按照反方向回传,最终传回给Activity,如果最后 Activity 也没有处理,本次事件才会被抛弃:
Activity <- PhoneWindow <- DecorView <- ViewGroup <- ... <- View

3)onTouchEvent中回调顺序
  • 单击事件(onClickListener) 需要两个两个事件(ACTION_DOWN 和 ACTION_UP )才能触发,如果先分配给onClick判断,等它判断完,用户手指已经离开屏幕,黄花菜都凉了,定然造成 View 无法响应其他事件,应该最后调用。(最后)
  • 长按事件(onLongClickListener) 同理,也是需要长时间等待才能出结果,肯定不能排到前面,但因为不需要ACTION_UP,应该排在 onClick 前面。(onLongClickListener > onClickListener)
  • 触摸事件(onTouchListener) 如果用户注册了触摸事件,说明用户要自己处理触摸事件了,这个应该排在最前面。(最前)
  • View自身处理(onTouchEvent) 提供了一种默认的处理方式,如果用户已经处理好了,也就不需要了,所以应该排在 onTouchListener 后面。(onTouchListener > onTouchEvent)
所以事件的调度顺序应该是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener
4)一些结论
  • 同一个事件序列从手指接触屏幕到离开屏幕,以down开始,中间含有数目不定的move,最终以up结束
  • 正常情况下,一个事件序列只能被一个View拦截并消耗
  • 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回false),那么同一事件序列中的其他事件都不会再交给它处理,并将事件重新交给它的父元素去处理
  • 如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,父元素的onTouchEvent不会调用,最终这些消失的点击事件会传递给Activity处理
  • ViewGroup默认不拦截任何事件
  • View没有onInterceptTouchEvent
  • View的onTouchEvent默认会消耗事件,除非它是不可点击的
  • View的enable不影响onTouchEvent返回值
……
2. 事件分发机制源码解析 参考:https://www.gcssloop.com/customview/dispatch-touchevent-source
1)Activity 对点击事件的分发过程
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }

事件开始交给Activity中的WIndow进行分发,返回flase意味着事件没人处理,那么Activity的 onTouchEvent会被调用
Window是一个抽象类,superDispatchTouchEvent也是抽象方法,其实现类是 PhoneWindow,PhoneWindow中处理:
@Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }

PhoneWindow会将事件传递给DecorView,DecorView就说当前界面的底层容器(即setContentView所设置的View的父容器)
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks

DecorView 继承自FrameLayout且是Activity根View的父View,所以事件到这里已经传递给顶级View了,即在Activity中通过setContentView所设置的VIew,顶级View也叫根View,一般是VIewGroup
2)顶级View(ViewGroup)对事件的分发过程 事件达到顶级View(一般是一个ViewGroup)后,会调用ViewGroup的dispatchTouchEvent,主要工作:
  • 判断自身是否需要(询问 onInterceptTouchEvent 是否拦截),如果需要,调用自己的 onTouchEvent
  • 自身不需要或者不确定,则询问 ChildView ,一般来说是调用手指触摸位置的 ChildView
  • 如果子 ChildView 不需要则调用自身的 onTouchEvent
源码分析:
public boolean dispatchTouchEvent(MotionEvent ev) { // 调试用 if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(ev, 1); }// 判断事件是否是针对可访问的焦点视图(很晚才添加的内容,个人猜测和屏幕辅助相关,方便盲人等使用设备) 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; // 处理第一次ACTION_DOWN. if (actionMasked == MotionEvent.ACTION_DOWN) { // 清除之前所有的状态 cancelAndClearTouchTargets(ev); resetTouchState(); }// 检查是否需要拦截. 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); // 恢复操作,防止被更改 } else { intercepted = false; } } else { // 没有目标来处理该事件,而且也不是一个新的事件事件(ACTION_DOWN), 进行拦截。 intercepted = true; }// 判断事件是否是针对可访问的焦点视图 if (intercepted || mFirstTouchTarget != null) { ev.setTargetAccessibilityFocus(false); }// 检查事件是否被取消(ACTION_CANCEL). 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) {// 如果事件是针对可访问性焦点视图,我们将其提供给具有可访问性焦点的视图。 // 如果它不处理它,我们清除该标志并像往常一样将事件分派给所有的 ChildView。 // 我们检测并避免保持这种状态,因为这些事非常罕见。 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(); final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; // 清除此指针ID的早期触摸目标,防止不同步。 removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); // 获取触摸位置坐标 final float y = ev.getY(actionIndex); // 查找可以接受事件的 ChildView final ArrayList preorderedList = buildOrderedChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; // ▼注意,从最后向前扫描 for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex); // 如果有一个视图具有可访问性焦点,我们希望它首先获取事件, // 如果不处理,我们将执行正常的分派。 // 尽管这可能会分发两次,但它能保证在给定的时间内更安全的执行。 if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; }// 检查View是否允许接受事件(即处于显示状态(VISIBLE)或者正在播放动画) // 检查触摸位置是否在View区域内 if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; }// getTouchTarget 中判断了 child 是否包含在 mFirstTouchTarget 中 // 如果有返回 target,如果没有返回 null newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { // ChildView 已经准备好接受在其区域内的事件。 newTouchTarget.pointerIdBits |= idBitsToAssign; break; // ??已经找到目标View,跳出循环 }resetCancelNextUpFlag(child); if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null) { 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; }ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); }if (newTouchTarget == null && mFirstTouchTarget != null) { // 没有找到 ChildView 接收事件 newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } }// 分发 TouchTarget if (mFirstTouchTarget == null) { // 没有 TouchTarget,将当前 ViewGroup 当作普通的 View 处理。 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // 分发TouchTarget,如果我们已经分发过,则避免分配给新的目标。 // 如有必要,取消分发。 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; } }// 如果需要,更新指针的触摸目标列表或取消。 if (canceled || actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { resetTouchState(); } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) { final int actionIndex = ev.getActionIndex(); final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); removePointersFromTouchTargets(idBitsToRemove); } }if (!handled && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); } return handled; }

3)View对点击事件的处理过程 事件调度:onTouchListener > onTouchEvent > onLongClickListener > onClickListene
public boolean dispatchTouchEvent(MotionEvent event) { ... boolean result = false; // result 为返回值,主要作用是告诉调用者事件是否已经被消费。 if (onFilterTouchEventForSecurity(event)) { ListenerInfo li = mListenerInfo; /** * 如果设置了OnTouchListener,并且当前 View 可点击,就调用监听器的 onTouch 方法, * 如果 onTouch 方法返回值为 true,就设置 result 为 true。 */ if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; }/** * 如果 result 为 false,则调用自身的 onTouchEvent。 * 如果 onTouchEvent 返回值为 true,则设置 result 为 true。 */ if (!result && onTouchEvent(event)) { result = true; } } ... return result; }

onTouchEvent中
public boolean onTouchEvent(MotionEvent event) { ... final int action = event.getAction(); // 检查各种 clickable if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { switch (action) { case MotionEvent.ACTION_UP: ... removeLongPressCallback(); // 移除长按 ... performClick(); // 检查单击 ... break; case MotionEvent.ACTION_DOWN: ... checkForLongClick(0); // 检测长按 ... break; ... } return true; // ??表示事件被消费 } return false; }

注意:
  • 不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件
  • 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关
五、View的滑动冲突
1. 常见的滑动冲突场景
  • 场景1——外部滑动方向和内部滑动方向不一致
  • 场景2——外部滑动方向和内部滑动方向一致
  • 场景3——上面两种情况的嵌套
2. 滑动冲突的处理规则
  • 对于场景1,用户左右滑动时,让外部的View拦截事件(假设外部是左右滑动的),用户上下滑动时,让内部View拦截事件,判断用户是左右还是上下滑动可以依据滑动路径的角度、速度、距离差等
  • 对于场景2,一般在业务上找突破点,根据业务规定在某种状态时需要哪一部分View响应用户的滑动
  • 场景3 同 场景2
3. 滑动冲突的解决方式(待深入学习) 1)外部拦截法 点击事件都先经过父容器的拦截处理,如果父容器需要此事就拦截,不需要就不拦截,符合点击事件的分发机制
重写onInterceptTouchEvent 方法:
  • ACTION_DOWN 这个事件父容器必须返回false
  • ACTION_MOVE 这个事件根据需求来决定是否拦截
  • ACTION_UP 事件,必须返回false
2)内部拦截法 【Android|第3章 View的事件体系】指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理,和事件分发机制不一致,需要配合 requestDisallowInterceptTouchEvent 方法

    推荐阅读