Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get Current Index #62

Open
ibrahimdevs opened this issue Jun 8, 2024 · 10 comments
Open

Get Current Index #62

ibrahimdevs opened this issue Jun 8, 2024 · 10 comments

Comments

@ibrahimdevs
Copy link

Firstly, thanks for great package! I'm already fan of SuperEditor, this package saved me again :)

I'm trying to create snapList, I think it will be very easy if I can get current Index. I'm listening ScrollEndNotification on NotificationListener, but I don't know which is the current item. Is there any workaround for this? If there would be a function gives us index of item based on alignment?

Thanks,

@ibrahimdevs
Copy link
Author

ibrahimdevs commented Jun 9, 2024

@knopp I think, if you put indexForOffset method to ListController, I can achieve this. I have 3 questions for you:
Is there any problem to make this indexForOffset function accessible from ListController, if not could you update it? (1)

  int? indexForOffset(double offset) {
    assert(_delegate != null, "ListController is not attached.");
    return _delegate!.indexForOffset(offset);
  }

I'm trying to accomplish 2 features;

  • Selected item's opacity should be 1, others 0.5.
    I can make it with listening ScrollEndNotification and call setState if the last focused index changed. (I have a problem here, I think because of ListController calculates real item extent, it can animate multiple times, so this ScrollEndNotification triggers multiple times. How can I avoid it?(2) )

  • When user scrolls the list, our list should snap the current list item's exact position.
    I can make it with listening UserScrollNotification and if this notification is idle, animate to last focused index (for set scroll position to item's exact position).

Does my solution ok, or could you offer better way to do this? (3)

Thank you very much!

          NotificationListener<ScrollNotification>(
            onNotification: (ScrollNotification scrollInfo) {
              if (scrollInfo.depth > 0) {
                print("scrollInfo.depth > 0");
                return false;
              }
              if (scrollInfo is ScrollEndNotification) {
                //When scroll is ended, we're checking the lastIndex and call onItemFocus callback.
                var lastIndex = listController.indexForOffset(scrollInfo.metrics.pixels);
                if (lastIndex != null && lastIndex != focusedItemIndex) {
                  setState((){
                    focusedItemIndex = lastIndex;
                  });
                }
              }

              if (scrollInfo is UserScrollNotification && scrollInfo.direction == ScrollDirection.idle) {
                //When user scroll is idle, we're checking the lastIndex and animate to this item for snap feature.
                var lastIndex = listController.indexForOffset(scrollInfo.metrics.pixels);
                if (lastIndex != null) {
                  if (lastIndex != focusedItemIndex) {
                    setState((){
                      focusedItemIndex = lastIndex;
                    });
                  }
                  listController.animateToItem(
                    index: focusedItemIndex,
                    scrollController: scrollController,
                    alignment: 0.5,
                    duration: (estimatedDistance) => Duration(milliseconds: 250),
                    curve: (estimatedDistance) => Curves.easeInOut,
                  );
                }
              }
              return true;
            },
            child: SuperListView.builder(
              controller: scrollController,
              listController: listController,
              itemBuilder: (context, int){
                return AnimatedOpacity(
                  duration: Duration(milliseconds: 250),
                  opacity: index == focusedItemIndex ? 1.0 : 0.5,
                  child: Text("${itemList[index]}"),
                );
              },
              itemCount: itemList.length,
              physics: ClampingScrollPhysics(),
            ),
          );

@knopp
Copy link
Collaborator

knopp commented Jun 9, 2024

I don't quite understand what you're trying to accomplish here. What exactly is "current item"? I can't really expose indexForOffset and offsetForIndex, since they are largerly estimations and change over time as items are being laid out.

@ibrahimdevs
Copy link
Author

@knopp Actually, I'm trying to make very similar to ListWheelScrollView, difference is variable itemExtent support.

We can define current item;

  • based on alignment (Viewport Alignment)
    or
  • first visible item of SuperListView
    So if there is a method like that fix my problem:
    int getVisibleIndex({double alignment = 0})

In my case, in my real code, I'm adding padding to SuperListView as half of the viewport size. So "current item" is center of viewport. I'm calling listController.animateToItem with alignment: 0.5 , so I can navigate my "current item". My problem is when user scrolls, how can I know what is my "current item"?

@knopp
Copy link
Collaborator

knopp commented Jun 9, 2024

There is visibleRange property on ListController that will give you the index of first and last currently visible items. It will not give you the "center" object since that's quite arbitrary.

It seems that for something like this you will probably want to go down to render objects and check the layout of your list item render object against list view viewport, which will give you the most correct answer.

@ibrahimdevs
Copy link
Author

@knopp I tried visibleRange but no luck, it's giving me only visible first and last item.

For render object part, it is too complicated for me, I don't know event where to start. Also who want to snap item should put some logic which is available already in this package.

What about this solution:

 int? snapByOffset({
    required double offset,
    required ScrollController scrollController,
    required double alignment,
    required Duration Function(double estimatedDistance) duration,
    required Curve Function(double estimatedDistance) curve,
    Rect? rect,
  }) {
    assert(_delegate != null, "ListController is not attached.");
    final targetIndex = _delegate!.indexForOffset(offset);
    if(targetIndex == null){
        return null;
    }
    animateToItem( 
    index: targetIndex,
                    scrollController: scrollController,
                    alignment: alignment,
                    duration: duration,
                    curve: curve,
                    );
    return targetIndex;
  }

You said "indexForOffset" is not reliable and can change over time, but in this particular feature, it is reliable for us.
I mean;

  • Jump to 1000. item.
  • This package calculates(based on estimations) and jumps to X offset.
  • User scrolls a little, if I call "indexForOffset(X+1)" this packages will return 1000. (based on estimation, it can change over time but it is correct now.)
  • So It can snap to 1000. item now.

Also another approach, you can make public this method with more accurate naming and we can do this logic.

int? estimatedIndexForOffset(double offset) {
    assert(_delegate != null, "ListController is not attached.");
    return _delegate!.indexForOffset(offset);
  }

For this one extra method, this package can use like ListWheelScrollView with variable height.
What do you think?

Thanks,

@knopp
Copy link
Collaborator

knopp commented Jun 9, 2024

Can you attach a video of this in action? The snapByOffset function will only work for items that are currently scrolled in (where the index estimation will be correct). I'd rather not have things in API that "sometimes" work, so maybe there is another way to approach this.

@ibrahimdevs
Copy link
Author

ibrahimdevs commented Jun 9, 2024

The video:

RPReplay_Final1717971863.MP4

Here is the code:

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

void main() {
  var randomList = List.generate(1000, (i) => "$i - ${"Lorem ipsum " * ((i % 10) + 1)}");
  runApp(
    MaterialApp(
      home: MyApp(list: randomList),
    ), // use MaterialApp
  );
}

class MyApp extends StatefulWidget {
  const MyApp({super.key, required this.list});
  final List<String> list;

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final ScrollController scrollController = ScrollController();
  final ListController listController = ListController();

  var focusedItemIndex = 0;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      listController.jumpToItem(
        index: 505,
        scrollController: scrollController,
        alignment: 0.5,
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: LayoutBuilder(
        builder: (BuildContext ctx, BoxConstraints constraint) {
          double _topPadding = constraint.maxHeight * 0.5;
          return NotificationListener<ScrollNotification>(
            onNotification: (ScrollNotification scrollInfo) {
              if (scrollInfo is ScrollEndNotification) {
                //When scroll is ended, we're checking the lastIndex and call onItemFocus callback.
                //Update and re-render with focusedItemIndex
                var lastIndex = listController.indexForOffset(scrollInfo.metrics.pixels);
                if (lastIndex != null && lastIndex != focusedItemIndex) {
                  setState(() {
                    focusedItemIndex = lastIndex;
                  });
                }
              }

              if (scrollInfo is UserScrollNotification && scrollInfo.direction == ScrollDirection.idle) {
                //User ended scroll, so we should snap.
                var lastIndex = listController.indexForOffset(scrollInfo.metrics.pixels);
                if (lastIndex != null) {
                  listController.animateToItem(
                    index: lastIndex,
                    scrollController: scrollController,
                    alignment: 0.5,
                    duration: (estimatedDistance) => Duration(milliseconds: 250),
                    curve: (estimatedDistance) => Curves.easeInOut,
                  );
                }
              }
              return true;
            },
            child: SuperListView.builder(
              controller: scrollController,
              listController: listController,
              padding: EdgeInsets.only(top: _topPadding, bottom: constraint.maxHeight - _topPadding),
              itemBuilder: (BuildContext, index) {
                return AnimatedContainer(
                  duration: Duration(milliseconds: 250),
                  child: Container(
                    color: index == focusedItemIndex ? Colors.amberAccent : Colors.white,
                    child: Text(
                      widget.list[index],
                      style: TextStyle(fontSize: 24),
                    ),
                  ),
                );
              },
              itemCount: widget.list.length,
              physics: ClampingScrollPhysics(),
            ),
          );
        },
      ),
    );
  }
}

