GithubHelp home page GithubHelp logo

simonwang9610 / indexed_scroll_observer Goto Github PK

View Code? Open in Web Editor NEW
5.0 2.0 0.0 15.65 MB

An elegant scroll observer for observing scrollable widgets in Flutter applications.

Home Page: https://pub.dev/packages/positioned_scroll_observer

License: MIT License

Java 0.34% Kotlin 0.08% Swift 0.80% Objective-C 0.39% Dart 68.80% CMake 12.42% C++ 14.13% C 1.25% HTML 1.13% Shell 0.66%
flutter flutter-ui scrollview-component slivergrid sliverlist

indexed_scroll_observer's Introduction

pub package GitHub Repo stars

Features

  1. Using jumpToIndex and animateToIndex to scroll to the specific index

  2. Jumping/animating to the position by specifying a ratio in a scroll view. See how to align the render object. That would be very useful if you want to quickly jump to the top, middle or end of a list/grid.

  3. Using PositionRetainedScrollPhysics to retain the old offset to avoid scrolling when adding new items into the top of ListView. See retain old scroll offset.

  4. Check if the specific index is visible on the screen. See check visibility.

  5. Check which items are visible in the viewport. See get visible items

  6. Check the visible ratio of the observed RenderObject in a viewport. See how to use it in a GroupList

  7. No breaking for your current sliver widgets, e.g., ListView/GridView, SliverList/SliverGrid/SliverAppBar, just wrapping your item widgets using ObserverProxy. Supported:

  • ListView
  • GridView
  • CustomScrollView
  • SingleChildScrollView
  • ListWheelScrollView
  • NestedScrollView (waiting testing)

Getting started

  1. First, creating and binding the observer to all items. (See box scroll observer and sliver scroll observer)

  2. then, using the observer like:

    _observer.jumpToIndex(
        index,
        position: _controller.position,
    );
    
    _observer.animateToIndex(
        index,
        position: _controller.position,
        duration: const Duration(milliseconds: 200),
        curve: Curves.fastLinearToSlowEaseIn,
    );

Usage for scroll views that do not rely on RenderSliver, e.g., SingleChildScrollView and ListWheelScrollView

  1. create a BoxScrollObserver for observing the box with multi children.

      final ScrollController _controller = ScrollController();
      late final _observer = ScrollObserver.boxMulti(
        axis: _axis,
        itemCount: 30,
      );
  2. bind the observer to the box's children. (Using ObserverProxy to wrap each item).

    SingleChildScrollView(
      controller: _controller,
      scrollDirection: _axis,
      child: Column(
        children: [
          for (int i = 0; i < 30; i++)
            ObserverProxy(
              observer: _observer,
              child: DecoratedBox(
                decoration: BoxDecoration(border: Border.all()),
                child: SizedBox(
                  height: 100,
                  width: 100,
                  child: Center(
                    child: Text("Column item $i"),
                  ),
                ),
              ),
            ),
          ],
        ),
      );

Usage for slivers widgets.

  1. create a SliverScrollObserver for observing the sliver with multi children.

      final ScrollController _controller = ScrollController();
      late final _observer = ScrollObserver.sliverMulti(itemCount: 30);
  2. bind the observer to each item for the sliver.

        ListView.builder(
          controller: _controller,
          itemBuilder: (context, index) => ObserverProxy(
            observer: _observer,
            child: ListTile(
              key: ValueKey<int>(index),
              leading: const CircleAvatar(
                child: Text("L"),
              ),
              title: Text("Positioned List Example $index"),
            ),
          ),
          itemCount: _itemCount,
        );

For ListView.custom and GridView.custom, you could also use PositionedChildListDelegate and PositionedChildBuilderDelegate for wrapping items in ObserverProxy conveniently

Usage

For observing slivers:

  1. observing a sliver with single child, using ScrollObserver.sliverSingle to create.
  2. observing a sliver with multi children, using ScrollObserver.sliverMulti to create.

