18

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 :/

7
  • 1
    Have you tried CustomScrollView and Slivers? Commented Mar 7, 2020 at 16:04
  • I didn't solve my solution with customscrollview. Commented Mar 8, 2020 at 2:48
  • 1
    I rewrite post, to make it more simple to read / understand Commented Mar 8, 2020 at 3:05
  • 1
    I think you can use NestedScrollView and put your red container inside headerSliverBuilder and the rest to the body of NestedScrollView. Commented Mar 8, 2020 at 7:01
  • 1
    An alternative is to use CustomScrollView and Slivers as Hamed said. SliverList instead of ListView which will avoid to use shrinkwrap and will allow each child to scroll the parent one. Commented Aug 2, 2022 at 21:45

6 Answers 6

30

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(),
Sign up to request clarification or add additional context in comments.

2 Comments

Note that using sliver could be an alternative for most of situation :).
if you face issue in ios check this solution. github.com/gsioteam/kinoko/issues/12 in List view builder put physics: Platform.isIOS ? const ClampingScrollPhysics(): const AlwaysScrollableScrollPhysics(),
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.

1 Comment

Timer is very tricky :'), but you show me a point a great thing : NotificationListener<OverscrollNotification>
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.

Comments

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]);
        })

Comments

-1

I hope this will be the best answer for you, Just iterate items inside the column like this

...(items.map((item)=>YourView());

1 Comment

2 solutions. The first one is NotificationListener which respond precisely to the main question. But, a better one is the second solution provided : the usage of Slivers to make this kind of UI.
-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

1 Comment

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 ;).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.