17

I'm using Flutter version 1.12.13+hotfix.

I'm looking for a solution to be able to scroll inside a ListView and when reached the bottom, automatically give scroll lead to the parent ListView.

enter image description here

The first solution to achieve that is to use "physics: ClampingScrollPhysics()" with "shrinkWrap: true". So I apply this solution to all sub Listview except first one (the red) because I need to wrap it inside a sized Container().

The problem come from the first one... ClampingScrollPhysics() didn't work with sized Container() !

So, when I scroll the red Listview and reach its bottom, scroll stoping... I need to put my finger outside this ListView to be able again to scroll.

@override
  Widget build(BuildContext context) {
    super.build(context);

    print("build MySongs");

    return ListView(
      children: <Widget>[
        Container(
          height: 170,
          margin: EdgeInsets.all(16),
          child: ListView(
            children: <Widget>[
              Container(color: Colors.red, width: 100, height: 100, padding: EdgeInsets.all(8), margin: EdgeInsets.all(8)),
              Container(color: Colors.red, width: 100, height: 100, padding: EdgeInsets.all(8), margin: EdgeInsets.all(8)),
              Container(color: Colors.red, width: 100, height: 100, padding: EdgeInsets.all(8), margin: EdgeInsets.all(8)),
            ],
          ),
        ),
        Container(
          margin: EdgeInsets.all(16),
          child: ListView(
            physics: ClampingScrollPhysics(),
            shrinkWrap: true,
            children: <Widget>[
              Container(color: Colors.orange, width: 100, height: 100, padding: EdgeInsets.all(8), margin: EdgeInsets.all(8)),
              Container(color: Colors.orange, width: 100, height: 100, padding: EdgeInsets.all(8), margin: EdgeInsets.all(8)),
              Container(color: Colors.orange, width: 100, height: 100, padding: EdgeInsets.all(8), margin: EdgeInsets.all(8)),
            ],
          ),
        ),
        Container(
          margin: EdgeInsets.all(16),
          child: ListView(
            shrinkWrap: true,
            physics: ClampingScrollPhysics(),
            children: <Widget>[
              Container(color: Colors.blue, width: 100, height: 100, padding: EdgeInsets.all(8), margin: EdgeInsets.all(8)),
              Container(color: Colors.blue, width: 100, height: 100, padding: EdgeInsets.all(8), margin: EdgeInsets.all(8)),
              Container(color: Colors.blue, width: 100, height: 100, padding: EdgeInsets.all(8), margin: EdgeInsets.all(8)),
            ],
          ),
        ),
        Container(
          margin: EdgeInsets.all(16),
          child: ListView(
            physics: ClampingScrollPhysics(),
            shrinkWrap: true,
            children: <Widget>[
              Container(color: Colors.green, width: 100, height: 100, padding: EdgeInsets.all(8), margin: EdgeInsets.all(8)),
              Container(color: Colors.green, width: 100, height: 100, padding: EdgeInsets.all(8), margin: EdgeInsets.all(8)),
              Container(color: Colors.green, width: 100, height: 100, padding: EdgeInsets.all(8), margin: EdgeInsets.all(8)),
            ],
          ),
        ),
      ],
    );
  }

Maybe in need post this question on Flutter github issue :/

Eng
  • 1,617
  • 2
  • 12
  • 25

5 Answers5

27

Thanks for Hamed Hamedi solution :) ! I made a better solution, I think, based on NotificationListener ! (I discovered this functionnality thanks to him).

@override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(8),
      color: Colors.yellow,
      child: ListView.builder(
        controller: controller,
        itemBuilder: (c, i) =>
        i == 10
          ? Container(
          height: 150,
          color: Colors.red,
          child: NotificationListener<OverscrollNotification>(
            onNotification: (OverscrollNotification value) {
              if (value.overscroll < 0 && controller.offset + value.overscroll <= 0) {
                if (controller.offset != 0) controller.jumpTo(0);
                return true;
              }
              if (controller.offset + value.overscroll >= controller.position.maxScrollExtent) {
                if (controller.offset != controller.position.maxScrollExtent) controller.jumpTo(controller.position.maxScrollExtent);
                return true;
              }
              controller.jumpTo(controller.offset + value.overscroll);
              return true;
            },
            child: ListView.builder(
              itemBuilder: (c, ii) => Text('-->' + ii.toString()),
              itemCount: 20,
            ),
          ),
        )
          : Text(i.toString()),
        itemCount: 45,
      ),
    );
  }

The solution wrapped into StatelessWidget :

import 'package:flutter/material.dart';

class ScrollParent extends StatelessWidget {
  final ScrollController controller;
  final Widget child;

  ScrollParent({this.controller, this.child});

  @override
  Widget build(BuildContext context) {
    return NotificationListener<OverscrollNotification>(
      onNotification: (OverscrollNotification value) {
        if (value.overscroll < 0 && controller.offset + value.overscroll <= 0) {
          if (controller.offset != 0) controller.jumpTo(0);
          return true;
        }
        if (controller.offset + value.overscroll >= controller.position.maxScrollExtent) {
          if (controller.offset != controller.position.maxScrollExtent) controller.jumpTo(controller.position.maxScrollExtent);
          return true;
        }
        controller.jumpTo(controller.offset + value.overscroll);
        return true;
      },
      child: child,
    );
  }
}

To go further, take a look of other implementation of NotificationListener which can be useful for pagination :). You can try also this :

