4

I want to nest PageViews in flutter, with a PageView inside a Scaffold inside a PageView. In the outer PageView I will have the logo and contact informations, as well as secundary infos. As a child, I will have a scaffold with the inner PageView and a BottomNavigationBar as the main user interaction screen. Here is the code I have so far:

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatefulWidget{

    @override
    State<StatefulWidget> createState() {
        return _MyAppState();
  }

}

class _MyAppState extends State<MyApp>{
    int index = 0;
    final PageController pageController = PageController();
    final Curve _curve = Curves.ease;
    final Duration _duration = Duration(milliseconds: 300);

    _navigateToPage(value){
        pageController.animateToPage(
            value,
            duration: _duration,
            curve: _curve
        );
        setState((){
            index = value;
        });
    }

    @override
    Widget build(BuildContext context) {

        return MaterialApp(
            title: 'PageViewCeption',
            home: PageView(
                children: <Widget>[
                    Container(
                        color: Colors.blue,
                    ),
                    Scaffold(
                        body: PageView(
                            controller: pageController,
                            onPageChanged: (page){
                                setState(() {
                                  index = page;
                                });
                            },
                            children: <Widget>[
                                Container(
                                    child: Center(
                                        child: Text('1', style: TextStyle(color: Colors.white))
                                    )
                                ),
                                Container(
                                    child: Center(
                                        child: Text('2', style: TextStyle(color: Colors.white))
                                    )
                                ),
                                Container(
                                    child: Center(
                                        child: Text('3', style: TextStyle(color: Colors.white))
                                    )
                                ),
                            ],
                        ),
                        backgroundColor: Colors.green,
                        bottomNavigationBar: BottomNavigationBar(
                            type: BottomNavigationBarType.fixed,
                            onTap: (value) =>_navigateToPage(value),
                            currentIndex: index,
                            items: [
                                BottomNavigationBarItem(
                                    icon: Icon(Icons.cake),
                                    title: Text('1')
                                ),
                                BottomNavigationBarItem(
                                    icon: Icon(Icons.cake),
                                    title: Text('2')
                                ),
                                BottomNavigationBarItem(
                                    icon: Icon(Icons.cake),
                                    title: Text('3')
                                )
                            ],
                        ),
                    ),
                    Container(
                        color: Colors.blue
                    )
                ],
            ),
        );
    }
}

Here is the result:

PageViewCeption

Problem is: When I am in the inner PageView, I can't get away from it to the outer one scrolling left on the first page, or scrolling right on the last page of the inner PageView. The only way to go back to the outer PageView in scrolling (swiping) on the BottomNavigationBar. In the docs of the Scroll Physics Class we find this in the description:

For example, determines how the Scrollable will behave when the user reaches the maximum scroll extent or when the user stops scrolling.

But I haven't been able to come up with a solution yet. Any thoughts?

Update 1

I had progress working with a CustomScrollPhysics class:

class CustomScrollPhysics extends ScrollPhysics{

     final PageController _controller;

     const CustomScrollPhysics(this._controller, {ScrollPhysics parent }) : super(parent: parent);

     @override
     CustomScrollPhysics applyTo(ScrollPhysics ancestor) {
       return CustomScrollPhysics(_controller, parent: buildParent(ancestor));
     }

     @override
     double applyBoundaryConditions(ScrollMetrics position, double value) {
       assert(() {
         if (value == position.pixels) {
           throw new FlutterError(
             '$runtimeType.applyBoundaryConditions() was called redundantly.\n'
             'The proposed new position, $value, is exactly equal to the current position of the '
             'given ${position.runtimeType}, ${position.pixels}.\n'
             'The applyBoundaryConditions method should only be called when the value is '
             'going to actually change the pixels, otherwise it is redundant.\n'
             'The physics object in question was:\n'
             '  $this\n'
             'The position object in question was:\n'
             '  $position\n'
           );
         }
         return true;
       }());
       if (value < position.pixels && position.pixels <= position.minScrollExtent){ // underscroll
         _controller.jumpTo(position.viewportDimension + value);
         return 0.0;
       }
       if (position.maxScrollExtent <= position.pixels && position.pixels < value) {// overscroll
         _controller.jumpTo(position.viewportDimension + (value - position.viewportDimension*2));
         return 0.0;
       }
       if (value < position.minScrollExtent && position.minScrollExtent < position.pixels) // hit top edge
         return value - position.minScrollExtent;
       if (position.pixels < position.maxScrollExtent && position.maxScrollExtent < value) // hit bottom edge
         return value - position.maxScrollExtent;
       return 0.0;
     }
}

