Skip to content

2、滚动到指定下标位置

林洵锋 edited this page Nov 13, 2023 · 5 revisions

建议搭配滚动视图的 cacheExtent 属性使用,将其赋予适当的值可避免不必要的翻页,分为以下几种情况:

  • 如果子部件是固定高度则使用 isFixedHeight 参数即可,不用设置 cacheExtent
  • 如果是详细页这类滚动视图,建议将 cacheExtent 设置为 double.maxFinite
  • 如果为子部件不等高的滚动视图,建议根据自身情况将 cacheExtent 设置为比较大且合理的值

2.1、基本使用

正常创建和使用 ScrollController 实例

ScrollController scrollController = ScrollController();

ListView _buildListView() {
  return ListView.separated(
    controller: scrollController,
    ...
  );
}

创建 ListObserverController 实例并将其传递给 ListViewObserver

ListObserverController observerController = ListObserverController(controller: scrollController);

ListViewObserver(
  controller: observerController,
  child: _buildListView(),
  ...
)

现在即可滚动到指定下标位置了

// 无动画滚动至下标位置
observerController.jumpTo(index: 1)

// 动画滚动至下标位置
observerController.animateTo(
  index: 1,
  duration: const Duration(milliseconds: 250),
  curve: Curves.ease,
);

如果你的视图是 CustomScrollView,其 slivers 中包含了 SliverListSliverGrid,这种情况也是支持的,只不过需要使用 SliverViewObserver,并在调用滚动方法时传入对应的 BuildContext 以区分对应的 Sliver

SliverViewObserver(
  controller: observerController,
  child: CustomScrollView(
    controller: scrollController,
    slivers: [
      _buildSliverListView1(),
      _buildSliverListView2(),
    ],
  ),
  sliverListContexts: () {
    return [
      if (_sliverViewCtx1 != null) _sliverViewCtx1!,
      if (_sliverViewCtx2 != null) _sliverViewCtx2!,
    ];
  },
  ...
)
observerController.animateTo(
  sliverContext: _sliverViewCtx2, // _sliverViewCtx1
  index: 10,
  duration: const Duration(milliseconds: 300),
  curve: Curves.easeInOut,
);

2.2、padding 参数

如果你的 ListViewGridView 有用到其 padding 参数时,也需要同步给该值!如:

ListView.separated(padding: _padding, ...);
GridView.builder(padding: _padding, ...);
observerController.jumpTo(index: 1, padding: _padding);

2.3、isFixedHeight 参数

如果列表子部件的高度是固定的,则建议使用 isFixedHeight 参数提升性能

该功能自从 1.17.0 版本后,开始支持 ListView/SliverListGridView/SliverGrid 以及其它由第三方构建的 ScrollView,之前的版本只支持 ListView/SliverList

// 无动画滚动至下标位置
observerController.jumpTo(index: 150, isFixedHeight: true)

// 动画滚动至下标位置
observerController.animateTo(
  index: 150, 
  isFixedHeight: true
  duration: const Duration(milliseconds: 250),
  curve: Curves.ease,
);

需要注意的是,如果你的滚动视图是由第三方库构建的,则可能需要使用 renderSliverType 参数去指明处理的方式,如果是 SliverListSliverGrid,则可以不指明。

renderSliverType 的类型:

enum ObserverRenderSliverType {
  list,
  grid,
}

如面对由 waterfall_flow 构建的瀑布流视图,在调用 jumpTo 方法时就需要指明 renderSliverTypeObserverRenderSliverType.grid

observerController.jumpTo(
  index: 13,
  isFixedHeight: true,
  renderSliverType: ObserverRenderSliverType.grid,
);

2.4、offset 参数

用于在滚动到指定下标位置时,设置整体的偏移量。

如在有 SliverAppBar 的场景下,其高度会随着 ScrollView 的滚动而变化,到达一定的偏移量后会固定高度悬浮于顶部,这时就需要使用到 offset 参数了。

SliverAppBar(
  key: appBarKey,
  pinned: true,
  expandedHeight: 200,
  flexibleSpace: FlexibleSpaceBar(
    title: const Text('AppBar'),
    background: Container(color: Colors.orange),
  ),
);
observerController.animateTo(
  ...
  offset: (offset) {
    // 根据目标偏移量 offset,计算出 SliverAppBar 的高度并返回
    // observerController 内部会根据该返回值做适当的偏移调整
    return ObserverUtils.calcPersistentHeaderExtent(
      key: appBarKey,
      offset: offset,
    );
  },
);

2.5、alignment 参数

