【Flutter】解决多级列表嵌套滑动冲突方案选择

4,786 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

前言

需求背景是在原频道页面实现二级Tab功能,二级列表页面上滑后TabBar是可置顶悬浮在顶部,底部切换TabBar时每个二级列表页面记录自身滑动偏移量,并且该改造不影响到原有布局框架结构。原框架结构实现为CustomScrollView[SliverRefreshWidget + SliverList],SliverRefreshWidget是DIY自定义刷新组件支持下拉刷新和上二楼功能。

改造遇到的问题

采用原CustomScrollView结构

增加二级Tab框架结构大概是这样的:CustomScrollView[SliverRefreshWidget + TabBar + TabView[ListViews]]。

PS: CustomScrollView的特殊性当然直接嵌套TabBar和TabView是不可以的,可以增加SliverToBoxAdapter来支持非Sliver族组件。

但因为ListView也是滑动列表和CustomScrollView是存在冲突关系,必须给ListView做一个高度限制因此调整框架结构为:CustomScrollView[SliverRefreshWidget + TabBar + SliverFillRemaining[TabView[ListViews]]]。

  1. 若不为ListView创建设置ScrollController,ListView列表无法上滑展示更多内容。
  2. 但若为ListView创建设置ScrollController,滑动ListView时无法将顶部内容收起。

以上两种情况均不会影响自定义SliverRefreshWidget 刷新功能,但会影响整体页面滑动操作,顶部和底部列表滑动存在冲突。

采用NestedScrollView结构

NestedScrollView结构大致上和CustomScrollView结构类似会是这样的:NestedScrollView[SliverRefreshWidget + TabBar + TabView[ListViews]]。

  1. 若不为ListView创建设置ScrollController,自定义SliverRefreshWidget功能失效。
  2. 若为ListView创建设置ScrollController,顶部和底部列表滑动同样会存在冲突问题在滑动ListView时无法满足先将顶部视图收起。

均上两种方案都无法直接满足需求功能实现,因此必须通过自定义列表结构来满足需求能力。

CustomScrollView底部List设置不可滑动CustomScrollView
当List滑动到底部后下拉滑动不能再透出顶部视图当滑动到指定高度后ListView底部内容无法滑动
a59d8a633eb47836dc27c57b5b591aeb.gif97bdb64fd12596eba20ee5486a9ba25e.gif

以下两种方式都无法触发下拉刷新(因为下拉都内部Postion消费了

NestedScrollView的List设置不可滑动NestedScrollView的List设置可滑动
当滑动到指定高度后ListView底部内容无法滑动当List滑动到底部后下拉滑动不能再透出顶部视图
1c3341fe1703cccb752868233d19814f.gif17ac521505cb7e12e526aaae75426f32.gif

方案设计

偏移量记录法

保持原始框架结构不变:CustomScrollView[SliverRefreshWidget + TabBar + SliverList]。但增加每次滑动的整体偏移量的值,当切换Tab后重新记录当前选中Tab滑动偏移量。再切换Tab时重新为ScrollerController设置切换后Tab的偏移量值。

该方案起初觉得满足需求,但在实际使用中会发现存在设计缺陷。例如在第一个Tab下滑动停留在顶部视图透出的情况,再切换到第二个Tab(偏移量是在顶部视图不透出情况)会导致顶部视图被隐藏了。实际需求应该是切换Tab只改变底部列表的偏移量顶部透出情况不变。因此该方案采用取巧形式不能完全满足需求。

CustomeScrollView自定义协调器

该方案是Flutter 实现类似美团外卖店铺页面滑动效果所实现的。其核心是参考了NestedScrollView实现方式内部实现了_NestedScrollCoordinator协调器来协助顶部视图和底部视图协同滑动操作。

其核心部分是会通过滑动监听来判断顶部视图展开还是收起状态。当顶部视图还是展开就执行收起动画从而让用户后续操作是滑动底部列表。

代码实现来自:juejin.cn/post/684490…

void onPointerUp(PointerUpEvent event) {
  final double _pagePixels = _pageScrollPosition.pixels;
  if (0.0 < _pagePixels && _pagePixels < _pageInitialOffset) {
    if (pageExpand == PageExpand.NotExpand &&
        _pageInitialOffset - _pagePixels > _scrollRedundancy) {
      _pageScrollPosition
          .animateTo(0.0,
              duration: const Duration(milliseconds: 400), curve: Curves.ease)
          .then((value) => pageExpand = PageExpand.Expanded);
    } else {
      pageExpand = PageExpand.Expanding;
      _pageScrollPosition
          .animateTo(_pageInitialOffset,
              duration: const Duration(milliseconds: 400), curve: Curves.ease)
          .then((value) => pageExpand = PageExpand.NotExpand);
    }
  }
}

但该方案还是无法完全满足交互需求,其滑动监听会直接通过执行动画强行收起或展开顶部视图从而没有顶部手势滑动操作了,因此该方案只能作为取舍方案做为备选。

CustomerNestedScrollView支持下拉刷新

NestedScrollView是在CustomeScrollView基础上实现支持头部和顶部联动的滑动组件,通过NestedScrollView源码会发现内部有两个ScrollController协同作用,一个是outController负责顶部组件布局滑动监听,一个是InnerController负责底部组件布局滑动监听。但当NestedScrollView滑动到整个Offset为0时是由底部布局来监听滑动事件,因此就无法支持由自定义SliverRefreshWidget来实现下拉刷新操作。\

开源库custom_nested_scroll_view满足了本次开发需求,它支持顶部视图下拉时监听TopOverscroll。

它的实现原理是改造_NestedScrollCoordinator协调器中applyUserOffset方法允许_outerPosition支持超出滑动;另外是修改调整unnestOffset, nestOffset, _getMetrics 这几个方法调整_outerPosition_innerPosition对于协调器的作用形式。

最终效果

🚀参考Demo看这里🚀

Video_20220821_031805_455.gif

参考