事件分发四部曲之三《CoordinatorLayout事件分发》

1,790 阅读6分钟

 本文是Android事件分发四部曲之三

 1. 事件分发四部曲之一《深度遍历讲解Android事件分发机制》

 2. 事件分发四部曲之二《嵌套滑动事件分析》

 3. 事件分发四部曲之三《CoordinatorLayout事件分析》

 4. 事件分发四部曲之四《Put All Together》

目前网上的事件分发分析大多数都是用线性思维的逻辑去分析,更有甚者还有用流程图大法分析的,我深以为不然。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设置,也可以通过代码设置。它的作用非常强大,有以下几个:

  1. 配合CoordinatorLayout改变传统的事件分发机制
  2. 配合CoordinatorLayout完成嵌套滑动事件处理
  3. 配合CoordinatorLayout处理有依赖关系子view的位置变化

本文我将主要讲解CoordinatorLayout是如何改变传统的事件分发机制。说到事件分发,那么我们不可避免的需要谈到以下三个方法,我们来看看它们在CoordinatorLayout中是如何实现的。

dispatchTouchEventonInterceptTouchEventonTouchEvent
无重写有重写有重写
  1. dispatchToucheEvent方法在CoordinatorLayout源码中没有重写,那么dispatch还是沿用了ViewGroup的分发机制。
  2. 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

  1. 传统事件分发图上所有的ViewGroup onIntercept返回false,所有的ViewGroup和View onTouch返回false 传统事件分发图

通过事件分发四步曲之一《深度遍历讲解Android事件分发机制》一文我们可以知道事件调用流程如下

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事件在传统的事件分发机制中是通过后序遍历来分发的。

  1. 在CL事件分发图中,我们假设所有的Behavior和View的onIntercept和onTouch都返回false。 CoordinatorLayout事件分发图

我们来写个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

调用流程图

我们可以得出结论:

  1. 在CL调用onIntercept时,它会从最后一个子View从后往前挨个调用的Behavior的onIntercept(注意,此处不考虑anchor、dependent等情况,这些情况拓扑排序会改变遍历的顺序,由于拓扑排序不在本文篇幅中,请自行查阅)。
  2. 在CL调用onTouch时,也会从后往前调用Behavior的onTouch

传统事件分发和CL事件分发区别是: CL处理事件时,优先交给它的子View的Behavior去处理,如果子View的Behavior不处理,才会按照传统的事件分发机制去分发

更多场景

Behavior类的onIntercept和onTouch方法返回值为Boolean类型,那么根据返回值不同,至少有四种情况。我们已经分析过了最简单的一种情况,case1

caseonInterceptTouchEventonTouchEvent
case1falsefalse
case2truefalse
case3falsetrue
case4truetrue

ViewGroup的情况也有四种

caseonInterceptTouchEventonTouchEvent
case1falsefalse
case2truefalse
case3falsetrue
case4truetrue

所有一共有4X4=16种,由于篇幅有限,不可能一一列举出来。我已将代码上传到github。建议你下载下来,亲自体验一番,巩固所得。

DEMO地址

我的开源项目

我开源了一个方便RecyclerView吸顶的Android库,欢迎您访问吸顶项目地址,如果您使用本库,请提出您的宝贵意见。

它目前支持以下功能:

  • 支持复杂吸顶View功能
  • 支持多类型吸顶功能
  • 支持开启和关闭吸顶功能
  • 支持设置吸顶偏移量
  • 支持兼容ItemDecoration和ItemAnimator
  • 支持RecyclerView数据变化和滚动到指定位置
  • 其它更多功能

如果你有任何问题,欢迎关注"字节小站"同名微信公众号,或者直接在评论区给留言