目前网上的事件分发分析大多数都是用线性思维的逻辑去分析,更有甚者还有用流程图大法分析的,我深以为不然。Android View对应的数据结构是树呀。我们必须用树的遍历去研究事件分发,才能够真正做到理解。本篇文章我将用树状思维带大家去熟悉CoordinatorLayout事件分发。
将View视图树状化
假设有这样一个场景:CoordinatorLayout是布局的根View。它有三个子View,分别是vp1、vp2、vp3,他们分别对应的Behavior叫做vp1 behavior、vp2 behavior、vp3 behavior。vp1、vp2有分别有view1、view2、view3和view4、view5、view6几个子View。vp3的子view vp4子view分别是view7、view8、view9。
看到上面的场景感觉很绕吧,是不是有点迷糊?在这么迷糊的情况下如果还用线性思维或者流程图满天飞的跟你讲解,你最终会不会云里雾里?我们把它转化称如下的树状图,有没有觉得瞬间清晰了起来。
Behavior的onInterceptTouchEvent、onTouchEvent方法
场景我们已经介绍完了,下面我们来简单介绍一下Behavior。它是CoordinatorLayout的静态内部类。我们可以在布局文件中通过app:layout_behavior属性给CoordinatorLayout的直接子View设置,也可以通过代码设置。它的作用非常强大,有以下几个:
- 配合CoordinatorLayout改变传统的事件分发机制
- 配合CoordinatorLayout完成嵌套滑动事件处理
- 配合CoordinatorLayout处理有依赖关系子view的位置变化
本文我将主要讲解CoordinatorLayout是如何改变传统的事件分发机制。说到事件分发,那么我们不可避免的需要谈到以下三个方法,我们来看看它们在CoordinatorLayout中是如何实现的。
dispatchTouchEvent | onInterceptTouchEvent | onTouchEvent |
无重写 | 有重写 | 有重写 |
- dispatchToucheEvent方法在CoordinatorLayout源码中没有重写,那么dispatch还是沿用了ViewGroup的分发机制。
- onInterceptTouchEvent和onTouchEvent被重写了,按照传统的事件分发机制,我们将先后分析它们。
首先我们先贴出它们的源码,不做深入的理解,我们能大概知道,它们的代码行数并不多,它们最终都是调用了performIntercept方法
CoordinatorLayout#onInterceptTouchEvent
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getActionMasked(); // Make sure we reset in case we had missed a previous important event. if (action == MotionEvent.ACTION_DOWN) { resetTouchBehaviors(true); } final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { resetTouchBehaviors(true); } return intercepted; }
CoordinatorLayout#onTouchEvent
@Override @SuppressWarnings("unchecked") public boolean onTouchEvent(MotionEvent ev) { boolean handled = false; boolean cancelSuper = false; MotionEvent cancelEvent = null; final int action = ev.getActionMasked(); if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) { // Safe since performIntercept guarantees that // mBehaviorTouchView != null if it returns true final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams(); final Behavior b = lp.getBehavior(); if (b != null) { handled = b.onTouchEvent(this, mBehaviorTouchView, ev); } } // Keep the super implementation correct if (mBehaviorTouchView == null) { handled |= super.onTouchEvent(ev); } else if (cancelSuper) { if (cancelEvent == null) { final long now = SystemClock.uptimeMillis(); cancelEvent = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); } super.onTouchEvent(cancelEvent); } if (cancelEvent != null) { cancelEvent.recycle(); } if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { resetTouchBehaviors(false); } return handled; }
private boolean performIntercept(MotionEvent ev, final int type) { boolean intercepted = false; boolean newBlock = false; MotionEvent cancelEvent = null; final int action = ev.getActionMasked(); final List<View> topmostChildList = mTempList1; getTopSortedChildren(topmostChildList); // Let topmost child views inspect first final int childCount = topmostChildList.size(); for (int i = 0; i < childCount; i++) { final View child = topmostChildList.get(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final Behavior b = lp.getBehavior(); if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) { // Cancel all behaviors beneath the one that intercepted. // If the event is "down" then we don't have anything to cancel yet. if (b != null) { if (cancelEvent == null) { final long now = SystemClock.uptimeMillis(); cancelEvent = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); } switch (type) { case TYPE_ON_INTERCEPT: b.onInterceptTouchEvent(this, child, cancelEvent); break; case TYPE_ON_TOUCH: b.onTouchEvent(this, child, cancelEvent); break; } } continue; } if (!intercepted && b != null) { switch (type) { case TYPE_ON_INTERCEPT: intercepted = b.onInterceptTouchEvent(this, child, ev); break; case TYPE_ON_TOUCH: intercepted = b.onTouchEvent(this, child, ev); break; } if (intercepted) { mBehaviorTouchView = child; } } // Don't keep going if we're not allowing interaction below this. // Setting newBlock will make sure we cancel the rest of the behaviors. final boolean wasBlocking = lp.didBlockInteraction(); final boolean isBlocking = lp.isBlockingInteractionBelow(this, child); newBlock = isBlocking && !wasBlocking; if (isBlocking && !newBlock) { // Stop here since we don't have anything more to cancel - we already did // when the behavior first started blocking things below this point. break; } } topmostChildList.clear(); return intercepted; }
这里我将三个跟事件分发相关的方法源码贴出来了,目的是让大家有个初步的印象,暂时不做深入的讲解。后面我将结合案例,循序渐进地讲解这几个方法。
传统事件分发和CoordinatorLayout事件分发区别
为了简化,下文用onIntercept代替onInterceptTouchEvent、onTouch代替onTouchEvent、CL代替CoordinatorLayout
- 传统事件分发图上所有的ViewGroup onIntercept返回false,所有的ViewGroup和View onTouch返回false
通过事件分发四步曲之一《深度遍历讲解Android事件分发机制》[1]一文我们可以知道事件调用流程如下
vp0#onIntercept -> vp3#onIntercept -> vp4#onIntercept -> view9#onTouch -> view8#onTouch -> view7#onTouch -> vp4#onTouch -> vp3#onTouch -> vp2#onIntercept -> view6#onTouch -> view5#onTouch -> view4#onTouch -> vp2#onTouch -> vp1#onIntercept -> view3#onTouch -> view2#onTouch -> view1#onTouch -> vp1#onTouch -> vp0#onTouch
如果你不能够理解这个调用流程,那么我建议你用笔在树形图上画画线。你也许会发现,Down事件在传统的事件分发机制中是通过后序遍历来分发的。
- 在CL事件分发图中,我们假设所有的Behavior和View的onIntercept和onTouch都返回false。
我们来写个Demo,打个Log先看看吧
/** * 场景一、所有的Behavior都不拦截事件,都不处理事件。所有的View都不拦截事件,所有的View都不处理事件 */ class CoordinatorLayoutEventOneActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val coordinatorLayout: CoordinatorLayout = CoordinatorLayout(this) //vp1 vp2 vp3 默认不拦截事件 默认不分发事件 val vp1 = MyFrameLayout(this) vp1.name = "VP1" val vp1Params = CoordinatorLayout.LayoutParams( CoordinatorLayout.LayoutParams.MATCH_PARENT, CoordinatorLayout.LayoutParams.MATCH_PARENT ) vp1Params.behavior = MyBehavior().apply { name = "VP1 Behavior" } coordinatorLayout.addView(vp1, vp1Params) val vp2 = MyFrameLayout(this) vp2.name = "VP2" val vp2Params = CoordinatorLayout.LayoutParams( CoordinatorLayout.LayoutParams.MATCH_PARENT, CoordinatorLayout.LayoutParams.MATCH_PARENT ) vp2Params.behavior = MyBehavior().apply { name = "VP2 Behavior" } coordinatorLayout.addView(vp2, vp2Params) val vp3 = MyFrameLayout(this) vp3.name = "VP3" val vp3Params = CoordinatorLayout.LayoutParams( CoordinatorLayout.LayoutParams.MATCH_PARENT, CoordinatorLayout.LayoutParams.MATCH_PARENT ) vp3Params.behavior = MyBehavior().apply { name = "VP3 Behavior" } coordinatorLayout.addView(vp3, vp3Params) vp1.addView(MyView(this).apply { name = "view1" }) vp1.addView(MyView(this).apply { name = "view2" }) vp1.addView(MyView(this).apply { name = "view3" }) vp2.addView(MyView(this).apply { name = "view4" }) vp2.addView(MyView(this).apply { name = "view5" }) vp2.addView(MyView(this).apply { name = "view6" }) val vp4 = MyFrameLayout(this).apply { name = "VP4" } vp3.addView(vp4) vp4.addView(MyView(this).apply { name = "view7" }) vp4.addView(MyView(this).apply { name = "view8" }) vp4.addView(MyView(this).apply { name = "view9" }) setContentView(coordinatorLayout) } }
在自定义MyFrameLayout、MyView、MyBehavior的事件处理方法中打印Log
override fun onInterceptTouchEvent( parent: CoordinatorLayout, child: View, ev: MotionEvent ): Boolean { Log.d(LogTag.tag,"$name onInterceptTouchEvent "+MotionEvent.actionToString(ev.action)) return interceptValue } @RequiresApi(Build.VERSION_CODES.KITKAT) override fun onTouchEvent(parent: CoordinatorLayout, child: View, ev: MotionEvent): Boolean { Log.d(LogTag.tag,"$name onTouchEvent "+MotionEvent.actionToString(ev.action)) return touchValue }
打印日志如下,忽略Cancel事件,我们标上行数
VP1 Behavior onInterceptTouchEvent ACTION_CANCEL VP2 Behavior onInterceptTouchEvent ACTION_CANCEL VP3 Behavior onInterceptTouchEvent ACTION_CANCEL 1. VP3 Behavior onInterceptTouchEvent ACTION_DOWN 2. VP2 Behavior onInterceptTouchEvent ACTION_DOWN 3. VP1 Behavior onInterceptTouchEvent ACTION_DOWN 4. VP3 onInterceptTouchEvent ACTION_DOWN 5. VP4 onInterceptTouchEvent ACTION_DOWN 6. view9 onTouchEvent ACTION_DOWN 7. view8 onTouchEvent ACTION_DOWN 8. view7 onTouchEvent ACTION_DOWN 9. VP4 onTouchEvent ACTION_DOWN 10. VP3 onTouchEvent ACTION_DOWN 11. VP2 onInterceptTouchEvent ACTION_DOWN 12. view6 onTouchEvent ACTION_DOWN 13. view5 onTouchEvent ACTION_DOWN 14. view4 onTouchEvent ACTION_DOWN 15. VP2 onTouchEvent ACTION_DOWN 16. VP1 onInterceptTouchEvent ACTION_DOWN 17. view3 onTouchEvent ACTION_DOWN 18. view2 onTouchEvent ACTION_DOWN 19.view1 onTouchEvent ACTION_DOWN 20. VP1 onTouchEvent ACTION_DOWN 21. Behavior onTouchEvent ACTION_DOWN 22. VP2 Behavior onTouchEvent ACTION_DOWN 23. VP1 Behavior onTouchEvent ACTION_DOWN
我们可以得出结论:
- 在CL调用onIntercept时,它会从最后一个子View从后往前挨个调用的Behavior的onIntercept(注意,此处不考虑anchor、dependent等情况,这些情况拓扑排序会改变遍历的顺序,由于拓扑排序不在本文篇幅中,请自行查阅)。
- 在CL调用onTouch时,也会从后往前调用Behavior的onTouch
传统事件分发和CL事件分发区别是:CL处理事件时,优先交给它的子View的Behavior去处理,如果子View的Behavior不处理,才会按照传统的事件分发机制去分发
更多场景
Behavior类的onIntercept和onTouch方法返回值为Boolean类型,那么根据返回值不同,至少有四种情况。我们已经分析过了最简单的一种情况,case1
case | onInterceptTouchEvent | onTouchEvent |
case1 | false | false |
case2 | true | false |
case3 | false | true |
case4 | true | true |
ViewGroup的情况也有四种
case | onInterceptTouchEvent | onTouchEvent |
case1 | false | false |
case2 | true | false |
case3 | false | true |
case4 | true | true |
所有一共有4X4=16种,由于篇幅有限,不可能一一列举出来。我已将代码上传到github。建议你下载下来,亲自体验一番,巩固所得。