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.
drag.update
with the DragUpdateDetails from OverscrollNotification method.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 w.r.t 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, () {});
}
Because its handled as part of OverScrolling for which the overscrolling has to occur. I felt this is good because, user should be intimated of the end of the tabs which otherwise is probably not so intuitive. But the core idea is using the
drag
method to drag.Optionally you can try wrapping the last tab element with a gesture detector maybe to initiate the drag say likeonTapDown
.I think I found a way. Give me some time.. I will perfect it and share.. :)
@MateusFelipe I have updated the answer. This is the best I could do.. Hope it helps you. You still might have to handle more cases. Perhaps you can ask your user for feedback and decide whether this smooth transition makes sense for them. :D
Thanks, man. I got a similar behavior before, but yours is better. The only flaw that both mine and your solution have is that It doesn't work well with fast swipes. Maybe you would like to update your dartpad?
Done.. I would say it has to be played with a lot to handle the drag cancel very well.. Fast dragging is an issue not with our solution but, the gesture fight between pageview and tabview itself.. if they expose an api to actually explicitly stop page view from listening then it can be done.