superlistapp / super_sliver_list

Drop-in replacement for SliverList and ListView that can handle large amount of items with variable extents and reliably jump / animate to any item.
https://superlistapp.github.io/super_sliver_list/
MIT License
277 stars 15 forks source link

initialScrollOffset is incorrect in 0.2.0 #35

Closed sososdk closed 6 months ago

sososdk commented 6 months ago

Use initialScrollOffset:

final scrollController = ScrollController(initialScrollOffset: index * 48);
CustomScrollView(
  controller: scrollController,
  ...
);

super_sliver_list 0.0.8 works fine.

super_sliver_list 0.2.0 initialScrollOffset is incorrect.

knopp commented 6 months ago

That is not unexpected. Even during initial layout the may scroll offset gets corrected in some cases. The offset estimation works differently in the lists. In 0.2.0 it is approximation based on existing items.

What is your use-case? Are all your list item fixed-extent? If so, you can provide extentEstimation, i.e.

SuperSliverList(
   extentEstimation(_,__) => 48,
)

which means that estimated item extent will match actual extent and there is no correction.

I think super_sliver_list currently needs way to reliably jump to initial index with variable extent lists. That's a missing feature currently.

knopp commented 6 months ago

Note that you can get quite far by simply jumping to initial item in first post frame callback.

 @override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      _listControllers.last.jumpToItem(
          index: 50, scrollController: _scrollController, alignment: 0.0);
    });
  }

This will show the list initially at the beginning, but only for one frame after which it immediately jump to requested item. The benefit here is that this works with variable extents, you don't need to know the height of your item and they don't need to be same.

sososdk commented 6 months ago

jumpToItem not work, when SuperSliverList wrapped with MultiSliver.

Example:

import 'package:flutter/material.dart';
import 'package:sliver_tools/sliver_tools.dart';
import 'package:super_sliver_list/super_sliver_list.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final scrollController = ScrollController(initialScrollOffset: 50 * 48);
  final listController = ListController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: CustomScrollView(
        controller: scrollController,
        slivers: [
          MultiSliver(
            pushPinnedChildren: true,
            children: [
              SliverPinnedHeader(
                child: Container(
                  height: 48,
                  color: Theme.of(context).colorScheme.inversePrimary,
                  child: const ListTile(title: Text('Group 1')),
                ),
              ),
              SuperSliverList(
                extentEstimation: (index, crossAxisExtent) => 48,
                delegate: SliverChildBuilderDelegate(
                  (context, index) => SizedBox(
                    height: 48,
                    child: ListTile(title: Text('Group 1 Item $index')),
                  ),
                  childCount: 30,
                ),
              )
            ],
          ),
          MultiSliver(
            pushPinnedChildren: true,
            children: [
              SliverPinnedHeader(
                child: Container(
                  height: 48,
                  color: Theme.of(context).colorScheme.inversePrimary,
                  child: const ListTile(title: Text('Group 2')),
                ),
              ),
              SuperSliverList(
                extentEstimation: (index, crossAxisExtent) => 48,
                delegate: SliverChildBuilderDelegate(
                  (context, index) => SizedBox(
                    height: 48,
                    child: ListTile(title: Text('Group 2 Item $index')),
                  ),
                  childCount: 30,
                ),
              )
            ],
          ),
          MultiSliver(
            pushPinnedChildren: true,
            children: [
              SliverPinnedHeader(
                child: Container(
                  height: 48,
                  color: Theme.of(context).colorScheme.inversePrimary,
                  child: const ListTile(title: Text('Group 3')),
                ),
              ),
              SuperSliverList(
                listController: listController,
                extentEstimation: (index, crossAxisExtent) => 48,
                delegate: SliverChildBuilderDelegate(
                  (context, index) => SizedBox(
                    height: 48,
                    child: ListTile(title: Text('Group 3 Item $index')),
                  ),
                  childCount: 30,
                ),
              )
            ],
          ),

          // SuperSliverList(
          //   listController: listController,
          //   extentEstimation: (index, crossAxisExtent) => 48,
          //   delegate: SliverChildBuilderDelegate(
          //     (context, index) => SizedBox(
          //       height: 48,
          //       child: ListTile(title: Text('Group 2 Item $index')),
          //     ),
          //     childCount: 90,
          //   ),
          // )
        ],
      ), //
      floatingActionButton: FloatingActionButton(
        onPressed: () => listController.jumpToItem(
          index: 2,
          scrollController: scrollController,
          alignment: 0.0,
        ),
        tooltip: 'Jump',
        child: const Icon(Icons.gps_fixed),
      ),
    );
  }
}
knopp commented 6 months ago

MultiSliver does not properly implement the childScrollOffset method so it is not possible to have jumpToOffset working with it. Second issue is that SliverGeometry does not currently have any way of signalling child obstruction extent, needed for determining visible children in sliver.

If you want to have sticky headers, you can use this widget from example and place SuperSliverLists directly in the CustomScrollView (without MultiSliver).

sososdk commented 6 months ago

Is there a way to avoid rebound?

https://github.com/superlistapp/super_sliver_list/assets/46806014/53cc1112-4289-405a-9e88-8d4382b88d25

knopp commented 6 months ago

The rebound is there because you are showing last item with alignment = 0, which means beginning of viewport. If you want to show last item at the end of the list, you need to set alignment to 1, which will get rid of rebound.

sososdk commented 6 months ago

Yes, you are right.

But when we jump to any item, we don’t know whether this item will cause a rebound.

Just like scroll_to_index does not cause a rebound.

knopp commented 6 months ago

That is a good point. I agree current behavior is counter-intuitive. Should be fixed by https://github.com/superlistapp/super_sliver_list/pull/38.

knopp commented 6 months ago

Overscroll should be fixed in 0.2.1 (just published).

sososdk commented 6 months ago

Thank you so much!