NotificationListener<ScrollStartNotification>(
  onNotification: (ScrollStartNotification value) {
    final ScrollMetrics metrics = value.metrics;
    if (!metrics.atEdge || metrics.pixels != 0) return true;
    print("Your callback here");
    return true;
  },
  child: child,
)

Or this :

NotificationListener<ScrollEndNotification>(
  onNotification: (ScrollEndNotification value) {
    final ScrollMetrics metrics = value.metrics;
    if (!metrics.atEdge || metrics.pixels == 0) return true;
    print("Your callback here");
    return true;
  },
  child: child,
)

if you face issue in ios check this solution. https://github.com/gsioteam/kinoko/issues/12

in List view builder put

physics: Platform.isIOS ? const ClampingScrollPhysics(): const AlwaysScrollableScrollPhysics(),
Nalawala Murtuza
  • 4,605
  • 1
  • 12
  • 21
Eng
  • 1,617
  • 2
  • 12
  • 25
  • Note that using sliver could be an alternative for most of situation :). – Eng Jan 16 '22 at 22:02
  • if you face issue in ios check this solution. https://github.com/gsioteam/kinoko/issues/12 in List view builder put physics: Platform.isIOS ? const ClampingScrollPhysics(): const AlwaysScrollableScrollPhysics(), – Nalawala Murtuza Dec 23 '22 at 05:25
8

A tricky way could be using NotificationListener. Put a Overscroll Notification Listener over your child scroll widget then ignore the pointer in case of overscroll. To let the child widget to scroll again in opposite direction, you have to set ignoring false after a short time. A detailed code sample:

class _MyHomePageState extends State<MyHomePage> {

  var _scrollParent = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        color: Colors.yellow,
        child: ListView.builder(
          itemBuilder: (c, i) => i == 10
              ? Container(
                  height: 150,
                  color: Colors.red,
                  child: IgnorePointer(
                    ignoring: _scrollParent,
                    child: NotificationListener<OverscrollNotification>(
                      onNotification: (_) {

                        setState(() {
                          _scrollParent = true;
                        });

                        Timer(Duration(seconds: 1), () {
                          setState(() {
                            _scrollParent = false;
                          });
                        });

                        return false;
                      },
                      child: ListView.builder(
                        itemBuilder: (c, ii) => Text('-->' + ii.toString()),
                        itemCount: 100,
                      ),
                    ),
                  ),
                )
              : Text(i.toString()),
          itemCount: 100,
        ),
      ),
    );
  }
}

There would be some flaws like double scrolling requirement by user to activate parent scroll event (first one will ignore the pointer), or using timer to disable ignoring that leads to misbehavior in fast scrolling actions. But the implementation simplicity towards other solutions would be immense.

Hamed Hamedi
  • 1,461
  • 1
  • 10
  • 19
  • 1
    Timer is very tricky :'), but you show me a point a great thing : NotificationListener – Eng Mar 10 '20 at 03:42
1

Thanks @Eng, your solution works. I found 1 issue with it that when I scroll fast (i.e with great velocity), when the inner scroller reaches it edge, the outer scroller will scroll but with no velocity at all (i.e jump to the given position and stop).

I managed to make it some what better but still not perfect with this code:

if (value.velocity > 0.1 || value.velocity < -0.1) {
          controller.animateTo(
              controller.offset + value.overscroll + value.velocity / 10,
              duration: const Duration(milliseconds: 200),
              curve: Curves.easeOut);
        } else {
          controller.jumpTo(
              controller.offset + value.overscroll + value.velocity / 4);
        }
        return true;

The ideal solution will be different I guess, because the velocity is the speed of the scrolling itself, so if the scroller is already at the edge the velocity will be zero no matter how fast I move my pointer to scroll.

Eran Ravid
  • 139
  • 1
  • 3
0

Lots of these solutions are overly complex. I ended up using the library, scroll_to_index and using a stateful widget to keep track of this behavior.

In my case my widget was getting built multiple times and I wanted it to just scroll 1x.

Here's a snippet:

final ItemScrollController scrollController = ItemScrollController(); 
bool hasScrolled = false;
.
[code omitted for brevity]
.
//in my widget being built:
scrollController: widget.scrollController,
        child: ScrollablePositionedList.builder(
            physics: Platform.isIOS ? const ClampingScrollPhysics(): const AlwaysScrollableScrollPhysics(),

            itemScrollController: widget.scrollController,
          itemCount: state.currentChapter.verses.length,
          itemBuilder: (context, i) {
            Future.delayed(Duration(milliseconds: 500)).then((value)  {
              if(widget.scrollController.isAttached && !hasScrolled) {
                widget.scrollController.jumpTo(index: state.currentVerse.verseNumber - 1);
                hasScrolled = true;
              }
            });
            return Verse(verse: state.currentChapter.verses[i]);
        })
cr1pto
  • 539
  • 3
  • 13
-5

Usually when I come across an issue like this, I use SingleChildScrollView with a column as the child, and then whatever I want as the children of that column. Here's some demo code.

SingleChildScrollView
(
  child: Column
  (
    children:
    [
     /* Your content goes here. */
    ]
  )
)

Let me know if that fits your use case

Abe
  • 421
  • 3
  • 11
  • I need to use ListView. I post my problem with a basic example. In my app, sub ListView data came from an api and the design is more complex. And, problem still same with Column when it was inside a sized Container ;). – Eng Mar 08 '20 at 03:10