[译]对design库中AppBarLayout嵌套滚动问题的修复

材料设计库的作者Chris Banes日前修复了嵌套滚动一直一来都存在的bug,下面是译文,原文在https://chris.banes.me/2017/06/09/carry-on-scrolling/ 

nested-scrolling.gif

如果你使用过 Material Design 组件 库(就是以前的 design support library),特别是类似 AppBarLayout这样的滚动组件,你很可能注意到了有时滑动会诡异的停止。上图是存在版本和修复后版本的对比。

可以看到,滚动手势在上滚的时候是连贯的,但是在v25.x,当用户下滑的时候流畅性是被打断了的。其实你想要的效果是app bar完全下拉,就如 v26.x 版本中那样。

造成这个问题的主要原因是被称之为嵌套滚动的一套Android API存在缺陷。它们是在Android Lollipop (API 21)中被添加进去的,是为了实现Material Design ‘Scrolling techniques’ 文档中所说的滚动手势而做的。这个API的主要目的是让任意父view都可以监听和截断滚动事件。

这个功能在支持库26.0.0-beta2中得到了加强,对嵌套滚动的API做了些改进。所以如果你对此感兴趣,可以更新尝试一下。

下面的内容中我们将深入讲解具体是如何实现的。


嵌套滚动回顾

让我们快速过一遍目前的嵌套滚动API。有两个主要的部分,滚动和划动(flinging),但是从滚动这条线来思考会容易些。这里约定,后面凡事涉及到“view”的表述,都是指用户触摸的滚动视图(如ScrollView, RecyclerView, 等)。

? User touches screen. 当触发 ACTION_DOWN 事件的时候,view调用每一个父view的startNestedScroll(),直到其中一个返回true。返回true表示那个parent对这个滚动感兴趣。如果没有parent返回true,那么嵌套滚动被取消,view执行它自己的操作。后面我们都假设有一个parent返回了true。

? User moves finger. 当 ACTION_MOVE 事件触发的时候,view将调用dispatchNestedPreScroll() 把事件发送给parent,让它消费部分/全部(或者不消费)用户手指滑动的距离。如果parent没有消费完所有的移动,view将自己接着消费并发送 dispatchNestedScroll()告知消费了多少。

? User lifts finger. 当触发 ACTION_UP 的时候,view计算是否需要继续移动。如果余下的速度足够大,它将调用 dispatchNestedPreFling() 让parent继续消费velocity。如果parent返回true并且消费了它,view的工作就完成了。否则view将开始划动并立即调用 dispatchNestedFling()。view将立即调用 stopNestedScroll() 来将嵌套滚动标记为结束,即使view自己实际上还处于fling中。

最后一句就是问题的关键所在。parent很可能并不想一下子消费整个fling手势,而是像响应一个scroll一样去处理。不幸的是目前的API并不支持。

Nested Scrolling++

在支持库的26.0.0-beta2版本中,我们发布了一些对嵌套滚动API的改进,以帮助修复这个问题。

如果你看了新的API,就知道我们所做的事情其实就是在现有的方法之上添加了一个新的参数 type 。type参数告诉你是什么类型的输入在驱动scroll事件,目前可以是这两种选项之一:ViewCompat.TYPE_TOUCH 和ViewCompat.TYPE_NON_TOUCH.

当事件直接来自用户触摸屏幕的时候,传递TYPE_TOUCH 。当用户并没有触摸屏幕的时候,传递 TYPE_NON_TOUCH。当前 TYPE_NON_TOUCH只用在fling这个场景之下,但是未来耶可能会用在其它场景,比如按键导航。

为了维护API的兼容性,我们默认为TYPE_TOUCH 。比如,这里是 startNestedScroll() 的定义:

@Override public void startNestedScroll(int axes) {     startNestedScroll(axes, ViewCompat.TYPE_TOUCH); } @Override public void startNestedScroll(int axes, int type) {     // Do something }

实践

有了这些新方法当然好了,但是如何实际中使用它们呢?其实绝大多少情况下并不需要管 type 参数。你可以无视事件触发的类型,不管三七二十一做同样的处理就可以了。

那么为什么还要添加这个参数呢?记得最开始我们提出的那个问题吗?为了修复那个问题我们必须改变在fling的时候API的一些行为。

让我们过一遍跟前面相同的过程:

? User touches screen.跟上面一模一样,但是这次传入了TYPE_TOUCH 参数:startNestedScroll(TYPE_TOUCH)。

? User moves finger. 同样的过程.

? User lifts finger. 同上. stopNestedScroll(TYPE_TOUCH) 被调用同时 ‘touch’ 嵌套滚动结束。 

view开始fling。如果view自己开始了fling,我们将开始新的一轮嵌套滚动,不过这次是TYPE_NON_TOUCH类型。从startNestedScroll(TYPE_NON_TOUCH),到dispatchNestedPreScroll(TYPE_NON_TOUCH) + dispatchNestedScroll(TYPE_NON_TOUCH), 最后是 stopNestedScroll(TYPE_NON_TOUCH)。这次所有事情都是view的fling (通常是一个 Scroller)驱动的,而不是触摸事件。

很好,不过我记得你说过可以忽略type参数? 

是的。 type 参数主要是用来维护现有的api以及它运行时的特性。99%%的情况下两种类型的事件都是一样的,而AppBarLayout中也正是忽略了type。

如何使用它 ?

如果你只是使用 AppBarLayout 以及它的伙伴,而不是直接使用嵌套滚动API,你只需更新到26.0.0-beta2就行了。

但是如果你要直接使用嵌套滚动API,就需要做一些改变了。新的方法添加到了NestedScrollingChild2 和 NestedScrollingParent2中,所以只需替换相应的接口。同时它们也被添加到了 CoordinatorLayout.Behavior ,所以你只需在相关的方法的末尾添加type就可以工作了。

最后

下面是另一个稍微慢点的fling视频:

Untitled.gif

相关文章