Search code examples
flutterdartscrollflutter-sliver

Flutter controll scrolling priority


I have a code that has these main widgets:

  CustomScrollView(
    physics: const AlwaysScrollableScrollPhysics(
      parent: BouncingScrollPhysics(),
    ),
    slivers: [
      SliverFillRemaining(
        hasScrollBody: true,
        child: ListView(
          children: [],
        ),
      ),
    ],
  ),

I have it this way because around the ListView widget I have a Column so that on top of I have a widget that simulates a title.

I chose to work with all of them this way so that when my list has 2-3 items, the entire list and title show on the centre of the screen, and the outer scroll is bouncing with the title.

When the list is longer, what I wanted to accomplish was almost what I got, but I want to know if I'm able to control the scrolling with these rules:

  1. Scroll the list;
  2. If the list has ended (either top or bottom), scroll the CustomScrollView

Edit

Here is a link for you to see what I mean. Test on Chrome mobile view so you can actually see the physics in place.

What I'm asking is that when my list has more items than can fit the screen, I mainly scroll my list, but when it gets to the bottom or the top, it lets my CustomScrollView handle the physics.

More explanations on the code are here.


Solution

  • I haven't delved that deep into scrolling yet, so i can only tell you what my approach would be. And i hope i understand your wanted behaviour correctly.

    Similar to your example snippet use a CustomScrollView with SliverFixedExtentList for the parts of your outer list.

    If you now want an inner list in the middle that is scrollable as well, use SliverToBoxAdapter with SizedBox.

    And if you want an inner scrollable list at the end that is expanded to the remaining screen space, then use SliverFillRemaining.

    To now scroll the outer list if the inner lists reach the scroll end, use a ScrollController with OverscrollNotification.

    And then you have inner scrollable lists that scroll the outer list when they reach the end of their own scroll:

    Widget _build(BuildContext context) {
      final ScrollController outerController = ScrollController(); // todo: this scroll controller should be created
      // inside of your state object instead!
    
      return CustomScrollView(
        controller: outerController,
        slivers: <Widget>[
          SliverFixedExtentList(
            itemExtent: 100,
            delegate: SliverChildBuilderDelegate(
              (BuildContext context, int index) => Container(color: Colors.red[(index % 4) * 200 + 200], height: 100),
              childCount: 10,
            ),
          ),
          SliverToBoxAdapter(
            child: SizedBox(
              height: 300,
              child: NotificationListener<OverscrollNotification>(
                child: ListView.builder(
                  itemBuilder: (BuildContext context, int index) =>
                      Container(color: Colors.green[(index % 4) * 200 + 200], height: 100),
                  itemCount: 10,
                ),
                onNotification: (OverscrollNotification notification) {
                  final double newOffset = outerController.offset + notification.overscroll;
                  outerController.jumpTo(newOffset);
                  return true;
                },
              ),
            ),
          ),
          SliverFillRemaining(
            hasScrollBody: true,
            child: NotificationListener<OverscrollNotification>(
              child: ListView.builder(
                itemBuilder: (BuildContext context, int index) =>
                    Container(color: Colors.blue[(index % 4) * 200 + 200], height: 100),
                itemCount: 10,
              ),
              onNotification: (OverscrollNotification notification) {
                final double newOffset = outerController.offset + notification.overscroll;
                if (newOffset < outerController.position.maxScrollExtent &&
                    newOffset > outerController.position.minScrollExtent) {
                  // todo: this if condition prevents bouncy scrolling which is a bit weird without better physics
                  // calculations
                  outerController.jumpTo(newOffset);
                }
                return true;
              },
            ),
          ),
        ],
      );
    }
    

    Edit: is this getting close to what you want? (Still has a bit buggy bouncy scrolling and not the best physics).

    class CustomScroll extends MaterialScrollBehavior {
      @override
      Set<PointerDeviceKind> get dragDevices => {PointerDeviceKind.touch, PointerDeviceKind.mouse};
    
      @override
      Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
        return child;
      }
    
      @override
      Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) {
        return child;
      }
    }
    
    void main() {
      runApp(MaterialApp(
        scrollBehavior: CustomScroll(),
        home: Scaffold(body: _build()),
      ));
    }
    
    extension RandomColorExt on Random {
      Color nextColor() {
        return Color.fromARGB(255, nextInt(256), nextInt(256), nextInt(256));
      }
    }
    
    final Random random = Random(DateTime.now().microsecondsSinceEpoch);
    final ScrollController outerController = ScrollController();
    
    Widget _build() {
      return LayoutBuilder(
        builder: (BuildContext context, BoxConstraints constraints) {
          return SingleChildScrollView(
            physics: const AlwaysScrollableScrollPhysics(
              parent: BouncingScrollPhysics(),
            ),
            controller: outerController,
            child: SizedBox(
              height: constraints.maxHeight,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  const Center(
                    child: Padding(
                      padding: EdgeInsets.all(32),
                      child: Text('Title'),
                    ),
                  ),
                  Flexible(
                    child: NotificationListener<OverscrollNotification>(
                      child: ListView.builder(
                        itemCount: 20,
                        shrinkWrap: true,
                        itemBuilder: (BuildContext context, int index) {
                          return Padding(
                            padding: const EdgeInsets.all(8),
                            child: ColoredBox(
                              color: random.nextColor(),
                              child: const SizedBox(height: 50),
                            ),
                          );
                        },
                      ),
                      onNotification: (OverscrollNotification notification) {
                        final double newOffset = outerController.offset + notification.overscroll;
                        outerController.jumpTo(newOffset);
                        return true;
                      },
                    ),
                  ),
                ],
              ),
            ),
          );
        },
      );
    }