Which is a modification of the ClampingScrollPhysics applyBoundaryConditions. It kinda works but because of the pageSnapping it is really buggy. It happens, because according to the docs:

Any active animation is canceled. If the user is currently scrolling, that action is canceled.

When the action is canceled, the PageView starts to snap back to the Scafold page, if the user stop draggin the screen, and this messes things up. Any ideas on how to avoid the page snapping in this case, or for better implementation for that matter?

2
  • I cannot figure out how I could implement the same result with one big PageView. Any ideas?
    – Drugo
    Commented Jul 18, 2018 at 2:09
  • 1
    It's been a while, but I think nesting is possible. Just listen to the drag event of the scroll. (If you're on the on the last tab-slide (length of your tabs) disable inner scroll with * physics: leftDragActive ? NeverScrollableScrollPhysics() : PageScrollPhysics() * Let me know if it works :)
    – Markus
    Commented Apr 30, 2019 at 0:24

1 Answer 1

2

I was able to replicate the issue on the nested PageView. It seems that the inner PageView overrides the detected gestures. This explains why we're unable to navigate to other pages of the outer PageView, but the BottomNavigationBar can. More details of this behavior is explained in this thread.

As a workaround, you can use a single PageView and just hide the BottomNavigationBar on the outer pages. I've modified your code a bit.

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _MyAppState();
  }
}

class _MyAppState extends State<MyApp> {
  var index = 0;
  final PageController pageController = PageController();
  final Curve _curve = Curves.ease;
  final Duration _duration = Duration(milliseconds: 300);
  var isBottomBarVisible = false;

  _navigateToPage(value) {
    // When BottomNavigationBar button is clicked, navigate to assigned page
    switch (value) {
      case 0:
        value = 1;
        break;
      case 1:
        value = 2;
        break;
      case 2:
        value = 3;
        break;
    }
    pageController.animateToPage(value, duration: _duration, curve: _curve);
    setState(() {
      index = value;
    });
  }

  // Set BottomNavigationBar indicator only on pages allowed
  _getNavBarIndex(index) {
    if (index <= 1)
      return 0;
    else if (index == 2)
      return 1;
    else if (index >= 3)
      return 2;
    else
      return 0;
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'PageViewCeption',
      home: Scaffold(
        body: Container(
          child: PageView(
            controller: pageController,
            onPageChanged: (page) {
              setState(() {
                // BottomNavigationBar only appears on page 1 to 3
                isBottomBarVisible = page > 0 && page < 4;
                print('page: $page bottom bar: $isBottomBarVisible');
                index = page;
              });
            },
            children: <Widget>[
              Container(
                color: Colors.red,
              ),
              Container(
                color: Colors.orange,
              ),
              Container(
                color: Colors.yellow,
              ),
              Container(
                color: Colors.green,
              ),
              Container(color: Colors.lightBlue)
            ],
          ),
        ),
        bottomNavigationBar: isBottomBarVisible // if true, generate BottomNavigationBar
            ? new BottomNavigationBar(
                type: BottomNavigationBarType.fixed,
                onTap: (value) => _navigateToPage(value),
                currentIndex: _getNavBarIndex(index),
                items: [
                  BottomNavigationBarItem(icon: Icon(Icons.cake), label: '1'),
                  BottomNavigationBarItem(icon: Icon(Icons.cake), label: '2'),
                  BottomNavigationBarItem(icon: Icon(Icons.cake), label: '3')
                ],
              )
        //else, create an empty container to hide the BottomNavigationBar
            : Container(
                height: 0,
              ),
      ),
    );
  }
}

enter image description here

1
  • Thanks! Thats a great out-of-the-box solution! The only donwside I see is that the BottomNavigationBar animation does not follow the swipping, and just vanish when on the outside page. But I really likes this solution.
    – Drugo
    Commented Oct 1, 2020 at 16:08

Not the answer you're looking for? Browse other questions tagged or ask your own question.