7

I have this application that has a sliverlist and a sliverAppbar. I need to get the current scroll position of each item in the sliverlist onscroll and determine if it has crossed the sliverAppbar and update the sliverappbar with the Title of the item. Say starting from Item1 once it crosses the SliverAppBar that means I am viewing Item1 content and when Item2 crosses the SliverAppBar, update the SliverAppBar with the title Item2 to mean the user is viewing Item2 content

I am trying to implement this using a NotificationListener<ScrollEndNotification> but I am stuck at the second NotificationListener that is supposed to emit notifications to the top of the parent at this line ScrollEndNotification(metrics: , context:context).dispatch(context); it throws an error that I should provide a metrics parameter which I don't know what to provide.

            SliverList(
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  ScrollEndNotification(metrics: , context:context).dispatch(context);
                  return  AutoScrollTag(
                        key: ValueKey(index),
                        controller: controller,
                        index: index,
                        child: Padding(
                     padding: const EdgeInsets.only(
                      top: 30.0, left: 20.0, right: 20.0),
                      child: Container(
                       color: Colors.red,
                     //height: 120.0
                     //height: A varying height

                        ),),},); ),

The complete code is

           Widget build(BuildContext context) {
           return Scaffold(
           backgroundColor: Colors.grey[100],
           body: NotificationListener<ScrollEndNotification>(
                    onNotification: (notification) {
                      if (notification is ScrollEndNotification) {
                     ///Here I need to know what widget index bubbled the notification, its position 
                     ///on the screen and its index 
                     //in the list, in order to do further implementation like update the 
                      //SliverAppBar
                      print('$notification');          
                      return true;
                    },
              child: CustomScrollView(
                 controller: controller
             slivers: <Widget>[
              SliverAppBar(
            title: Text(title),
             ), 
         SliverList(
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  ScrollEndNotification(metrics: , context:context).dispatch(context);
                  return  AutoScrollTag(
                        key: ValueKey(index),
                        controller: controller,
                        index: index,
                        child: Padding(
                     padding: const EdgeInsets.only(
                      top: 30.0, left: 20.0, right: 20.0),
                      child: Container(
                       color: Colors.red,
                     //height: 120.0
                     //height: A varying height
                        ),),},); ),

Also if you have a better implementation on how I can achieve this do help me out. In short, I need to keep track when an item is scrolled off the screen and get the say index of it in the Sliverlist. Keeping in mind that the item has a variable size container that expands according to the number of children in it. It is a common UX pattern in ecommerce apps. Eg Viewing menus as the user scrolls down and updating what menu the user is viewing as the title crosses the screen.

Providing a link to the gist so you get an idea of the complete implementation

Taio
  • 3,152
  • 11
  • 35
  • 59
  • How about tracking the global location of each child? [How to track the top Item in the viewport for a list flutter](https://stackoverflow.com/a/66383068/14272882) – yellowgray May 18 '21 at 11:04

2 Answers2

5

I implemented what you want for various height children.
To make a various child's height, give a height randomly.

  1. Get each index children's height and save distance from top.
  2. Add a scroll listener for get current scroll position
  3. Get just hidden child index with n'th child's distance and current position
  4. Change the title with just hidden child index

enter image description here

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:scroll_to_index/scroll_to_index.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  String title = 'Viewing item 0';
  Random random = Random();

  Map<int, double> itemHeight = {};
  int currentHideIndex = 0;

  final _mainScrollController = ScrollController();
  @override
  void initState() {
    super.initState();
    _mainScrollController.addListener(_onMainScroll);
  }

  void _onMainScroll() {
    int justHiddenIndex =
        findIndexJustHidden(_mainScrollController.position.pixels);
    print('justHiddenIndex: $justHiddenIndex');
    if (currentHideIndex != justHiddenIndex) {
      setState(() {
        title = 'Viewing item ${justHiddenIndex + 1}';
      });
      currentHideIndex = justHiddenIndex;
    }
  }

  int findIndexJustHidden(currentPosition) {
    int index = -1;
    for (var item in itemHeight.entries) {
      if (currentPosition > item.value) {
        index = item.key;
      } else {
        if (index != 0) {
          return index;
        }
      }
    }
    return index;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[100],
      body: CustomScrollView(
        controller: _mainScrollController,
        slivers: <Widget>[
          SliverAppBar(
            title: Text(title),
            pinned: true,
          ),
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (BuildContext context, int index) {
                double randomHeight;
                if (!itemHeight.containsKey(index)) {
                  randomHeight = (random.nextInt(100) + 40) * 1.0;
                  print('index: $index, randomHeight: $randomHeight');
                  double beforeSumHeight =
                      index == 0 ? 0 : itemHeight[index - 1];
                  print({
                    'index': index,
                    'beforeSumHeight': beforeSumHeight,
                    'height': randomHeight
                  });
                  itemHeight[index] = beforeSumHeight + randomHeight;
                } else {
                  randomHeight = index == 0
                      ? itemHeight[index]
                      : itemHeight[index] - itemHeight[index - 1];
                }

                return AutoScrollTag(
                  key: ValueKey(index),
                  controller: AutoScrollController(),
                  index: index,
                  child: Container(
                    height: randomHeight,
                    decoration: BoxDecoration(
                        color: Colors.red.withOpacity(0.5),
                        border: Border(
                          bottom: BorderSide(
                            color: Color(0XFF000000).withOpacity(0.08),
                            width: 1.0,
                            style: BorderStyle.solid,
                          ),
                        )),
                    padding: EdgeInsets.only(top: 10, left: 20.0, right: 20.0),
                    child: Text('$index'),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildBody() {
    return Container();
  }
}

class WidgetSize extends StatefulWidget {
  final Widget child;
  final Function onChange;

  const WidgetSize({
    Key key,
    @required this.onChange,
    @required this.child,
  }) : super(key: key);

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

class _WidgetSizeState extends State<WidgetSize> {
  @override
  Widget build(BuildContext context) {
    SchedulerBinding.instance.addPostFrameCallback(postFrameCallback);
    return Container(
      key: widgetKey,
      child: widget.child,
    );
  }

  var widgetKey = GlobalKey();
  var oldSize;

  void postFrameCallback(_) {
    var context = widgetKey.currentContext;
    if (context == null) return;

    var newSize = context.size;
    if (oldSize == newSize) return;

    oldSize = newSize;
    widget.onChange(newSize);
  }
}

KuKu
  • 6,654
  • 1
  • 13
  • 25
  • This looks like the children increase heights incrementally. If that makes sense. Can you make a random height for each child, say one has a height of 40 another 20, 100,,, just random. This looks like the only answer that is close to what I need to achieve. Will this work with random heights children, that's my question. – Taio May 21 '21 at 10:22
  • When I fetch from the database, the children will have varying heights, not heights that are increasing linearly. Tell me if that will work, random heights while knowing which has scrolled off the screen – Taio May 21 '21 at 10:24
  • @Taio I changed code as your requirement. The randomly decided height is more easier than before. But whether the height is decided by randomly had been decided or had been decided or decided by child's height, key solution is same that calculate with each index children's scroll position and current scroll position. – KuKu May 21 '21 at 11:04
  • If there is a decided height in fetched data, just making 'itemHeight' object that make by adding n-1's height and n's height. Myy example is more complicated because random height is changed when rebuild. – KuKu May 21 '21 at 11:07
  • The added code broke it. Change the appBar text to 'Viewing item $index' so you can get what am saying. It should update to the currently being viewed item. As it is right now when say index 9 crosses the screen it says 'viewing item 8' while it should be 'viewing item 9'. Can you check this before I mark the answer – Taio May 21 '21 at 14:03
  • @Taio I changed. I hope it help you. – KuKu May 21 '21 at 14:23
1

You don't need to use ScrollEndNotification. In fact, I find the UX more responsive when the SliverAppBar updates during scrolling, and this is the most similar result to an onscroll handler anyways. Your idea of using notifications works fine if you change to just using the regular ScrollNotification. The problem is actually pretty straightforward in this case because the SliverList children have fixed height. The children are always 150 pixels high (120 pixels is content with 30 padding).

With this in mind, we can actually calculate the index of the child that gets scrolled out in the notification handler. I had to change the SliverAppBar to be pinned to keep the bar (and the scrolled out index) visible during scroll.

Note that if the height of the child items wasn't fixed we'd need to calculate each row's height every time, so this approach would need to be refactored.

Essentially the logic is:

// Get scroll position `progress` and subtract `SliverAppBar` height.
double progress = notification.metrics.pixels - 60;
// Calculate index scrolled off the screen.
index = (progress ~/ 150) - 1;

I've also added a valid index check so that negative indices don't get displayed by mistake.

((index >= 0) ? index.toString() : "")

Complete code:

class _MyHomePageState extends State<MyHomePage> {
  late AutoScrollController controller =
      AutoScrollController(initialScrollOffset: 0);
  int index = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[100],
      body: NotificationListener<ScrollNotification>(
        onNotification: (ScrollNotification notification) {
          double progress = notification.metrics.pixels - 60;
          setState(() {
            index = (progress ~/ 150) - 1;
          });
          return true;
        },
        child: CustomScrollView(
          controller: controller,
          slivers: <Widget>[
            SliverAppBar(
              title: Text(
                  widget.title + " " + ((index >= 0) ? index.toString() : "")),
              expandedHeight: 60,
              pinned: true,
            ),
            SliverList(delegate: SliverChildBuilderDelegate(
              (BuildContext context, int localIndex) {
                return AutoScrollTag(
                  key: ValueKey(localIndex),
                  controller: controller,
                  index: localIndex,
                  child: Padding(
                    padding: const EdgeInsets.only(
                        top: 30.0, left: 20.0, right: 20.0),
                    child: Container(color: Colors.red, height: 120.0),
                  ),
                );
              },
            )),
          ],
        ),
      ),
    );
  }
}
Pranav Kasetti
  • 8,770
  • 2
  • 50
  • 71
  • Actually am really sorry, for the confusion, that container does not have a fixed height of 120.0, that height could change depending on the child which should be a Column actually with a couple (varying number of) children. Can your solution be used for varying heights – Taio Apr 28 '21 at 18:02
  • Hi, @Taio can you share a demo project so I can take a look? Or if you can update the question code then I will try it out. Thanks – Pranav Kasetti Apr 28 '21 at 18:27
  • Check it out here, https://gist.github.com/kevinrobert3/5ab89c285bd72a2c51c6435df0774093 key point is that a list can have several items in it. Say list1 has 4 items list 2 has 6 items. They all vary in size.. – Taio Apr 28 '21 at 18:35
  • Sorry, but that gist completely changes the whole question! I'd recommend asking another question instead. :) Breaking down the widget tree in that example into smaller build functions would also make the issue easier to debug. Thanks. – Pranav Kasetti Apr 28 '21 at 19:46
  • I do not think it changes the question. I just gave a minimal code in the question. The concept is the same, i need to get the index of the item when an item crosses the screen, just that. Thank you though – Taio Apr 29 '21 at 04:29
  • No, it does change the question completely. When you have variable-sized children in a SliverList, you need to keep track of the list heights for each index in an array as you create each child. Then you can query the heights array when getting the scroll position, inefficiently searching. The approach for fixed-size children is much more efficient, and your gist would need to be completely refactored. The framework you used, and ScrollController itself, do not have methods for getting the index for a scroll position so this is completely non-trivial. I hope that makes sense. – Pranav Kasetti Apr 29 '21 at 08:51