【Flutter】用Flutter实现京东首页吸顶Tab效果,竟如此简单?—— Flutter的NestedScrollView分析

3,624 阅读5分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。

前言

随着春节长假的临近,公司项目也逐渐进入封网阶段,总之千言万语最后都汇成一句话:

E29697DCEB63117023FCB1847ECEDF6B.jpg

接下来应该有不少时间瞎搞了,但愿~

回正题,这次要说的内容正如标题所述,京东首页的吸顶效果;

在Android原生上,这貌似是一个有些麻烦的问题:

正如这篇文章所述,最主要的问题是嵌套的滑动事件无法实现两者互通:

仿京东、淘宝首页,通过两层嵌套的RecyclerView实现tab的吸顶效果

在Android中,手势在不同View之间的传递,如果不像ViewPager那样自己处理的话,就是基于 ViewParentCompat以及NestedScrollChild和NestedScrollParent那套,说白了就是一层一层的View分发传递;由此带来的问题就是,如果在两层嵌套滑动的View之间插入了一层View,这个View不支持嵌套滑动摆烂的话,就没人接着分发事件,自然无法实现嵌套滑动;

当然这只是其中一种解决方案,如果用 CoordinatorLayout 也同样可以实现这个效果;不过仍然要自定义来解决获知child中哪个是要嵌套滑动的View的问题;例如这个:

PersistentCoordinatorLayout

总之没法一步到位;

而在Flutter上,就没有上面这个问题:

如果用Flutter来做,非常简单,NestedScrollView本身就有这种隔着祖宗十八代也能找到亲戚关系的能力,并且也基于此实现了事件分发传递:换句话说,完全不需要自己处理,NestedScrollView本身就可以实现;

那么带着问题来看一下Flutter 的 NestedScrollView 是如何实现的:

1、NestedScrollView怎么就能做到隔着数层仍能联系起来父类和子类的?

首先先确定一下基础,NestedScrollView的本质还是Scrollable,可以说所有可滑动的Widget都是Scrollable;

那么对Scrollable做分析,自然该关注的地方就是其Position和ScrollController,而 NestedScrollView 本身跟 ListView 差距其实并不大,流程还是那一套;

但是在给Scrollable的controller这块,NestedScrollView 自己实现了 ScrollController 并赋予了自己本身的 Scrollable 并通过 child 的 PrimaryScrollController ,将另一个构造出来的ScrollController 赋予了 child (所以如果给子ListView设置了Controller,NestedScrollView就不会支持嵌套滑动的处理,原因就在这);

image.png

image.png

而之后的ListView ,就可以通过 PrimaryScrollController 获取到这个 scrollController ,并设置给Scrollable ;

image.png

这样,NestedScrollView,既可以操作自己的Scrollable,也可以操作子代中对应要控制的Scrollable了;掌握了ScrollController,跟掌握Scrooable没有太大区别;

同时,由于这两个ScrollController都是 _NestedScrollController ,并由 _NestedScrollCoordinator 这个协调者类统一创建;在创建的时候,这个协调者也将自身传进去,这样,无论是父Scrollable,子Scrollable,还是这个 Coordinator 都能互相获取得知;

2、光联系起来也没用,NestedScrollView是如何处理手势操作的?

在大体流程上,NestedScrollView 跟之前提到的Android中的手势操作处理方式,其实没有太大差别;

按步骤来一步步看下:

屏蔽子Scrollable的手势处理

在Android 中,如果RecyclerView中有嵌套,其外部的RecyclerView会完全接管手势事件;在Flutter中,也差不多是这个思路,不过实现方式稍有不同;

在之前的ListView分析系列文章中可以得知:手势通知器是通过调用setCanDrag(true) 才正式激活应用的;而调用这个setCanDrag方法的,还是 ScrollPosition ;

在NestedScrollView中也是同理,不过由于其使用的是自己新建的 _NestedScrollController ,其 ScrollPosition 也是用的 _NestedScrollPosition ;

来到其调用setCanDrag方法的地方,可以看到无论是哪个Scrollable被调用了 applyNewDimensions ,都不会调用子Scrollable对应position的 applyNewDimensions,只会对外层的outPosition调用 setCanDrag ;

这样,子Scrollable永远不会调用setCanDrag,自然无法激活手势通知器,也无法参与手势竞争了,手势就会完全有外层的Scrollable来接管;

image.png

协调者中的updateCandarg方法: image.png

NestedScrollPosition中的updateCanDrag: image.png

由此可以看出,在Flutter中,其实没有手势拦截这个概念,或者说,要做手势拦截,通过的方式是直接不分发手势,或者让子View不参与手势处理不加到手势竞争器中,再其次就是让子View在手势竞争中失败;虽然思路和最终结果都一样,但在流程上,跟Android的手势事件流处理模型还是有不同的;

手势分发处理

上面取消了子 Scrollable 的手势操作权限,将所有手势事件交给父Scrollable处理;而父Scrollable所做的事,就是更新自己的pixel的时候,处理下过度滑动的部分,并传递到子Scrollable;

为此,还将所有activity都同步到子Scrollable;换句话说就是父Scrollable 开始 DragActivity的时候,子Scrollable也是一样;在activity层面完全同步父Scrollable;

具体跟流程来看下:

首先,NestedScrollPosition会将这些操作先交给协调者做个统一处理分发:

image.png

在这里,将delegate设置为coordinator自己,代表以后ScrollActivity的计算处理对象是协调者这个类:

image.png

以Drag事件为例,在DragActivity中,响应手势更新的地方是update,会将手势信息交给delegate去处理更新,在NestedScrollView中,也就是上面提到的协调者;

它会对更新的手势操作做个统一的分发处理:

up方向中的处理逻辑: image.png

down方向的的处理逻辑 image.png

简单的来说就是:

UP方向的时候的处理逻辑:

如果 子Scrollable处于过度滑动,导致pixel是负数,那么先由他处理,复位到0的时候再由外层的父Scrollable处理,如果父Scrollable消费不了手势过度滑动的话,那么就再由子Scrollable处理;

Down方向的时候的处理逻辑:

如果 有float 模式,那么先由外层处理;之后再由 子Scrollable消费;消费不了的部分再交给外层;最后仍消费不了,那再传给子 Scrollable;

结语

这篇对NestedScrollView 是如何实现跨Widget协调嵌套滑动做了个简单的分析,按照国际惯例,这时候应该有个效果展示:

demo.webp

不得不说,小部件模式的思路,有时候还是真的挺有用