alignment 参数用于指定你期望定位到子部件的对齐位置,该值需要在 [0.0, 1.0] 这个范围之间。默认为 0,比如:

  • alignment: 0 : 滚动到子部件的顶部位置
  • alignment: 0.5 : 滚动到子部件的中间位置
  • alignment: 1 : 滚动到子部件的尾部位置

2.6、cacheJumpIndexOffset 属性

为了性能考虑,在默认情况下,列表在滚动到指定位置时,ScrollController 会对子部件的信息进行缓存,便于下次直接使用。

但是对于子部件高度一直都是动态改变的场景下,这反而会造成不必要的麻烦,所以这时可以通过对 cacheJumpIndexOffset 属性设置为 false 来关闭这一缓存功能。

2.7、clearIndexOffsetCache 方法

如果你想保留滚动的缓存功能,并且只想在特定情况下去清除缓存,则可以使用 clearIndexOffsetCache 方法。

/// Clear the offset cache that jumping to a specified index location.
clearIndexOffsetCache(BuildContext? sliverContext) {
  ...
}

其形参 sliverContext 只有在你自行管理 ScrollViewBuildContext 时才需要传递。

2.8、初始下标位置

  • 方式一: initialIndex

最简单的使用方式,直接指定下标即可

observerController = ListObserverController(controller: scrollController)
  ..initialIndex = 10;
  • 方式二: initialIndexModel

定制初始下标位置的配置,各参数说明请看该节的最后部分

observerController = ListObserverController(controller: scrollController)
  ..initialIndexModel = ObserverIndexPositionModel(
    index: 10,
    isFixedHeight = true,
    alignment = 0.5,
  );
  • 方式三: initialIndexModelBlock

回调内返回 ObserverIndexPositionModel 对象, 适用于在一些参数无法一开始就可以确定的场景下使用,如 sliverContext

observerController = SliverObserverController(controller: scrollController)
  ..initialIndexModelBlock = () {
    return ObserverIndexPositionModel(
      index: 6,
      sliverContext: _sliverListCtx,
      offset: calcPersistentHeaderExtent,
    );
  };

ObserverIndexPositionModel 的定义:

ObserverIndexPositionModel({
  required this.index,
  this.sliverContext,
  this.isFixedHeight = false,
  this.alignment = 0,
  this.offset,
  this.padding = EdgeInsets.zero,
});
属性 类型 描述
index int 初始下标
sliverContext BuildContext 滚动视图的 BuildContext
isFixedHeight bool 如果列表子部件的高度是固定的,则建议使用 isFixedHeight 参数提升性能,默认为 false
alignment double 指定你期望定位到子部件的对齐位置,该值需要在 [0.0, 1.0] 这个范围之间。默认为 0
offset double Function(double targetOffset) 用于在滚动到指定下标位置时,设置整体的偏移量
padding EdgeInsets 当你的 ListViewGridView 有用到 padding 参数时,也需要同步给该值,其实情况则不需要

2.9、滚动状态

1.18.0 版本开始,新增了一些滚动状态,方便监听,注意,只有在调用了 ObserverControllerjumpToanimateTo 方法后才会发出。

通知 描述
ObserverScrollStartNotification 开始执行滚动任务,当前还未找到目标 item
ObserverScrollInterruptionNotification 滚动任务被中断,如:传入了不存在的下标,没有 ScrollController
ObserverScrollDecisionNotification 滚动任务决策完毕,当前找到了目标 item
ObserverScrollEndNotification 滚动任务正常执行完毕

以上的通知均继承自 ObserverScrollNotification,可以做为 NotificationListener 所需的泛型。

通知顺序: ObserverScrollStartNotification -> ObserverScrollDecisionNotification -> ObserverScrollEndNotification.

ObserverScrollInterruptionNotification 不参与上述顺序,在滚动条件不满足时便会发出,发出后滚动任务结束,不会再发出其它通知!

NotificationListener<ObserverScrollNotification>(
  child: widget,
  onNotification: (notification) {
    if (notification is ObserverScrollStartNotification) {
      
    } else if (notification is ObserverScrollInterruptionNotification) {
      
    } else if (notification is ObserverScrollDecisionNotification) {
      
    } else if (notification is ObserverScrollEndNotification) {
      
    }
    return true;
  },
);

jumpToanimateTo 方法也是在 1.18.0 版本才正式支持 awaitawait 后的结果有两种可能:

  1. 中断(同 ObserverScrollInterruptionNotification
  2. 正常结束(同 ObserverScrollEndNotification