Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nesting PageView: Change the outer PageView when the inner PageView reaches the end

Tags:

flutter

Consider a PageView usage where we have one PageView inside another one:

PageView(
  children: [
    Container(color: Colors.red),
    Column(
      children: [
        Text('Title'),
        PageView(
          children: [
            Container(color: Colors.green),
            Container(color: Colors.yellow),
          ],
        ),
      ],
    )
  ],
);

Such architecture will allow us to transition between the outer PageView to the inner PageView. But once we reached the inner PageView we can't get out anymore.

Here's a gif showcasing it:

enter image description here

In such architecture, we can't "merge" both PageViews into one, because the inner PageView doesn't fill the screen

How could we achieve a similar result while allowing to transition from both PageView without getting stuck?

like image 203
Rémi Rousselet Avatar asked Nov 15 '22 15:11

Rémi Rousselet


1 Answers

I have a workaround using NotificationListener by controlling the page view with PageController. The user actually control the lower PageView and pass the offset to the higher PageView.

final _pageController1 = PageController(); // Controlled
final _pageController2 = PageController(); // Controller

@override
Widget build(BuildContext context) {
  var leftOverScroll = 0.0; // total over-scroll offset on left side
  var rightOverScroll = 0.0;

  return PageView(
    physics: const ClampingScrollPhysics(),
    controller: _pageController1,
    children: [
      Container(color: Colors.red),
      Column(
        children: [
          const Text('Title'),
          Expanded(
            child: NotificationListener(
              onNotification: (notification) {
                // over scroll to the left side
                if (notification is OverscrollNotification &&
                    notification.overscroll < 0) {
                  leftOverScroll += notification.overscroll;
                  _pageController1.position.correctPixels(
                      _pageController1.position.pixels +
                          notification.overscroll);
                  _pageController1.position.notifyListeners();
                }

                // scroll back after left over scrolling
                if (leftOverScroll < 0) {
                  if (notification is ScrollUpdateNotification) {
                    final newOverScroll = math.min(
                        notification.metrics.pixels + leftOverScroll, 0.0);
                    final diff = newOverScroll - leftOverScroll;
                    _pageController1.position.correctPixels(
                        _pageController1.position.pixels + diff);
                    _pageController1.position.notifyListeners();
                    leftOverScroll = newOverScroll;
                    _pageController2.position.correctPixels(0);
                    _pageController2.position.notifyListeners();
                  }
                }

                // release left
                if (notification is UserScrollNotification &&
                    notification.direction == ScrollDirection.idle &&
                    leftOverScroll != 0) {
                  _pageController1.previousPage(
                      curve: Curves.ease,
                      duration: const Duration(milliseconds: 400));
                  leftOverScroll = 0;
                }

                // over scroll to the right side
                if (notification is OverscrollNotification &&
                    notification.overscroll > 0) {
                  rightOverScroll += notification.overscroll;
                  _pageController1.position.correctPixels(
                      _pageController1.position.pixels +
                          notification.overscroll);
                  _pageController1.position.notifyListeners();
                }

                // scroll back after right over scrolling
                if (rightOverScroll > 0) {
                  if (notification is ScrollUpdateNotification) {
                    final maxScrollExtent =
                        notification.metrics.maxScrollExtent;
                    final newOverScroll = math.max(
                        notification.metrics.pixels +
                            rightOverScroll -
                            maxScrollExtent,
                        0.0);
                    final diff = newOverScroll - rightOverScroll;
                    _pageController1.position.correctPixels(
                        _pageController1.position.pixels + diff);
                    _pageController1.position.notifyListeners();
                    rightOverScroll = newOverScroll;
                    _pageController2.position.correctPixels(maxScrollExtent);
                    _pageController2.position.notifyListeners();
                  }
                }

                // release right
                if (notification is UserScrollNotification &&
                    notification.direction == ScrollDirection.idle &&
                    rightOverScroll != 0) {
                  _pageController1.nextPage(
                      curve: Curves.ease,
                      duration: const Duration(milliseconds: 400));
                  rightOverScroll = 0;
                }

                return false;
              },
              child: PageView(
                physics: ClampingScrollPhysics(),
                controller: _pageController2,
                children: [
                  Container(color: Colors.green),
                  Container(color: Colors.yellow),
                ],
              ),
            ),
          ),
        ],
      ),
      Container(color: Colors.blue),
    ],
  );
}

For the part how to avoid the notifications come from the other child widget, I can only think of wrapping it with another NotificationListener

NotificationListener(
  onNotification: (_)=> true,
  child: ListView.builder(
    scrollDirection: Axis.horizontal,
    itemBuilder: (ctx,index) => Container(width: 50,color: Colors.primaries[index%Colors.primaries.length],),
  ),
),

Result:

enter image description here

At the end:

  • I only handle the left side over scroll of the PageView
  • To keep it simple, I don't implement the PageScrollPhysic when the drag come to end (calculated by velocity and page position).
like image 126
yellowgray Avatar answered May 24 '23 16:05

yellowgray