@knopp
Copy link
Collaborator

knopp commented Jun 10, 2024

I think the solution here would be for ListController to report index + offset + extent for all visible items, instead of just first and last index. That way you could find the index of center item from the scroll notification by iterating over the list.

@ibrahimdevs
Copy link
Author

Exactly! Could you add this feature? This way, we can use this package as snap list like ListWheelScrollView. More, because of reporting all visible item's index, offset and extent, maybe there would be some other use cases for different needs.

@LinXunFeng
Copy link

@ibrahimdevs Hope this can help you.

2024-09-16.14.52.01.mov
import 'package:flutter/material.dart';
import 'package:scrollview_observer/scrollview_observer.dart';
import 'package:super_sliver_list/super_sliver_list.dart';

void main() {
  var randomList = List.generate(
    1000,
    (i) => "$i - ${"Lorem ipsum " * ((i % 10) + 1)}",
  );
  runApp(
    MaterialApp(
      home: MyApp(list: randomList),
    ), // use MaterialApp
  );
}

class MyApp extends StatefulWidget {
  const MyApp({super.key, required this.list});
  final List<String> list;

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final ScrollController scrollController = ScrollController();
  final ListController listController = ListController();
  late ListObserverController observerController = ListObserverController(
    controller: scrollController,
  );

