Skip to content

2、Scrolling to the specified index location

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

It should be used with the scrollView's cacheExtent property. Assigning an appropriate value to it can avoid unnecessary page turning. The recommendations are as follows:

  • If child widgets are fixed height in scrollView, use isFixedHeight instead of cacheExtent.
  • For scrollView such as detail page, it is recommended to set cacheExtent to double.maxFinite.
  • If child widgets are dynamic height in scrollView, it is recommended that setting cacheExtent to a large and reasonable value depending on your situation.

2.1、Basic usage

Create and use instance of ScrollController normally.

ScrollController scrollController = ScrollController();

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

Create an instance of ListObserverController pass it to ListViewObserver

ListObserverController observerController = ListObserverController(controller: scrollController);

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

Now you can scroll to the specified index position

// Jump to the specified index position without animation.
observerController.jumpTo(index: 1)

// Jump to the specified index position with animation.
observerController.animateTo(
  index: 1,
  duration: const Duration(milliseconds: 250),
  curve: Curves.ease,
);

If you use CustomScrollView and its slivers contain SliverList and SliverGrid, this is also supported, but you need to use SliverViewObserver, and pass the corresponding BuildContext to distinguish the corresponding Sliver when calling the scroll method.

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、Parameter padding

If your ListView or GridView uses its padding parameter, you need to sync that value as well! Such as:

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

2.3、Parameter isFixedHeight

If the height of a list child widget is fixed, it is recommended to use the 'isFixedHeight' parameter to improve performance.

This feature has supported ListView/SliverList, GridView/SliverGrid and other ScrollView built by third-party package since version 1.17.0. The previous version only supported ListView/SliverList.

// Jump to the specified index position without animation.
observerController.jumpTo(index: 150, isFixedHeight: true)

// Jump to the specified index position with animation.
observerController.animateTo(
  index: 150, 
  isFixedHeight: true
  duration: const Duration(milliseconds: 250),
  curve: Curves.ease,
);

It should be noted that if your ScrollView is built by third-party package, you may need to use the renderSliverType parameter to specify the processing method. If sliver is SliverList and SliverGrid, you do not need to specify it.

Type of renderSliverType:

enum ObserverRenderSliverType {
  list,
  grid,
}

For example, the waterfall flow view built by waterfall_flow needs to specify renderSliverType as ObserverRenderSliverType.grid when calling the jumpTo method.

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

2.4、Parameter offset

Used to set the whole scrollView offset when scrolling to a specified index.

For example, in the scene with SliverAppBar, its height will change with the scrolling of ScrollView. After reaching a certain offset, it will be suspended on the top with a fixed height, and then we must pass this fixed height to the offset parameter.

SliverAppBar(
  key: appBarKey,
  pinned: true,
  expandedHeight: 200,
  flexibleSpace: FlexibleSpaceBar(
    title: const Text('AppBar'),
    background: Container(color: Colors.orange),
  ),
);
observerController.animateTo(
  ...
  offset: (offset) {
    // The height of the SliverAppBar is calculated base on target offset and is returned in the current callback.
    // The observerController internally adjusts the appropriate offset based on the return value.
    return ObserverUtils.calcPersistentHeaderExtent(
      key: appBarKey,
      offset: offset,
    );
  },
);

2.5、Parameter alignment

The alignment specifies the desired position for the leading edge of the child widget. It must be a value in the range [0.0, 1.0]. Such as:

  • alignment: 0 : Scrolling to the top position of the child widget.
  • alignment: 0.5 : Scrolling to the middle position of the child widget.
  • alignment: 1 : Scrolling to the tail position of the child widget.

2.6、Property cacheJumpIndexOffset

For performance reasons, ScrollController will caches the child's information by default when the listView jump or animate to the specified location, so that it can be used next time directly.

However, in scence where the height of child widget is always changing dynamically, this will cause unnecessary trouble, so you can turn this off by setting the cacheJumpIndexOffset property to false.

2.7、Method clearIndexOffsetCache

You can use the clearIndexOffsetCache method if you want to preserve the cache function of scrolling and only want to clear the cache in certain cases.

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

The parameter sliverContext needs to be passed only if you manage ScrollView's BuildContext by yourself.

2.8、Initial index location

  • Method 1: initialIndex

The simplest way to use, directly set the location index.

observerController = ListObserverController(controller: scrollController)
  ..initialIndex = 10;
  • Method 2: initialIndexModel

You can customize the configuration of the initial index position. See the end of this section for property descriptions.

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

You need return ObserverIndexPositionModel object within the callback.

This method applies to some of scenarios those the parameters can't be determined from the start, such as sliverContext.

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

The structure of ObserverIndexPositionModel :

ObserverIndexPositionModel({
  required this.index,
  this.sliverContext,
  this.isFixedHeight = false,
  this.alignment = 0,
  this.offset,
  this.padding = EdgeInsets.zero,
});
Property Type Desc
index int The index of child widget.
sliverContext BuildContext The target sliver [BuildContext].
isFixedHeight bool If the height of the child widget and the height of the separator are fixed, please pass true to this property. Defaults to false
alignment double The alignment specifies the desired position for the leading edge of the child widget. It must be a value in the range [0.0, 1.0]. Defaults to 1.0
offset double Function(double targetOffset) Use this property when locating position needs an offset.
padding EdgeInsets If your ListView or GridView uses its padding parameter, you need to sync that value as well! Otherwise it is not required.

2.9、Scroll state

Starting from version 1.18.0, some new scrolling notifications have been added to facilitate listening of scrolling state. Note that they will only be emitted after the jumpTo method or animateTo method of ObserverController is called.

Notification Desc
ObserverScrollStartNotification Start executing the scrolling task, the target item has not been found yet
ObserverScrollInterruptionNotification The scrolling task is interrupted, such as: a non-existent index is passed in, there is no ScrollController, etc.
ObserverScrollDecisionNotification The scrolling task decision is completed, the target item is currently found.
ObserverScrollEndNotification The scrolling task is end normally

The above notifications all extend from ObserverScrollNotification and can be used as the generic type required by NotificationListener.

Notification sequence: ObserverScrollStartNotification -> ObserverScrollDecisionNotification -> ObserverScrollEndNotification.

ObserverScrollInterruptionNotification does not participate in the above sequence. It will be issued when the scrolling conditions are not met. After it is issued, the scrolling task will end and no other notifications will be posted!

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;
  },
);

The jumpTo method and animateTo method also officially support await in the 1.18.0 version. There are two possibilities for the result after await:

  1. Interruption (same as ObserverScrollInterruptionNotification)
  2. End normally (same as ObserverScrollEndNotification)