How to synchronize two or more PageView with different controllers
Solution 1
I find a hacky solution inspired by @yusufpats's solution. I describe more detail below:
Add Listener to each PageController
This part is a little tricky because when the controller listen to each other, the page will actually stuck and unscrollable (Why?). I add bool to check which one is scrolling and need to be listen.
bool _isPage1Scrolling;
bool _isPage2Scrolling;
void initState() {
_isPage1Scrolling = false;
_isPage2Scrolling = false;
...
_controller1.addListener(() {
if(_isPage1Scrolling){
// Control _controller2
}
}
_controller2.addListener(() {
if(_isPage1Scrolling){
// Control _controller2
}
}
Control other PageController by how?
This is the most hard part because if I use animateTo
or jumpTo
, the "Controlled controller" looks very strange and not looks fluently. It is by design when user call these 2 functions, the page will always turn to "BallisticScrollActivity" after reach the position for a very short period (bounce back to stable page position). I found some solutions from ScrollPosition
inside the controller but seems only the last one can do well and no warning in result:
- jumpToWithoutSettling(value): Deprecated and may cause bug (which I am not understand yet)
- forcePixels(value): Protected function (Why I can still use it?)
- correctPixels(value): This will shift pixel without notifying. So I have to notify listener by my self.
I use offset
as the first page's shift and calculate the other page's shift by viewportFraction
// Control _controller2
// _controller2.position.jumpToWithoutSettling(...
// _controller2.position.forcePixels(...
_controller2.position.correctPixels(_controller1.offset * _controller2.viewportFraction / _controller1.viewportFraction);
_controller2.position.notifyListeners();
Finally listen to PageView itself
I use NotificationListener
but not GestureDetector
because the onTapDown
& onTapUp
are not fit-able for scrolling notification. Sometime there is no onTapDown
event when I touch and scroll very fast.
I research the notification type and find something inside:
- It give a UserScrollNotification with direction when I drag it from the begining
- It give another UserScrollNotification with direction is idle when page become stable
There may be a better way to detect it. I just use a simple way I can think of.
...
child: NotificationListener(
onNotification: (notification){
if(notification is UserScrollNotification){
if(notification.direction != ScrollDirection.idle){
(_controller2.position as ScrollPositionWithSingleContext).goIdle();
_isPage1Scrolling = true;
_isPage2Scrolling = false;
}
else{
_isPage1Scrolling = false;
}
}
return false;
},
child: PageView.builder(
...
Maybe someone notice the following line:
(_controller2.position as ScrollPositionWithSingleContext).goIdle();
This line is for a Edge case that If I drag the firs PageView and then drag anther PageView before the first one return to a stable position
. The first PageView's scroll position is still in a BallisticScrollActivity state and I need to force Idle it before I can control it.
Any suggestion is welcome!
Solution 2
So, you need to attach a listener on one of the PageView
widget's PageController
(_controller1
), and then change the offset
of the other PageView
widget's PageController
(_controller2
).
You need to add this piece of code in the initState
after initialising the controllers:
_controller1.addListener(() {
_controller2.jumpTo(_controller1.offset);
});
Updated answer (with page selected synchronisation):
_controller1.addListener(() {
_controller2.animateToPage(
_controller1.page.toInt(),
duration: Duration(milliseconds: 500),
curve: Curves.ease,
);
});
Updated answer (with detecting manual scroll):
import 'package:flutter/material.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyPageControllers(),
),
),
);
}
}
class MyPageControllers extends StatefulWidget {
@override
_MyPageControllersState createState() => _MyPageControllersState();
}
class _MyPageControllersState extends State<MyPageControllers> {
PageController _controller1;
PageController _controller2;
int manualController = -1;
Widget _itemBuilder(BuildContext context, int index) => Container(
color: Colors.primaries[index % Colors.primaries.length],
child: Center(
child: Text(
index.toString(),
style: TextStyle(color: Colors.white, fontSize: 60),
),
),
);
@override
void initState() {
super.initState();
_controller1 = PageController(viewportFraction: 0.8);
_controller2 = PageController(viewportFraction: 0.5);
_controller1.addListener(() {
if (manualController == 1) {
_controller2.jumpTo(_controller1.offset);
}
});
_controller2.addListener(() {
if (manualController == 2) {
_controller1.jumpTo(_controller2.offset);
}
});
// _controller1.addListener(() {
// _controller2.animateToPage(
// _controller1.page.toInt(),
// duration: Duration(milliseconds: 500),
// curve: Curves.ease,
// );
// });
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: GestureDetector(
onTapDown: (tapDownDetails){
manualController = 1;
setState(() {});
},
onTapUp: (tapUpDetails){
manualController = -1;
setState(() {});
},
child: PageView.builder(
controller: _controller1,
itemBuilder: _itemBuilder,
),
),
),
SizedBox(
height: 40,
),
Expanded(
child: GestureDetector(
onTapDown: (tapDownDetails){
manualController = 2;
setState(() {});
},
onTapUp: (tapUpDetails){
manualController = -1;
setState(() {});
},
child: PageView.builder(
controller: _controller2,
itemBuilder: _itemBuilder,
),
),
),
],
);
}
}
Comments
-
yellowgray over 1 year
I have 2 PageViews with different viewportFraction. Is there any way to scroll one of the PageViews and the other one is scrolled on the same page or offset?
Also ask, is it possible to control the PageView to middle of the offset in code?
class MyPageControllers extends StatefulWidget { @override _MyPageControllersState createState() => _MyPageControllersState(); } class _MyPageControllersState extends State<MyPageControllers> { PageController _controller1; PageController _controller2; Widget _itemBuilder(BuildContext context, int index) => Container( color: Colors.primaries[index % Colors.primaries.length], child: Center( child: Text( index.toString(), style: TextStyle(color: Colors.white, fontSize: 60), ), ), ); @override void initState() { super.initState(); _controller1 = PageController(viewportFraction: 0.8); _controller2 = PageController(viewportFraction: 0.5); } @override Widget build(BuildContext context) { return Column( children: [ Expanded( child: PageView.builder( controller: _controller1, itemBuilder: _itemBuilder, ), ), SizedBox( height: 40, ), Expanded( child: PageView.builder( controller: _controller2, itemBuilder: _itemBuilder, ), ), ], ); } }
-
yellowgray over 3 yearsThanks for the response. I already tried this. The controllers will actually stuck if they listen to each other at the same time. The offset from these 2 page are also not the same and the page will not result in the same page while scrolling.
-
yusufpats over 3 yearsIf you want the pages to be synchronised, you will have to attach a listener to the
_controller1
to listen to page change event, then set the new page con_controller2
-
yellowgray over 3 yearsDo you mean add same kind of listener to
_controller2
as the same code you write on_controller1
? -
yusufpats over 3 yearsSet the selected page of
_controller2
based on current page of_controller1
. See the updated answer above. -
yellowgray over 3 yearsHi, your answer only control
_controller2
with_controller1
. I want to control them with both side. Also the animation ofanimateToPage
makes it looks asynchronous. -
yusufpats over 3 yearsThe first answer, if you need both interdependent: You will have to attach the listener on
_controller2
as well and set value to_controller1
, like shown in the answer. Second, Yes it will make it look out of synchrony, as theanimateToPage
gets called after the new page has been set on_controller1
. If you need synchronising, you will need to use theoffset
method mentioned above, and adjust the_controller2
offset by interpolating the scroll based on widths of the items in the 2 PageViews. -
yellowgray over 3 yearsThanks for your explanation. For the second part I will try to find a solution with
offset
. The problem is still on the first part. If I add_controller1.addListener((){_controller2 ...
and_controller2.addListener((){_controller1 ...
at the same time, the scrolling will actually stuck (Maybe it recursively infinitely listen to each other?). -
yusufpats over 3 yearsThats sounds like an interesting problem, I have opened a new question for this here: stackoverflow.com/questions/64457895/… The way it worked for me is by wrapping the 2 pageviews in
GestureDetector
and keping a a track of which one is a manual scroll based ontapUp
andtapDown
. See the updated answer above.