For observing other scroll views that have no sliver descendants.

  1. observing a box with single child, using ScrollObserver.boxSingle to create. (rare cases and need more testing)
  2. observing a box with multi children, using ScrollObserver.boxMulti to create.

Checking index is visible on the screen

get visible items

More details, see API reference.

Using PositionRetainedScrollPhysics for retaining the old scroll offset

ListView.builder(
  controller: _controller,
  reverse: true,
  physics: const PositionRetainedScrollPhysics(),
  itemBuilder: (context, index) => _items[index],
  itemCount: _itemCount,
);

Jump/animate to a ratio position in a viewport

_observer.showInViewport(
  _controller.position,
  alignment: 0.5,
);

By setting different alignment, you could jump/animate to the position according to the ratio: alignment.

  1. for alignment = 0.0, it would align the render object' leading to the leading of the viewport's main axis extent.
  2. for alignment = 0.5, it would align the render object's center to the center of the viewport;s main axis extent.
  3. for alignment = 1.0, it would align the render object's trailing to the trailing of the viewport's main axis extent.

you could also specify alignment as the number between [0, 1]

Pay attention

  • The item widget/builder must be wrapped using ObserverProxy
  • All observers would normalizeIndex to ensure the index is in a valid range determined by itemCount of observers, so developers should also update observers' itemCount when the scroll views' item count changes.
  • Items that have the same RenderObject observed by an observer should share the same observer instance, instead of creating different observers for each item.
  • When using ScrollObserver.boxMulti, axis is required so that the observer could estimate the scroll offset along the correct main axis.

Examples:

FAQ

TODO

Contributions

Feel free to contribute to this project.

If you find a bug or want a feature, but don't know how to fix/implement it, please fill an issue.

If you fixed a bug or implemented a feature, please send a pull request

indexed_scroll_observer's People

Contributors

simonwang9610 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

indexed_scroll_observer's Issues

[PR]: `ListWheelScrollView` and `SingleChildScrollView` support (break change)

Currently, all kinds of observers only support scroll views that have a RenderSlvier between items and RenderAbstractViewport, e.g., SliverList, SliverGrid, ListView ad GridView. Therefore, it would require tracking up to find the closest RenderSliver ancestor for observation.

However, such usages do not work on ListWheelScrollView and SingleChildScrollView that do not have a RenderSliver between the viewport and items. So this package should provide a better way to support such kinds of scroll views that do not depend on RenderSliver.

In order to differentiate the two kinds of scroll views (one relying on RenderSliver, while the other does not rely on RenderSliver), this package could create two kinds of observers to support them respectively. For example:

  1. SliverScrollObserver for observing scroll views that rely on RenderSliver.
  2. BoxScrollObserver for observing scroll views that have no RenderSliver between items and its viewport.

Besides, PositionScrollController may make it a little bit complicated to manage multiple observers, so it would be removed directly. Developers should care about creating and managing observers manually, instead of delegating to PositionedScrollController centrally.

To summarize:

  • break changes

    1. PositionedScrollController is removed, and developers should create and manage different observers manually.
    2. Two kinds of observers: SliverScrollObserver and BoxScrollObserver are provided. In contrast, ScrollObserver would be changed into the observer factory that allows developers to create different observers conveniently.
    3. ListWheelScrollView and SingleChildScrollView are supported by using BoxScrollObserver. Developers could find examples at example/.
  • improvements

  1. better estimation for jumping/animating to a specific index. Particularly, items have a fixed item extent.

getVisibleItems question

Can getVisibleItems be added to check whether the display is complete?
Because now if an item is not fully displayed, it will still be recognized by the getVisibleItems method

ListView.separated is not supported

ListView.separated is not supported

Expanded(
child: ListView.separated(
controller: _controller,
physics: const BouncingScrollPhysics(),
itemBuilder: (context, index) => _items[index],
itemCount: _itemCount,
separatorBuilder: (BuildContext context, int index) {
return const VerticalDivider(color: Colors.yellow,width: 10,);
},
),
)