  var focusedItemIndex = 0;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      listController.jumpToItem(
        index: 505,
        scrollController: scrollController,
        alignment: 0.5,
      );

      await Future.delayed(const Duration(milliseconds: 100));
      observerController.dispatchOnceObserve();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: LayoutBuilder(
        builder: (BuildContext ctx, BoxConstraints constraint) {
          double _topPadding = constraint.maxHeight * 0.5;
          return Stack(
            children: [
              _buildListView(constraint, _topPadding),
              Positioned(
                top: _topPadding,
                left: 0,
                right: 0,
                child: Container(height: 1, color: Colors.red),
              ),
            ],
          );
        },
      ),
    );
  }

  Widget _buildListView(
    BoxConstraints constraint,
    double topPadding,
  ) {
    Widget resultWidget = SuperListView.builder(
      controller: scrollController,
      listController: listController,
      padding: EdgeInsets.only(
        top: topPadding,
        bottom: constraint.maxHeight - topPadding,
      ),
      itemBuilder: (buildContext, index) {
        return AnimatedContainer(
          duration: const Duration(milliseconds: 250),
          child: Container(
            color:
                index == focusedItemIndex ? Colors.amberAccent : Colors.white,
            child: Text(
              widget.list[index],
              style: const TextStyle(fontSize: 24),
            ),
          ),
        );
      },
      itemCount: widget.list.length,
      physics: const ClampingScrollPhysics(),
    );
    resultWidget = ListViewObserver(
      controller: observerController,
      leadingOffset: topPadding,
      child: resultWidget,
      onObserve: (result) {
        setState(() {
          focusedItemIndex = result.firstChild?.index ?? 0;
        });
        print('firstIndex: ${result.firstChild?.index}');
      },
      customTargetRenderSliverType: (obj) {
        return 'RenderSuperSliverList' == obj.runtimeType.toString();
      },
    );
    return resultWidget;
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants