简体   繁体   中英

How to "merge" scrolls on a TabBarView inside a PageView?

I have an app that uses a PageView on its main page. Today, I got assigned to insert a TabBarView in one of these pages. The problem is that when I scroll the between the tabs when in the last tab, scrolling to the left won't scroll the PageView.

I need a way to make the scroll of page view scroll when at the start or end of the tabbarview.

I found a question with the inverted problem: flutter PageView inside TabBarView: scrolling to next tab at the end of page

However, the method stated there is not suitable to my issue.

I made a minimal example:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'TabBarView inside PageView',
        home: MyHomePage(),
      );
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final PageController _pageController = PageController();

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(
          title: Text('TabBarView inside PageView'),
        ),
        body: PageView(
          controller: _pageController,
          children: <Widget>[
            Container(color: Colors.red),
            GreenShades(),
            Container(color: Colors.yellow),
          ],
        ),
      );
}

class GreenShades extends StatefulWidget {
  @override
  _GreenShadesState createState() => _GreenShadesState();
}

class _GreenShadesState extends State<GreenShades>
    with SingleTickerProviderStateMixin {
  TabController _tabController;

  @override
  void initState() {
    this._tabController = TabController(length: 3, vsync: this);
    super.initState();
  }

  @override
  Widget build(BuildContext context) => Column(
        children: <Widget>[
          TabBar(
            labelColor: Colors.green,
            indicatorColor: Colors.green,
            controller: _tabController,
            tabs: <Tab>[
              const Tab(text: "Dark"),
              const Tab(text: "Normal"),
              const Tab(text: "Light"),
            ],
          ),
          Expanded(
            child: TabBarView(
              controller: _tabController,
              children: <Widget>[
                Container(color: Colors.green[800]),
                Container(color: Colors.green),
                Container(color: Colors.green[200]),
              ],
            ),
          )
        ],
      );

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }
}

Note that, in this MRE, it's possible to reach the 3rd page if you drag the TabBar, but not if you drag the TabBarView.

How may I achieve this behavior?


Edit:

As stated by @Fethi, there's a similar question: Is it possible to swipe from an TabBarView content area to an adjacent PageView page?

However, the question was not answered satisfactorily, as the solution given does not really "blend" the scroll, although the behavior is similar to what was described. It doesn't scroll naturally.

This is possible by using the PageController.postion attribute's drag method, which internally drags the ScrollPosition of the screen. This way, user can intuitively drag the pages like drag halfway and then leave or continue fully.

The idea is inspired from the other post to use the OverScrollNotification but add rather more step to continue intuitive dragging.

  1. Collect the DragstartDetail when user starts scrolling.
  2. Listen for OverScrollNotification and start the draging and at the same time update the drag using the drag.update with the DragUpdateDetails from OverscrollNotification method.
  3. On ScrollEndNotification cancel the the drag.

To keep the idea simple I am pasting only build method of the Tabs page.

A fully working example is available in this dart pad .

演示

  @override
  Widget build(BuildContext context) {
    // Local dragStartDetail.
    DragStartDetails dragStartDetails;
    // Current drag instance - should be instantiated on overscroll and updated alongside.
    Drag drag;
    return Column(
      children: <Widget>[
        TabBar(
          labelColor: Colors.green,
          indicatorColor: Colors.green,
          controller: _tabController,
          tabs: <Tab>[
            const Tab(text: "Dark"),
            const Tab(text: "Normal"),
            const Tab(text: "Light"),
          ],
        ),
        Expanded(
          child: NotificationListener(
            onNotification: (notification) {
              if (notification is ScrollStartNotification) {
                dragStartDetails = notification.dragDetails;
              }
              if (notification is OverscrollNotification) {
                drag = _pageController.position.drag(dragStartDetails, () {});
                drag.update(notification.dragDetails);
              }
              if (notification is ScrollEndNotification) {
                drag?.cancel();
              }
              return true;
            },
            child: TabBarView(
              controller: _tabController,
              children: <Widget>[
                Container(color: Colors.green[800]),
                Container(color: Colors.green),
                Container(color: Colors.green[200]),
              ],
            ),
          ),
        ),
      ],
    );
  }

Old Answer

The above might not handle some edge cases. If you need more control below code provides the same result but you can handle UserScrollNotification . I am pasting this because, it might be useful for others who would like to know which direction the use is scrolling wrt the Axis of the ScrollView .

              if (notification is ScrollStartNotification) {
                dragStartDetails = notification.dragDetails;
              }

              if (notification is UserScrollNotification &&
                  notification.direction == ScrollDirection.forward &&
                  !_tabController.indexIsChanging &&
                  dragStartDetails != null &&
                  _tabController.index == 0) {
                _pageController.position.drag(dragStartDetails, () {});
              }

              // Simialrly Handle the last tab.
              if (notification is UserScrollNotification &&
                  notification.direction == ScrollDirection.reverse &&
                  !_tabController.indexIsChanging &&
                  dragStartDetails != null &&
                  _tabController.index == _tabController.length - 1) {
                _pageController.position.drag(dragStartDetails, () {});
              }

so you want to scroll the page view to the left when you reach the end of tabs and the same goes to scrolling to the right when on the first tab, what i have been thinking about is manually swipe the page view when in those cases as follow:

index value should the index of page that comes before the tab bar page and after it.

pageController.animateToPage(index,
          duration: Duration(milliseconds: 500), curve: Curves.ease); 

here is a complete code of what you are looking for, hopefully this helps!

I have a different approach using Listener Widget and TabView physics as show below:

//PageView Widget
@override
Widget build(BuildContext context) {
  return Scaffold(
   body: PageView(
    children: [
     Widge1()
     TabBarWidget(),
     Widget2()
    ]
   )
  )
}

//TabBar Widget
final _physycsNotifier = ValueNotifier<bool>(false);
....
....
@override
Widget build(BuildContext context) {
  return Column(
   children: [
    TabBar(
      controller: _tabController,
      //... other properties
    )
    Expanded(
      child: Listener(
       onPointerMove: (event) {
          final offset = event.delta.dx;
          final index = _tabController.index;

          //Check if we are in the first or last page of TabView and the notifier is false
          if(((offset > 0 && index == 0) || (offset < 0 && index == _categories.length - 1)) && !_physycsNotifier.value){
            _physycsNotifier.value = true;
          }
        },
        onPointerUp: (_) => _physycsNotifier.value = false;
        child: ValueListenableBuilder<bool>(
          valueListenable: _physycsNotifier,
          builder: (_, value, __) {
            return TabBarView(
              controller: _tabController,
              physics: value ? NeverScrollableScrollPhysics() : null,
              children: List.generate(_categories.length, (index) {
                return _CategoryTab(index: index);
              })
            );
          },
        ),
      )
    )
   ]
  )
}

this works fine if you set default physics for PageView and TabView (it means null) if you set other physisc like BouncingScrollPhsysisc there will be some bugs, but i think this is good workaround.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM