【Flutter】自定义ListView开发记录(八)—— 支持嵌套滑动(二)

2,908 阅读5分钟

前言

在之前的文章中,以最简单的方式实现了一个简易版的嵌套滑动,说白了,其实现方式完完全全依托于Flutter自带的手势竞争,并没有做一些精细化的处理,现在就来解决下这个问题;

但是毕竟效果肯定不对,所以思来想去,最后还是参考了下Android这块的嵌套滑动机制,设计不会,但是我会抄啊~

PS:先对上一篇文章末尾提到的那个bug做个补充说明,其实出现那个问题的主要原因还是PagePhysics的引入导致的,由于外面的可滑动View在手势竞争中失败了,触发了cancel自动复位导致的,所以其实来说,这块不算什么问题,正常逻辑就该这样;

PPS:后来发现,好像android原生上就存在ViewPager嵌套ViewPager,快速滑动会跳过中间部分Item的情况……现在我有点懵了,这个嵌套滑动的问题算不算bug~~~算了,即使不算bug的话,修复也简单,把那帮activity的 shouldIgnorePointer 改回去就完事了

设计

首先是Android 的 RecyclerView 嵌套滑动机制的处理,这块说白了,参考NestedScrollParent和NestedScrollChild这两个整就完事了,其实呢,这块的具体实现逻辑,像我之前的方案和extend_tab这种已经实现了,所做的事就是抽离封装,没啥好说的;

不过引入了这个PagePhysics效果就是另一回事了,以Android为例,ViewPager嵌套ViewPager的效果是这样的:

而像最新的ViewPager2就没这种效果,原因嘛就是ViewPager2是基于RecyclerView实现的,使用的是上面的NestedScroll机制,而ViewPager仅仅是一个单纯的ViewGroup,并没用NestedScroll那套,具体细节就不再赘述,这里简单说下ViewPager实现这个效果的方式:

这块的核心逻辑其实不复杂,当触发手势事件的时候:

  • 如果当前没处在触发嵌套滑动的情况下:
    • 事件拦截这块首先判断一下子ViewPager是否可以接着滑动,如果不可以就拦截掉事件自己处理;
    • 如果可以滑动,就不拦截事件,而子ViewPager接受到事件后,就会请求父ViewPager不要再拦截事件;
  • 如果已经触发了嵌套滑动,那么所有事件均由父ViewPager来处理

回到Flutter层面,如果要将ViewPager这种效果整合进来,其实就仅仅需要解决一个问题,对标上面的第二种情况,如何实现让父ListView的手势竞争能竞争过子ListView?

由于flutter的手势竞争默认情况由最先加入的,或者说最底层的Widget取得胜利(除非最先的那个拒绝胜利);所以一般情况下,父ListView是不会获得手势事件的;而这次要让父ListView能赢得胜利,我设想中的实现方式有这么几种:

  • 只要子ListView拒绝胜利,那么自然父ListView获胜,所以可以考虑加入让子ListView竞争失败的方法;
  • 还是子ListView的 controller 接管控制所有事件,但是子ListView的Controller通过加入tag的方式来

分析

方案一

方案一这块,说白了就是让父ListView所持有的GestureArneaMember 获得胜利,在Scrollable中,持有的GestureArneaMember 其实就是横竖两种方向的 DragGestureRecognizer ,即 VerticalDragGestureRecognizer 或者 HorizontalDragGestureRecognizer ,但是根据研究,好像DragGestureRecognizer并不支持直接手势插队,所以或许需要自定义手势竞争器?

不过改动的话,应该也好改,我这想到两种方式:

  • 以DragGestureRecognizer为例,其判断竞争胜利的方法就是 _hasSufficientGlobalDistanceToAccept 方法;

    说白了,继承一个DragGestureRecognizer,给其引入id或者tag这种可以区分身份的东西,将所持有的 position 带过去 ,重写其 handleEvent 方法,在其用于判断是否竞争胜利的 _hasSufficientGlobalDistanceToAccept 中加入条件即可;

  • 改变顺序,将原来最底层优先改为最顶层优先,不过这意味着PointerRouter这块也要改动……虽然我感觉这是最合理的方式,但是这改动好像涉及到底层,毕竟也没提供修改PointerRouter的修改方式,所以当我没说;

不过无论哪种方式都要自定义手势竞争器,这意味着要改动Scrollable……毕竟原本的 Scrollable 并不暴露手势竞争器;而这个手势竞争器和其涉及到的部分,基本就是 Scrollable的全部;

方案二

这种方案应该是改动涉及部分最小的,实现方式也就按照之前的方案一样,只不过加个tag来做拦截;但是我感觉不太合理,毕竟从操作的角度来说,子ListView滑不了,就应该由父ListView来获取手势胜利,毕竟我滑动的就是父ListView嘛,现在仍由子ListView来管理,我感觉多嵌套几层就会很混乱;

实现

最终呢,出于学习思路考虑,还是使用方案一,毕竟看上去高大上一点点,最重要的是,符合我认为合理的逻辑;

不过估计跟ListView的嵌套效果结合起来,还是要采用方案二的方式~

回正题,方案一的实现也不复杂,主要是一堆私有方法搞的挺麻烦的;

首先在构造Scrollable前,获取当前context中的父ListView持有的position,并将自己和父ListView的position传给 GestureRecognizer :

image.png

image.png

而 GestureReconizer 所做的判断也很简单,在胜利条件判断中加入这条:仅且仅当存在父ListView,而且自身处于边界不可再滑动的情况下返回false拒绝胜利,自然手势就轮到了父ListView去持有:

image.png

image.png

现在再来看下效果:

QQ20211223-172928-HD.gif

现在跟Android那块一样了,ViewPager效果下,只有滑动到边界才允许将事件给父ListView,否则就内部消费,触发OverScroll之类的,事件分发这块也是成功由父ListView接管:

QQ20211223-174126-HD.gif