专栏名称: 郭霖
Android技术分享平台,每天都有优质技术文章推送。你还可以向公众号投稿,将自己总结的技术心得分享给大家。
目录
相关文章推荐
鸿洋  ·  Google 为何设计了如此难用的 ... ·  昨天  
stormzhang  ·  时间不多了 ·  2 天前  
鸿洋  ·  Android14 WMS/AMS ... ·  2 天前  
鸿洋  ·  一款高效的HarmonyOS工具包 ·  3 天前  
鸿洋  ·  Android主线程锁监控的一种方案 ·  1 周前  
51好读  ›  专栏  ›  郭霖

Material Design中的CoordinatorLayout和Behavior详解

郭霖  · 公众号  · android  · 2016-09-21 07:30

正文

今日科技快讯


919是乐视一年一度的狂欢节,就在919狂欢节晚会上,乐视汽车宣布已经完成了10.8亿美元的首轮融资。第一轮融资就如此大手笔,看来乐视汽车未来的路会好走许多,至少短期内是不差钱了。


关于国行版三星Note 7爆炸事件又有了新的进展,三星发表公告称,国行版的Note 7电池不存在问题,手机损坏是由于外部加热所导致的。加上之前有网友爆料称这次事件有友商恶意抹黑的可能,真相变得更加扑朔迷离。不过,不管是在国内还是国外,三星都在经历一个前所未有的信任危机。


作者简介


本篇来自 leon 的投稿,阐述了 嵌套滚动机制 以及 CoordinatorLayout.Behavior ,文章讲得很清楚了,我就不多说了,大家赶快进入正文吧~


leon 的博客地址:

http://ltlovezh.com


嵌套滚动机制


本文将介绍下 CoordinatorLayout 是如何协调 子View 间关系的。在介绍 CoordinatorLayout 之前,首先需要了解下 嵌套滚动机制NestedScrolling)。


所谓嵌套滚动其实就是界面布局中包含 一个可滚动的列表 一个不可滚动的View,这样在滚动列表时,首先将不可滚动View移出屏幕或移进屏幕,待不可滚动View固定时,才会继续滚动滚动列表的内容。


为什么会有嵌套滚动机制?


之前我们处理 Touch事件 时,主要通过重写 View dispatchTouchEvent、onInterceptTouchEvent onTouchEvent 等方法处理滚动事件。但这种事件处理方式有一个痛点:


  • Touch事件 要么被 父View 处理,要么被 子View 处理,很难在两者之间协调处理。


也就是说:一旦 子View 决定处理 Touch事件,那么事件就会一直下发到 子View,即使 子View 不想处理中间的某个 Touch事件(返回false),那么 父View 也没办法接着处理这个 Touch事件 了;除非 父View 拦截中间的某个 Touch事件 自己处理,但是一旦拦截了 Touch事件,那么后续的 Touch事件 将永远不会下发到 子View 了。


针对这种问题,Android 提供了 NestedScrolling 机制,实现嵌套滚动机制主要依赖四个类:


  1. NestedScrollingChild

  2. NestedScrollingParent

  3. NestedScrollingChildHelper

  4. NestedScrollingParentHelper


一般情况下,滚动列表需要实现 NestedScrollingChild 接口,以支持将滚动事件分发给 父ViewGroup,相应的,父ViewGroup 需要实现 NestedScrollingParent 接口,以支持将滚动事件进一步的分发给各个 子View;而下面的两个类则是进行嵌套滚动的辅助类。


一般实现 NestedScrollingChild 接口的滚动列表会把滚动事件委托给 NestedScrollingChildHelper 辅助类来处理。例如:RecyclerView 实现了 NestedScrollingChild 接口,它内部就会把滚动相关事件委托给 NestedScrollingChildHelper 对象来处理,如下所示:




NestedScrollingChild 的方法有很多,更多的可参见源码,上述摘录的是和嵌套滚动机制相关的四个方法。


当我们滚动 RecyclerView 时,RecyclerView 首先会通过 startNestedScroll 方法通知 父ViewGroup(“我马上要滚动了,是否有兄弟节点要一起滚动?”),父ViewGroup 会进一步把滚动事件分发给所有 子View(实际是分发给和 子View绑定的Behavior),感兴趣的 子View 会特别关注,即 Behavior.onStartNestedScroll 方法返回 true


