How to link two pageviews in flutter?
i.e. if one of them goes to page x the other should go to page x as well.
I thought two PageViews having the same controller would do the trick.
But that doesn't seem to be the case.
I tried having a list of controllers and when one of the pageviews' page changes, I'm calling jumpToPage on all the other pageviews' controllers but all the other PageViews are not in the widget runtime tree initially (They're outside the screen) thus giving out errors.
In my case PageView(children:[Pageview(...), Pageview(...)]) is the structure.
And after I open the other pageviews once, the errors are all gone but the current pageview is also getting jumped even though I removed it.
There're no infinite loops because of the other pageview's event firing at the same time.
/// Inside a stateful widget
PageView(
controller: widget.controller,
onPageChanged: (pno) {
widget.controllers.where((x) {
return x != widget.controllers[widget.idx];
}).forEach((colpv) {
colpv.controller?.jumpToPage(pno);
});
},
);
This is a minimal example that reproduces what I'm doing. It's in the ColPageView widget.
The full code
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
SystemChrome.setEnabledSystemUIOverlays([]);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Experiments',
theme: ThemeData.dark(),
home: MyHomePage(title: 'FlutterExps'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<PageControllerC> _controllers;
PageController _rowController;
PageController _mainController;
#override
void initState() {
_controllers = [
PageControllerC(
controller: PageController(keepPage: true),
recorded: 0,
),
PageControllerC(
controller: PageController(keepPage: true),
recorded: 1,
),
];
_controllers.forEach((f) {
f.controller.addListener(() {
print("Listener on ${f.recorded}");
});
});
_mainController = PageController();
_rowController = PageController();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
controller: _rowController,
children: [
ColPageView(
idx: 0,
controllers: _controllers,
controller: _mainController,
children: <Widget>[
ColoredWidget(
color: Colors.cyan,
direction: ">",
),
ColoredWidget(
color: Colors.orange,
direction: ">>",
),
],
),
ColPageView(
idx: 1,
controllers: _controllers,
controller: _mainController,
children: [
ColoredWidget(
color: Colors.green,
direction: "<",
),
ColoredWidget(
color: Colors.yellow,
direction: "<<",
),
],
),
],
),
);
}
}
class PageControllerC {
PageController controller;
int recorded;
PageControllerC({
this.recorded,
this.controller,
});
}
class ColPageView extends StatefulWidget {
final List<Widget> children;
final List<PageControllerC> controllers;
final int idx;
final PageController controller;
const ColPageView({
Key key,
this.children = const <Widget>[],
#required this.controllers,
#required this.idx,
this.controller,
}) : super(key: key);
#override
_ColPageViewState createState() => _ColPageViewState();
}
class _ColPageViewState extends State<ColPageView> {
#override
Widget build(BuildContext context) {
return PageView(
controller: widget.controllers[widget.idx].controller,
// controller: widget.controller,
scrollDirection: Axis.vertical,
children: widget.children,
onPageChanged: (pno) {
widget.controllers.where((x) {
return x != widget.controllers[widget.idx];
}).forEach((colpv) {
// if (colpv != widget.controllers[widget.idx]) {
colpv.controller?.jumpToPage(pno);
// }
// else{
print("memmem ${widget.idx}");
// }
});
print("col-${widget.idx} changed to $pno");
},
);
}
}
class ColoredWidget extends StatefulWidget {
final Color color;
final String direction;
const ColoredWidget({
Key key,
#required this.color,
#required this.direction,
}) : super(key: key);
#override
_ColoredWidgetState createState() => _ColoredWidgetState();
}
class _ColoredWidgetState extends State<ColoredWidget>
with AutomaticKeepAliveClientMixin<ColoredWidget> {
#override
Widget build(BuildContext context) {
super.build(context);
return Container(
color: widget.color,
child: Center(
child: Text(
widget.direction,
style: TextStyle(
fontSize: 100,
color: Colors.black,
),
),
));
}
#override
bool get wantKeepAlive => true;
}
I was able to link two pageviews given that they both reside in a pageview.
Note: They're discretely linked.
Maintain a list of controllers
Track the current vertical position in the HomePage widget.
And also the current horizontal pageview's position.
If a widget's page is being changed and it is visible in the viewport then make all other pages jump to where this goes. Check if it is in the widget tree before making it jump.
Else if it's not in the viewport don't apply the same callback as it should only be affected by the one in the viewport (or the currently scrolling one).
When initializing any pageview check the current vertical position and jump to that page.
This is not efficient as I'm keeping all the pageviews in the widget tree alive even if they are not visible. (I will update the answer if I come up with one that is efficient)
This is working because both pageviews are in a single pageview which is horizontal.
I will try to provide another example where both the pageviews are in the viewport (in a row for example) and the linking is continuous.
This can be extended to multiple page views and which leads to a fullscreen GridView.
Full code.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
SystemChrome.setEnabledSystemUIOverlays([]);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Experiments',
theme: ThemeData.dark(),
home: MyHomePage(title: 'FlutterExps'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<PageController> _controllers;
PageController _rowController;
ValueNotifier<int> _horizPage = ValueNotifier(0);
ValueNotifier<int> _vertPage = ValueNotifier(0);
#override
void initState() {
_controllers = [
PageController(keepPage: true),
PageController(keepPage: true),
];
_rowController = PageController();
_horizPage.value = _rowController.initialPage;
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
controller: _rowController,
onPageChanged: (pno) {
setState(() {
_horizPage.value = pno;
});
},
children: [
ColPageView(
idx: 0,
currHoriz: _horizPage,
vertstate: _vertPage,
controllers: _controllers,
children: <Widget>[
ColoredWidget(
color: Colors.cyan,
direction: ">",
),
ColoredWidget(
color: Colors.orange,
direction: ">>",
),
],
),
ColPageView(
idx: 1,
currHoriz: _horizPage,
vertstate: _vertPage,
controllers: _controllers,
children: [
ColoredWidget(
color: Colors.green,
direction: "<",
),
ColoredWidget(
color: Colors.yellow,
direction: "<<",
),
],
),
],
),
);
}
}
class ColPageView extends StatefulWidget {
final int idx;
final List<Widget> children;
final List<PageController> controllers;
final ValueNotifier<int> currHoriz;
final ValueNotifier<int> vertstate;
const ColPageView({
Key key,
this.children = const <Widget>[],
#required this.controllers,
#required this.currHoriz,
#required this.vertstate,
#required this.idx,
}) : super(key: key);
#override
_ColPageViewState createState() => _ColPageViewState();
}
class _ColPageViewState extends State<ColPageView> {
#override
void initState() {
widget.controllers[widget.idx] = PageController(
initialPage: widget.vertstate.value ?? 0,
keepPage: true,
);
super.initState();
}
#override
Widget build(BuildContext context) {
return PageView(
controller: widget.controllers[widget.idx],
scrollDirection: Axis.vertical,
children: widget.children,
onPageChanged: (widget.idx == widget.currHoriz.value)
? (pno) {
widget.controllers.forEach((colpv) {
if (colpv != widget.controllers[widget.idx]) {
if (colpv.hasClients && colpv.page != pno) {
colpv.jumpToPage(pno);
}
}
});
// Set latest vertical position
widget.vertstate.value = pno;
// print("col-${widget.idx} changed to $pno");
// set horizontal coord to be null
// As we've finished dealing with it
widget.currHoriz.value = null;
}
: null,
);
}
}
class ColoredWidget extends StatefulWidget {
final Color color;
final String direction;
const ColoredWidget({
Key key,
#required this.color,
#required this.direction,
}) : super(key: key);
#override
_ColoredWidgetState createState() => _ColoredWidgetState();
}
class _ColoredWidgetState extends State<ColoredWidget>
with AutomaticKeepAliveClientMixin<ColoredWidget> {
#override
Widget build(BuildContext context) {
super.build(context);
return Container(
color: widget.color,
child: Center(
child: Text(
widget.direction,
style: TextStyle(
fontSize: 100,
color: Colors.black,
),
),
));
}
#override
bool get wantKeepAlive => true;
}
Related
I use the following code snippet to create a tab bar with 20 tabs along with their views (you can copy-paste the code to try it out, it complies with no problems):
import 'package:flutter/material.dart';
class TabBody extends StatefulWidget {
final int tabNumber;
const TabBody({required this.tabNumber, Key? key}) : super(key: key);
#override
State<TabBody> createState() => _TabBodyState();
}
class _TabBodyState extends State<TabBody> {
#override
void initState() {
print(
'inside init state for ${widget.tabNumber}'); //<--- I want this line to execute only once
super.initState();
}
getDataForTab() {
//getting data for widget.tabNumber
}
#override
Widget build(BuildContext context) {
return Center(
child: Container(
color: Colors.grey,
child: Text('This is tab #${widget.tabNumber} body')),
);
}
}
class MainPage extends StatefulWidget {
const MainPage({Key? key}) : super(key: key);
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
List<Text> get _tabs {
var list = [for (var i = 0; i < 20; i += 1) i];
List<Text> tabs = list.map((i) => Text('Tab Title $i')).toList();
return tabs;
}
List<TabBody> get _tabsBodies {
var list = [for (var i = 0; i < 20; i += 1) i];
List<TabBody> bodies = list.map((i) => TabBody(tabNumber: i)).toList();
return bodies;
}
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: _tabs.length,
child: Column(
children: <Widget>[
Container(
width: double.infinity,
height: 50,
color: Colors.black,
child: TabBar(
isScrollable: true,
tabs: _tabs,
),
),
Expanded(
child: TabBarView(
children: _tabsBodies, //<--- i want this to be one child only
),
)
],
),
);
}
}
I need to do the following but couldn't find a way for that:
I want to let the TabBarView to have only one child of type TabBody not a list of _tabsBodies, i.e. the print statement in initState should execute once.
I want to execute the function getDataForTab every time the tab is changed to another tab.
so in general I need to refresh the tab body page for each tab selection, in contrast to the default implementation of the DefaultTabController widget which requires to have n number of tab bodies for n number of tabs.
You'll need to do three things:
Remove the TabBarView. You don't need it if you want to have a single widget. (Having a TabBar does not require you to have a TabBarView)
Create your own TabController so you can pass the current index to the TabBody.
Listen to the TabController and update the state to pass the new index to TabBody.
Here's a fully runnable example and that you can copy and paste to DartPad
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatelessWidget {
final String title;
const MyHomePage({
Key? key,
required this.title,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: MainPage(),
);
}
}
class MainPage extends StatefulWidget {
const MainPage({Key? key}) : super(key: key);
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage>
with SingleTickerProviderStateMixin {
late final tabController =
TabController(length: 20, vsync: this, initialIndex: 0);
#override
void initState() {
super.initState();
tabController.addListener(() {
if (tabController.previousIndex != tabController.index && !tabController.indexIsChanging) {
print('setting state'); // <~~ will print one time now
setState(() {});
}
});
}
List<Text> get _tabs {
var list = [for (var i = 0; i < 20; i += 1) i];
List<Text> tabs = list.map((i) => Text('Tab Title $i')).toList();
return tabs;
}
#override
void dispose() {
tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Container(
width: double.infinity,
height: 50,
color: Colors.black,
child: TabBar(
isScrollable: true, tabs: _tabs, controller: tabController),
),
Expanded(
child: TabBody(tabNumber: tabController.index),
)
],
);
}
}
class TabBody extends StatefulWidget {
final int tabNumber;
const TabBody({required this.tabNumber, Key? key}) : super(key: key);
#override
State<TabBody> createState() => _TabBodyState();
}
class _TabBodyState extends State<TabBody> {
#override
void initState() {
print(
'inside init state for ${widget.tabNumber}'); //<--- I want this line to execute only once
super.initState();
}
getDataForTab() {
//getting data for widget.tabNumber
}
#override
Widget build(BuildContext context) {
return Center(
child: Container(
color: Colors.grey,
child: Text('This is tab #${widget.tabNumber} body')),
);
}
}
Few notes about the example:
when you create a TabController, you'll need a ticker. You can use SingleTickerProviderStateMixin to make the class itself a ticker (hence: vsync: this). Alternatively, you can create your own and pass it to TabController.vsync parameter.
class _MainPageState extends State<MainPage> with SingleTickerProviderStateMixin {
late final tabController = TabController(length: 20, vsync: this, initialIndex: 0);
Here we are listening to the tab controller whenever the tabs changes:
#override
void initState() {
super.initState();
tabController.addListener(() {
if (tabController.previousIndex != tabController.index && !tabController.indexIsChanging) {
print('setting state'); // <~~ will print one time now
setState(() {});
}
});
}
edit: you'll also need to dispose the tabController. I updated the code above.
I create swipe app for swipe Page1(), Page2() in main.dart which class Page1(), Page2() show in pages.dart. All code I use from this tutorial (github).
I want to create 30 pages. Can I use for loop to create 30 class ?
main.dart
class _MyHomePageState extends State<MyHomePage> {
int _seletedItem = 0;
var _pages = [Page1(), Page2()];
var _pageController = PageController();
pages.dart
class Page1 extends StatelessWidget{
#override
Widget build(BuildContext context) {
return Scaffold(body: Center(child: Text('First Page', style: TextStyle(fontSize: 50.0),),),);
}
}
class Page2 extends StatelessWidget{
#override
Widget build(BuildContext context) {
return Scaffold(body: Center(child: Text('Second Page',style: TextStyle(fontSize: 50.0),),),);
}
}
You can do it like this example here i am passing index as param to page but you can pass your custom data from your custom list too.
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _seletedItem = 0;
var _pageController = PageController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Page Controller looop - Demo'),
),
body: PageView(
children: List<Widget>.generate(1000,(i) => Page(i:i);,
onPageChanged: (index) {
setState(() {
_seletedItem = index;
});
},
controller: _pageController,
),
);
}
}
class Page extends StatelessWidget {
final i;
const Page({Key key, this.i}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(
'$i Page',
style: TextStyle(fontSize: 50.0),
),
),
);
}
}
In continuation with question
The solution provided above is good. But hard for me to implement in my project.
Expected results:
I've created two tabs.
In each tab I have SingleChildScrollView wrapped with Scrollbar.
I can not have the primary scrollcontroller in both the tabs, because that throws me exception: "ScrollController attached to multiple scroll views."
For Tab ONE I use primary scrollcontroller, for Tab TWO I created Scrollcontroller and attached it.
Widgets in both the tabs should be scrollabale using keyboard and mouse.
Actual results:
For Tab ONE with primary scrollcontroller I can scroll both by keyboard and dragging scrollbar.
But for Tab TWO with non primary scrollcontroller, I have to scroll only by dragging scrollbar. This tab doesn't respond to keyboard page up /down keys.
When keyboard keys are used in Tab TWO actually contents of tab ONE are getting scrolled.
Check code:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: TabExample(),
);
}
}
class TabExample extends StatefulWidget {
const TabExample({Key key}) : super(key: key);
#override
_TabExampleState createState() => _TabExampleState();
}
class _TabExampleState extends State<TabExample> {
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(icon: Text('Tab ONE')),
Tab(icon: Text('Tab TWO')),
],
),
title: Text('Tabs Demo'),
),
body: TabBarView(
children: [
WidgetC(),
WidgetD(),
],
),
),
);
}
}
class WidgetC extends StatefulWidget {
const WidgetC({Key key}) : super(key: key);
#override
_WidgetCState createState() => _WidgetCState();
}
class _WidgetCState extends State<WidgetC>
with AutomaticKeepAliveClientMixin<WidgetC> {
List<Widget> children;
#override
void initState() {
children = [];
for (int i = 0; i < 20; i++) {
children.add(
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Container(
height: 100,
width: double.infinity,
color: Colors.blue,
child: Center(child: Text('$i')),
),
),
);
}
super.initState();
}
#override
Widget build(BuildContext context) {
super.build(context);
return Scrollbar(
key: PageStorageKey('WidgetC'),
isAlwaysShown: true,
showTrackOnHover: true,
child: SingleChildScrollView(
child: Column(
children: children,
),
),
);
}
#override
bool get wantKeepAlive => true;
}
class WidgetD extends StatefulWidget {
const WidgetD({Key key}) : super(key: key);
#override
_WidgetDState createState() => _WidgetDState();
}
class _WidgetDState extends State<WidgetD>
with AutomaticKeepAliveClientMixin<WidgetD> {
List<Widget> children;
ScrollController _scrollController;
#override
void initState() {
_scrollController = ScrollController();
children = [];
for (int i = 0; i < 20; i++) {
children.add(
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Container(
height: 100,
width: double.infinity,
color: Colors.green,
child: Center(child: Text('$i')),
),
),
);
}
super.initState();
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
super.build(context);
return Scrollbar(
key: PageStorageKey('WidgetD'),
isAlwaysShown: true,
showTrackOnHover: true,
controller: _scrollController,
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: children,
),
),
);
}
#override
bool get wantKeepAlive => true;
}
This has been accepted as a bug in flutter.
Pl follow for progress here: https://github.com/flutter/flutter/issues/83711
Note for other developers facing same issue.
To overcome the mentioned problem, I changed my design layout. Instead of tabbar view I used Navigationrail widget. This solved my problem.
NavigationRail widget allowed me to attach primary scroll controller to multiple widgets without giving me exception: "ScrollController attached to multiple scroll views."
Sample code.
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
/// This is the main application widget.
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: MyStatefulWidget(),
);
}
}
/// This is the stateful widget that the main application instantiates.
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key key}) : super(key: key);
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
/// This is the private State class that goes with MyStatefulWidget.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _selectedIndex = 0;
WidgetC _widgetC = WidgetC();
WidgetD _widgetD = WidgetD();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('NavigationRail Demo'), centerTitle: true),
body: Row(
children: <Widget>[
NavigationRail(
elevation: 8.0,
selectedIndex: _selectedIndex,
onDestinationSelected: (int index) {
setState(() {
_selectedIndex = index;
});
},
labelType: NavigationRailLabelType.all,
groupAlignment: 0.0,
destinations: const <NavigationRailDestination>[
NavigationRailDestination(
icon: Icon(Icons.favorite_border),
selectedIcon: Icon(Icons.favorite),
label: Text('Tab ONE'),
),
NavigationRailDestination(
icon: Icon(Icons.bookmark_border),
selectedIcon: Icon(Icons.book),
label: Text('Tab TWO'),
),
],
),
const VerticalDivider(thickness: 1, width: 1),
// This is the main content.
Expanded(
child: _getPageAtIndex(_selectedIndex),
)
],
),
);
}
Widget _getPageAtIndex(int index) {
switch (index) {
case 0:
return _widgetC;
case 1:
return _widgetD;
}
return Container();
}
}
class WidgetC extends StatefulWidget {
const WidgetC({Key key}) : super(key: key);
#override
_WidgetCState createState() => _WidgetCState();
}
class _WidgetCState extends State<WidgetC>
with AutomaticKeepAliveClientMixin<WidgetC> {
List<Widget> children;
#override
void initState() {
children = [];
for (int i = 0; i < 20; i++) {
children.add(
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Container(
height: 100,
width: double.infinity,
color: Colors.primaries[Random().nextInt(Colors.primaries.length)],
child: Center(child: Text('$i')),
),
),
);
}
super.initState();
}
#override
Widget build(BuildContext context) {
super.build(context);
return Scrollbar(
key: PageStorageKey('WidgetC'),
isAlwaysShown: true,
showTrackOnHover: true,
child: SingleChildScrollView(
child: Column(
children: children,
),
),
);
}
#override
bool get wantKeepAlive => true;
}
class WidgetD extends StatefulWidget {
const WidgetD({Key key}) : super(key: key);
#override
_WidgetDState createState() => _WidgetDState();
}
class _WidgetDState extends State<WidgetD>
with AutomaticKeepAliveClientMixin<WidgetD> {
List<Widget> children;
// ScrollController _scrollController;
#override
void initState() {
// _scrollController = ScrollController();
children = [];
for (int i = 0; i < 20; i++) {
children.add(
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Container(
height: 100,
width: double.infinity,
color: Colors.primaries[Random().nextInt(Colors.primaries.length)],
child: Center(child: Text('$i')),
),
),
);
}
super.initState();
}
#override
void dispose() {
// _scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
super.build(context);
return Scrollbar(
key: PageStorageKey('WidgetD'),
isAlwaysShown: true,
showTrackOnHover: true,
// controller: _scrollController,
child: SingleChildScrollView(
// controller: _scrollController,
child: Column(
children: children,
),
),
);
}
#override
bool get wantKeepAlive => true;
}
I'd like to achieve a bidirectional Pageview or a fullscreen Gridview.
i.e. The layout would look like this.
| | |
| | |
_____|_______|_______|____
| | |
| | |
.....| i,j | i+1,j |.....
| | |
| | |
_____|_______|_______|_____
| | |
| | |
.....| i,j+1 |i+1,j+1|.....
| | |
| | |
_____|_______|_______|_____
| | |
| | |
| | |
Each i,j represents a fullscreen. Thus, the viewport of the device will only be able to view a particular (i,j)
at any point in time.
And from that position on swiping the screen
left, viewport goes to i+1, j
right, viewport goes to i-1, j
up, viewport goes to i, j+1
down, viewport goes to i, j-1
I'd like to specify the number of rows, columns. (Not just 4)
This is my code so far.
Which renders 4 such screens Video
(SVG)
(I haven't handled the controller logic)
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
// Hide the status bar
SystemChrome.setEnabledSystemUIOverlays([]);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Experiments',
theme: ThemeData.dark(),
home: MyHomePage(title: 'FlutterExps'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
children: [
PageView(
scrollDirection: Axis.vertical,
children: [
ColoredWidget(
color: Colors.cyan,
direction: ">",
),
ColoredWidget(
color: Colors.orange,
direction: ">>",
),
],
),
PageView(
scrollDirection: Axis.vertical,
children: [
ColoredWidget(
color: Colors.green,
direction: "<",
),
ColoredWidget(
color: Colors.yellow,
direction: "<<",
),
],
),
],
),
);
}
}
class ColoredWidget extends StatefulWidget {
final Color color;
final String direction;
const ColoredWidget({
Key key,
#required this.color,
#required this.direction,
}) : super(key: key);
#override
_ColoredWidgetState createState() => _ColoredWidgetState();
}
class _ColoredWidgetState extends State<ColoredWidget>
with AutomaticKeepAliveClientMixin<ColoredWidget> {
#override
Widget build(BuildContext context) {
super.build(context);
return Container(
color: widget.color,
child: Center(
child: Text(
widget.direction,
style: TextStyle(
fontSize: 100,
color: Colors.black,
),
),
));
}
#override
bool get wantKeepAlive => true;
}
But this clearly wouldn't work as I need to control all the connected adjacent PageViews etc.. which I don't understand how to proceed.
I was able to do it.
A minimal code on Github
// import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
SystemChrome.setEnabledSystemUIOverlays([]);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Experiments',
theme: ThemeData.dark(),
home: MyHomePage(title: 'FlutterExps'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<PageController> _controllers;
PageController _rowController;
// No need to use valuenotifiers here
// But I need pass by reference thus using it as a wrapper
ValueNotifier<int> currIdxNotifier = ValueNotifier(0);
ValueNotifier<int> currUpNotifier = ValueNotifier(0);
#override
void initState() {
_controllers = [
PageController(),
PageController(),
PageController(),
];
_rowController = PageController(
// initialPage: 0,
// keepPage: true,
);
currIdxNotifier.value = _rowController.initialPage;
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
// pageSnapping: true,
controller: _rowController,
onPageChanged: (pno) {
// MUST set state and trigger a rebuild
// As horizontal viewport changed
setState(() {
currIdxNotifier.value = pno;
// print("${currIdxNotifier.value} horizz");
});
},
children: [
ColPageView(
idx: 0,
currup: currUpNotifier,
notifier: currIdxNotifier,
controllers: _controllers,
children: <Widget>[
ColoredWidget(
color: Colors.orange[50],
text: "0, 0",
),
ColoredWidget(
color: Colors.orange[100],
text: "0, 1",
),
ColoredWidget(
color: Colors.orange[200],
text: "0, 2",
),
ColoredWidget(
color: Colors.orange[300],
text: "0, 3",
),
],
),
ColPageView(
idx: 1,
currup: currUpNotifier,
notifier: currIdxNotifier,
controllers: _controllers,
children: [
ColoredWidget(
color: Colors.green[100],
text: "1, 0",
),
ColoredWidget(
color: Colors.green[200],
text: "1, 1",
),
ColoredWidget(
color: Colors.green[300],
text: "1, 2",
),
ColoredWidget(
color: Colors.green[400],
text: "1, 3",
),
],
),
ColPageView(
idx: 2,
currup: currUpNotifier,
notifier: currIdxNotifier,
controllers: _controllers,
children: [
ColoredWidget(
color: Colors.teal[100],
text: "2, 0",
),
ColoredWidget(
color: Colors.teal[200],
text: "2, 1",
),
ColoredWidget(
color: Colors.teal[300],
text: "2, 2",
),
ColoredWidget(
color: Colors.teal[400],
text: "2, 3",
),
],
),
],
),
);
}
}
/// All the vertical pageviews are here
class ColPageView extends StatefulWidget {
final List<Widget> children;
final List<PageController> controllers;
final ValueNotifier<int> notifier;
final ValueNotifier<int> currup;
final int idx;
const ColPageView({
Key key,
this.children = const <Widget>[],
#required this.idx,
#required this.currup,
#required this.notifier,
#required this.controllers,
}) : super(key: key);
#override
_ColPageViewState createState() => _ColPageViewState();
}
class _ColPageViewState extends State<ColPageView> {
#override
void initState() {
// Just initialized
// Set the start value to be the current vertical value
widget.controllers[widget.idx] = PageController(
initialPage: widget.currup.value ?? 0,
keepPage: true,
);
// print("INIT STATE ${widget.idx}");
// print("INIT STATE ${widget.currup.value}");
super.initState();
}
#override
Widget build(BuildContext context) {
return PageView(
// pageSnapping: true,
controller: widget.controllers[widget.idx],
// controller: widget.controller,
scrollDirection: Axis.vertical,
children: widget.children,
onPageChanged: (widget.notifier.value == widget.idx)
? (pno) {
// if the global horizontal page is the current widget
// var rand = Random();
// var randnn = rand.nextDouble();
widget.controllers.forEach((colpv) {
if (widget.controllers[widget.idx] == colpv) {
// print("same widget so return $randnn");
return;
}
// https://github.com/flutter/flutter/issues/20621#issuecomment-445504085
// Only if the controller has clients
bool isSelected = colpv.hasClients
? colpv.page == pno
: colpv.initialPage == pno;
// Not the same page as everyone
if (!isSelected) {
// print("not selected");
if (colpv.hasClients) {
colpv.animateToPage(
pno,
duration: Duration(milliseconds: 200),
curve: Curves.easeIn,
);
}
}
// set the current updated value of the vertical coord
widget.currup.value = pno;
// print("$pno $isSelected");
});
// print("col-${widget.idx} changed to $pno");
// set horizontal coord to be null
// As we've finished dealing with it
widget.notifier.value = null;
}
: (_) {
// Others which are not the currently moving pageview
// SHOULD not have any listeners
// Spent 5hrs trying to figure this out
// print("nope ${widget.notifier.value} == ${widget.idx}");
},
);
}
}
/// A Widget that simply displays a color and an input text
/// NOTE: This is a StatefulWidget because needs to use keepalive
class ColoredWidget extends StatefulWidget {
/// Color to display the widget in
final Color color;
final String text;
const ColoredWidget({
Key key,
#required this.color,
#required this.text,
}) : super(key: key);
#override
_ColoredWidgetState createState() => _ColoredWidgetState();
}
class _ColoredWidgetState extends State<ColoredWidget>
with AutomaticKeepAliveClientMixin<ColoredWidget> {
#override
Widget build(BuildContext context) {
super.build(context);
return Container(
color: widget.color,
child: Center(
child: Text(
widget.text,
style: TextStyle(
fontSize: 100,
color: Colors.black,
),
),
));
}
// Need to use this or the state of the pageview will be lost
// In this case, if not using keepalive it would still work but
// it will scroll down every time the page gets changed horizontally
// TODO: Destroy if not next to current pageview
#override
bool get wantKeepAlive => true;
}
A similar question (by me) on how to link to pageviews.
I have two tabs, the left tab having a list of tiles and the right tab having nothing. The user can drag the screen from right-to-left or left-to-right to get from one tab to the other.
The left tab has a list of dismissible tiles that only have "direction: DismissDirection.startToEnd" (from left-to-right) enabled so that the user can still theoretically drag (from right-to-left) to go to the right tab.
However, I believe the Dismissible widget still receives the right-to-left drag information which is disabling the TabView drag to change tabs.
In essence, how do I allow the right-to-left drag to be detected by only the TabView and not the Dismissible item?
If an explicit solution/example with code snippets can be given, I would very very much appreciate the help!
Here's a paste for your main.dart file:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/semantics.dart';
void main() {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark(),
home: MainPage(),
);
}
}
class MainPage extends StatefulWidget {
#override
State<StatefulWidget> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage>
with SingleTickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
_tabController = TabController(vsync: this, length: 2, initialIndex: 1);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
color: Colors.black,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Expanded(
child: TabBarView(
controller: _tabController,
children: <Widget>[
TabWithSomething(),
TabWithNothing(),
],
),
),
],
),
),
),
);
}
}
class TabWithNothing extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Container(
child: Text("Swipe from left-to-right!"),
),
);
}
}
class TabWithSomethingItem implements Comparable<TabWithSomethingItem> {
TabWithSomethingItem({this.index, this.name, this.subject, this.body});
TabWithSomethingItem.from(TabWithSomethingItem item)
: index = item.index,
name = item.name,
subject = item.subject,
body = item.body;
final int index;
final String name;
final String subject;
final String body;
#override
int compareTo(TabWithSomethingItem other) => index.compareTo(other.index);
}
class TabWithSomething extends StatefulWidget {
const TabWithSomething({Key key}) : super(key: key);
static const String routeName = '/material/leave-behind';
#override
TabWithSomethingState createState() => TabWithSomethingState();
}
class TabWithSomethingState extends State<TabWithSomething> {
List<TabWithSomethingItem> TabWithSomethingItems;
void initListItems() {
TabWithSomethingItems =
List<TabWithSomethingItem>.generate(10, (int index) {
return TabWithSomethingItem(
index: index,
name: 'Item $index',
subject: 'Swipe from left-to-right to delete',
body: "Swipe from right-to-left to go back to old tab");
});
}
#override
void initState() {
super.initState();
initListItems();
}
void _handleDelete(TabWithSomethingItem item) {
setState(() {
TabWithSomethingItems.remove(item);
});
}
#override
Widget build(BuildContext context) {
Widget body;
body = ListView(
children:
TabWithSomethingItems.map<Widget>((TabWithSomethingItem item) {
return _TabWithSomethingListItem(
item: item,
onDelete: _handleDelete,
dismissDirection: DismissDirection.startToEnd,
);
}).toList());
return body;
}
}
class _TabWithSomethingListItem extends StatelessWidget {
const _TabWithSomethingListItem({
Key key,
#required this.item,
#required this.onDelete,
#required this.dismissDirection,
}) : super(key: key);
final TabWithSomethingItem item;
final DismissDirection dismissDirection;
final void Function(TabWithSomethingItem) onDelete;
void _handleDelete() {
onDelete(item);
}
#override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Semantics(
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
const CustomSemanticsAction(label: 'Delete'): _handleDelete,
},
child: Dismissible(
key: ObjectKey(item),
direction: dismissDirection,
onDismissed: (DismissDirection direction) => _handleDelete(),
background: Container(
color: theme.primaryColor,
child: const ListTile(
leading: Icon(Icons.delete, color: Colors.white, size: 36.0))),
child: Container(
decoration: BoxDecoration(
color: theme.canvasColor,
border: Border(bottom: BorderSide(color: theme.dividerColor))),
child: ListTile(
title: Text(item.name),
subtitle: Text('${item.subject}\n${item.body}'),
isThreeLine: true),
),
),
);
}
}
UPDATE:
I'm thinking we could change the "dismissible.dart" file to change the "TabControlller", but i'm not sure how I might do that.
In the "dismissible.dart" file:
...
void _handleDragUpdate(DragUpdateDetails details) {
if (!_isActive || _moveController.isAnimating)
return;
final double delta = details.primaryDelta;
if (delta < 0) print(delta); // thinking of doing something here
...