Estimation for GridView infinitely

Since the current estimation for GridView does not consider the cross count when using jumpToIndex and animateToIndex, it would try estimating the scroll offset infinitely when jumping or animating.

iShot_2023-03-16_11.10.21.mp4

The main reason is that each estimation for index cannot hit the exact scroll offset, and consequently, jumping or animating would estimate forever.

PositionRetainedScrollPhysics have a bug When item heights vary

this is code

class OfficialListExample extends StatefulWidget {
  const OfficialListExample({super.key});

  @override
  State<OfficialListExample> createState() => _OfficialListExampleState();
}

class _OfficialListExampleState extends State<OfficialListExample> {
  int _itemCount = 100;

  final ScrollController _controller = ScrollController();

  late final SliverScrollObserver _observer =
      MultiChildSliverObserver(itemCount: _itemCount);

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
  }

  late final List<Widget> _items = List.generate(_itemCount, (index) => index)
      .map(
        (index) => ObserverProxy(
          observer: _observer,
          child: ListTile(
            leading: const CircleAvatar(
              child: Text("L"),
            ),
            title: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text("Positioned List Example $index"),
                SizedBox(
                  height: index.toDouble(),
                )
              ],
            ),
          ),
        ),
      )
      .toList();

  @override
  void dispose() {
    _controller.dispose();
    _observer.clear();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        title: const Text("List View Example"),
      ),
      body: Column(
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              OutlinedButton(
                onPressed: () {
                  _observer.showInViewport(
                    _controller.position,
                    alignment: 0.0,
                  );
                },
                child: const Text("Go to start"),
              ),
              OutlinedButton(
                onPressed: () {
                  _observer.showInViewport(
                    _controller.position,
                    alignment: 0.5,
                  );
                },
                child: const Text("Go to middle"),
              ),
              OutlinedButton(
                onPressed: () {
                  _observer.showInViewport(
                    _controller.position,
                    alignment: 1.0,
                  );
                },
                child: const Text("Go to end"),
              ),
            ],
          ),
          SliverJumpWidget(
            label: "without animation",
            onJump: (index) {
              _observer.jumpToIndex(
                index,
                position: _controller.position,
              );
            },
          ),
          SliverJumpWidget(
            label: "animation",
            onJump: (index) {
              _observer.animateToIndex(
                index,
                position: _controller.position,
                duration: const Duration(milliseconds: 1000),
                curve: Curves.fastLinearToSlowEaseIn,
              );
            },
          ),
          Expanded(
            child: ListView.builder(
              controller: _controller,
              reverse: true,
              physics: const PositionRetainedScrollPhysics(),
              itemBuilder: (context, index) => _items[index],
              itemCount: _itemCount,
            ),
          )
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _addItem();
          // _observer.getVisibleItems(
          //   scrollExtent: ScrollExtent.fromPosition(_controller.position),
          // );
          // print(_observer.visibleRatioInViewport(
          //     ScrollExtent.fromPosition(_controller.position)));
        },
        child: const Icon(Icons.visibility_off_rounded),
      ),
    );
  }

  void _goStart() {
    _observer.showInViewport(_controller.position);
  }

  void _addItem() {
    _itemCount++;
    _observer.itemCount = _itemCount;
    _items.insert(
      0,
      ObserverProxy(
        observer: _observer,
        child: ListTile(
          leading: const CircleAvatar(
            child: Text("L"),
          ),
          title: Column(
            children: [
              Text("Positioned List Example $_itemCount"),
              SizedBox(
                height: _itemCount.toDouble(),
              )
            ],
          ),
        ),
      ),
    );

    setState(() {});
  }

  void _deleteItem() {
    _itemCount = max(--_itemCount, 0);
    _observer.itemCount = _itemCount;

    _items.removeLast();

    setState(() {});
  }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.