针对这个流程,我们看下代码上的实现,RecyclerView 会在 Down事件 时调用 startNestedScroll 方法,我们看下 NestedScrollingChildHelper.startNestedScroll 方法的实现:




上述方法会找到能够协调处理滚动事件的 父ViewGroup,然后调用它的 onStartNestedScroll 方法,因为 CoordinatorLayout 实现了 NestedScrollingParent 接口,所以我们看下 CoordinatorLayout.onStartNestedScroll 方法:




上述方法会遍历每一个 子View,询问它们是否对滚动列表的滚动事件感兴趣,若 Behavior.onStartNestedScroll 方法返回 true,则表示感兴趣,那么滚动列表后续的滚动事件都会分发到该 子View的Behavior。而把 Behavior 绑定到 View 的方法有两种:


  • 在布局文件中通过 app:layout_behavior 属性指定。

  • 在自定义View中通过 @CoordinatorLayout.DefaultBehavior 注解指定,就像 AppBarLayout 那样。


因此,我们可以在自定义的 Behavior.onStartNestedScroll 方法中根据实际情况决定是否对滚动事件感兴趣。


OK,假设 CoordinatorLayout 的某个 子ViewRecyclerView 的滚动事件感兴趣,接下来 RecyclerView 就会把用户的滚动事件源源不断的分发给之前找到的 父ViewGroup,然后 父ViewGroup 则进一步分发给感兴趣的 子View。等到感兴趣的 子View 处理完滚动事件后,若用户的滚动距离没有被消费完,那么 RecyclerView 才有机会处理滚动事件,例如:用户一次性滚动了 10px,其中 某个View 消费了 8px,那么 RecyclerView 就只能滚动 2px 了。


针对这个流程,我们看下代码实现,RecyclerView 会在 Move事件 时,计算出滚动距离,然后通过 dispatchNestedPreScroll 方法进行分发,我们看下 NestedScrollingChildHelper.dispatchNestedPreScroll 方法的实现:




该方法的 第3个参数 是一个长度为2的一维数组,用于记录 父ViewGroup(其实是父ViewGroup的子View)消费的滚动长度,若滚动距离没有用完,则滚动列表处理剩下的滚动距离;第4个参数 也是一个长度为2的一维数组,用于记录滚动列表本身的偏移量,该参数用于修复用户 Touch事件 的坐标,以保证下一次滚动距离的正确性。这些处理逻辑可参见 RecyclerView Move事件 的代码,此处不再贴代码了。


然后,父ViewGroup 就会把滚动事件分发给感兴趣的 子View,因为 CoordinatorLayout 实现了 NestedScrollingParent 接口,所以我们看下 CoordinatorLayout.onNestedPreScroll 方法:




CoordinatorLayout 的处理很简单,把滚动事件分发给各个 子View Behavior.onNestedPreScroll 方法处理,并计算出最终消费的滚动距离。


因此,我们可以在自定义的 Behavior.onNestedPreScroll 方法中处理 子View 的滚动事件,然后根据实际情况填写消费的滚动距离。


OK,假设 RecyclerView 的滚动距离没有被 CoordinatorLayout 消费完,那么接下来 RecyclerView 应该处理这些滚动事件了。在 RecyclerView onTouchEvent 方法中会调用 scrollByInternal 处理内容滚动,关键代码如下所示:




如上所示,RecyclerView 通过 LayoutManager 处理了剩余的滚动距离,然后计算出对剩余滚动量的消费情况,通过 dispatchNestedScroll 方法继续分发给CoordinatorLayout,而 CoordinatorLayout 则通过 onNestedScroll 方法分发给感兴趣的 子View Behavior 处理。这部分的代码逻辑和 onNestedPreScroll 类似,就不贴出了,感兴趣的可以直接看源码。


因此,我们可以在自定义的 Behavior.onNestedScroll 方法中检测到滚动距离的最终消费情况。


OK,现在假设用户结束滚动操作了,即应该结束一系列的滚动事件了,RecyclerView 会在 UP事件 中调用 stopNestedScroll 方法,该方法和上面介绍的三个方法类似,都会先把事件分发给 父ViewGroup,然后 父ViewGroup 再把事件分到各个 子View,最终触发 子View Behavior.onStopNestedScroll 方法,感兴趣可以可接看源码,此处不再贴出。


