0

So My head can't think of a way to engineer this, I have scrollable infinite list, every item of the list contains some information and one is date.

So I'd like to track the current top index Item to update another widget of the section date we are on kinda like some messaging apps do e.g: like 'Signal'.

what I tried

I thought taking the whole scroll height and dividing the items number but that was wrong as items are expandable so turned out dumb.

can someone please share an insight

I am using a CustomScrollView that looks like this

CustomScrollView(
      slivers: [
        SliverToBoxAdapter(
          child: Container(
             height: 100,
             color: Colors.blue
            ),
        ),
        SliverToBoxAdapter(
          child: Container(
             height: 100,
             color: Colors.blue
            ),
        ),
         SliverPersistentHeader(
          delegate: PersistentHeader(
            widget: Container()
           ),
           pinned: true,
           ),

// I am trying to trace items in this section
         SliverList(
           delegate: SliverChildBuilderDelegate((context, index) {
             return ListTile(title: Text('bron $index'),);
           }))
        
      ],
    )
bihire boris
  • 1,530
  • 3
  • 19
  • 44

1 Answers1

1

In short, you can achieve it by adding Listener to ScrollController.

A. Calculate by scroll offset

enter image description here

In this solution, you need to know the first item offset in the list and each item height.

final double initialOffset = 300;
final double itemHeight = 50;

Then you can change the index every time ScrollController is scrolled:

...

final ScrollController _scrollController = ScrollController();

...
@override
Widget build(BuildContext context) {
  final index = ValueNotifier<int>(-1);

  _scrollController.addListener(() {
    index.value = ((_scrollController.offset-initialOffset)/itemHeight).round()-1;
  });

  return CustomScrollView(
    controller: _scrollController,
    slivers: [
      SliverToBoxAdapter(
        child: Container(height: 100, color: Colors.blue),
       ),
       SliverToBoxAdapter(
         child: Container(height: 100, color: Colors.blue),
       ),
       ValueListenableBuilder(
         valueListenable: index, 
         builder: (_,value,__){
           return SliverPersistentHeader(
             pinned: true,
             delegate: PersistentHeader(value),
           );
         },
       ),
       SliverList(
         delegate: SliverChildBuilderDelegate(
         ...

B. Calculate children location on the screen

enter image description here

This is much more complicated.

It keeps track of all children in the list inside the screen and returns the index from the screen offset:

bool inTopArea(double dy) => dy <= 60 && dy >=0;

Also, it needs Global keys to keep track of all children and gets the global location (from here)

extension GlobalKeyExtension on GlobalKey {
  Rect get globalPaintBounds {
    final renderObject = currentContext?.findRenderObject();
    var translation = renderObject?.getTransformTo(null)?.getTranslation();
    if (translation != null && renderObject.paintBounds != null) {
      return renderObject.paintBounds
        .shift(Offset(translation.x, translation.y));
    } else {
      return null;
    }
  }
}

final keys = <GlobalKey>[];

Because you use builder to generate the list, the child must be a stateful widget that contains dispose method, which represents the widget is out of the screen:

  ...

  SliverList(
    delegate: SliverChildBuilderDelegate(
      (context, index) {
        final uniqueKey = GlobalKey();
        keys.add(uniqueKey);
        return MyChildWidget(
          index: index,
          dispose: (){keys.remove(uniqueKey);},
          key: uniqueKey,
        );
      },
    ),
  ),

  ...

class MyChildWidget extends StatefulWidget {
  const MyChildWidget({this.index,this.dispose,Key key}):super(key: key);

  final int index;
  final VoidCallback dispose;

  @override
  _MyChildWidgetState createState() => _MyChildWidgetState();
}

class _MyChildWidgetState extends State<MyChildWidget> {
  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text('bron ${widget.index}'),
    );
  }
  @override
  void dispose() {
    widget.dispose();
    super.dispose();
  }
}

Finally, the calculation of every child dy location when scrolling:

_scrollController.addListener(() {
  final tops = keys.where((aKey) {
    return inTopArea(aKey.globalPaintBounds.topLeft.dy);        
  });
  if(tops.isNotEmpty){
    index.value = (tops.first.currentState as _MyChildWidgetState).widget.index;
  }else{
    index.value = -1;
  }
});
yellowgray
  • 4,006
  • 6
  • 28