因此,我们可以在自定义的 Behavior.onStopNestedScroll 方法中检测到滚动事件的结束。


OK,整个嵌套滚动机制就介绍完了,可见跟我们直接打交道的就是 CoordinatorLayout.Behavior 类了,通过重写该类中的方法,我们不仅可以监听滚动列表的滚动事件,还可以做很多其他的事情,感兴趣的可以详细看下 Behavior 接口中的方法说明。


监听View之间的状态变化


这里我比较感兴趣的是通过 Behavior 监听 View 之间的状态变化,例如:位置、大小、背景色 等,要实现这种状态监听,需要重写 Behavior 的两个方法:


  • layoutDependsOn : 决定需要监听的View对象

  • onDependentViewChanged : 当监听对象的状态发生变化时,会回调该方法,我们可以根据监听对象的状态,变换自己View的状态。


首先,我们来看下这两个方法在 CoordinatorLayout 中是怎么被调用的?


经过代码摸索和测试,发现这两个方法基本都是在 CoordinatorLayout.dispatchOnDependentViewChanged 方法中被调用的,而该方法则会在 CoordinatorLayout 每次绘制之前被调用,核心代码如下所示:




从上述代码可知,每次重绘 CoordinatorLayout 之前,都会调用 dispatchOnDependentViewChanged 方法,好吧,该方法是核心部分,来看下代码:




如上所示,核心代码都添加了详细的注释,这里简单总结下:


1. 形成依赖关系的方法有两种:


  • 通过 app:layout_anchor 属性指定参照的View;

  • 通过 layoutDependsOn 方法判断


2. 若是通过第二种方式形成的依赖关系,那么只有当 被依赖View 的 Rect区域 发生变化时,所有依赖于 该View 的 其他View 才会收到 onDependentViewChanged 回调。


3. 若在 Behavior.onDependentViewChanged 方法中根据 所依赖View 的状态修改了 当前View 的位置,那么也应该重写 Behavior 的 onLayoutChild,这样才能保持一致。


OK,这两个方法的实现原理已经介绍完了。


实际案例


下面我们来看一个同时包含 嵌套滚动View间状态监听 的 Demo。


首先看一下效果图:



当向上滚动 TextView 时,首先会把 TextView 滚动出屏幕,然后才会滚动 RecyclerView 的内容;当向下滚动时,首先会把 TextView 滚动到屏幕内,然后才会滚动 RecyclerView 的内容;同时 TextView 的位置依赖于 Button 的位置,RecyclerView 的位置依赖于 TextView 的位置(保证 RecyclerView 不会被 TextView 遮盖住)。


实现上述效果的布局文按如下所示:




CoordinatorLayout 包含 3个子View,其中 RecyclerView 依赖于 TextViewTextView 依赖于 Button(通过 Behavior.layoutDependsOn 方法指定),RecyclerView 的滚动会带动 TextView 的滚动。


下面来看一下 RecyclerView Behavior,该 Behavior 仅仅实现了 layoutDependsOn onDependentViewChanged 方法,目的是根据 TextView 的位置,计算出 RecyclerView 的位置,这样才能保证 RecyclerView 的顶部靠着 TextView 的底部,而不被 TextView 盖住。代码如下所示:




然后来看一下 TextView MyBehavior,该 Behavior 不仅仅实现了依赖关系,同时还实现了嵌套滚动,代码如下所示:




TextView MyBehavior 的稍微复杂一些,主要是实现了跟着 RecyclerView 的滚动而滚动,同时又依赖于 Button 的位置决定 TextView 的最终位置。


OK,到此为止,简要介绍了 嵌套滚动机制CoordinatorLayout.Behavior 的使用方法,Behavior 的方法还有很多,感兴趣的可以多尝试下。


参考文章


Android嵌套滑动机制(NestedScrolling)

https://segmentfault.com/a/1190000002873657


探究Behavior的真实面目

http://www.tuicool.com/articles/uU7vqya


Android Support Design中CoordinatorLayout与Behaviors 初探

https://segmentfault.com/a/1190000002888109


更多



如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。


欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号:


推荐文章
stormzhang  ·  时间不多了
2 天前
鸿洋  ·  一款高效的HarmonyOS工具包
3 天前
少女兔  ·  2017男友宠爱程度对照表
7 年前
手艺门  ·  百年铜艺,鹿喜人间
7 年前