to put it simply:
is there a way to have multiple sccrollable widgets (say, SingleSchildScrollView
) together in sync?
i just want 2 scrollables that can scroll the other as i scroll one.
this way i can use Stack
to put them on top of each other and the one behind can scroll following the front one.
or maybe put them in another set of Column
or Row
so that they are separate, but still scrolls by just scrolling either one.
i tried using controller
but it does not seems to be doing what i think it is.
Try the code below for example, the "RIGHT" will be in front of the "LEFT" and if i try to scroll them, only the RIGHT will move. so how do i move them both together at the same time??
please dont tell me to put the stack inside a ListView
, that is not what i need.
class _MyHomePageState extends State<MyHomePage> {
final ScrollController _mycontroller = new ScrollController();
@override
Widget build(BuildContext context) {
body:
Container(
height: 100,
child:
Stack( children: <Widget>[
SingleChildScrollView(
controller: _mycontroller,
child: Column( children: <Widget>[
Text('LEFT '),
Text('LEFT '),
Text('LEFT '),
Text('LEFT '),
Text('LEFT '),
Text('LEFT '),
],)
),
SingleChildScrollView(
controller: _mycontroller,
child: Column(children: <Widget>[
Text(' RIGHT'),
Text(' RIGHT'),
Text(' RIGHT'),
Text(' RIGHT'),
Text(' RIGHT'),
Text(' RIGHT'),
],)
),
])
)
}}
i believe this question has been asked before in multiple forums before but nobody has put a conclusion or solution to this at all. (see here)
You can swipe up and down on your stack to scroll through the widgets you added, or you can let iOS automatically switch between them for you.
A scroll controller creates a ScrollPosition to manage the state specific to an individual Scrollable widget. To use a custom ScrollPosition, subclass ScrollController and override createScrollPosition. A ScrollController is a Listenable.
I was just facing the same problem, and there's now an official package for that: linked_scroll_controller.
Using that package, you just have to create a master LinkedScrollControllerGroup
to keep track of the scroll offset and it'll then provide you separate ScrollController
s (kept in sync) via LinkedScrollControllerGroup.addAndGet()
.
i managed to sync multiple scrollables by using their offset
, utilizing their ScrollNotification
.
here's a rough code example:
class _MyHomePageState extends State<MyHomePage> {
ScrollController _mycontroller1 = new ScrollController(); // make seperate controllers
ScrollController _mycontroller2 = new ScrollController(); // for each scrollables
@override
Widget build(BuildContext context) {
body:
Container(
height: 100,
child: NotificationListener<ScrollNotification>( // this part right here is the key
Stack( children: <Widget>[
SingleChildScrollView( // this one stays at the back
controller: _mycontroller1,
child: Column( children: <Widget>[
Text('LEFT '),
Text('LEFT '),
Text('LEFT '),
Text('LEFT '),
Text('LEFT '),
Text('LEFT '),
],)
),
SingleChildScrollView( // this is the one you scroll
controller: _mycontroller2,
child: Column(children: <Widget>[
Text(' RIGHT'),
Text(' RIGHT'),
Text(' RIGHT'),
Text(' RIGHT'),
Text(' RIGHT'),
Text(' RIGHT'),
],)
),
]),
onNotification: (ScrollNotification scrollInfo) { // HEY!! LISTEN!!
// this will set controller1's offset the same as controller2's
_mycontroller1.jumpTo(_mycontroller2.offset);
// you can check both offsets in terminal
print('check -- offset Left: '+_mycontroller1.offset.toInt().toString()+ ' -- offset Right: '+_mycontroller2.offset.toInt().toString());
}
)
)
}}
basically each SingleChildScrollView
has its own controller
.
each controller
has their own offset
values.
use the NotificationListener<ScrollNotification>
to notify any movement, anytime they are scrolled.
then for each scroll gesture (i believe this is a frame by frame basis),
we can add jumpTo()
command to set the offset
s in anyway we like.
cheers!!
PS. if the list has different length, then the offset will be different and you will get a stack overflow error if you try to scroll past its limit. make sure to add some exceptions or error handling. (i.e. if else
etc.)
Thanks for your answer @Chris, I ran into the same problem and built my solution on top of yours. It works over multiple widgets and allows syncronized scrolling from any of the widget in the "group".
PSA: This seems to be working fine, but I'm just getting started and this might break in the most fabulous way you could imagine
It works using a NotificationListener<ScrollNotification>
plus an independent ScrollController
for each scrollable Widget that should be synchronized.
The class looks like this:
class SyncScrollController {
List<ScrollController> _registeredScrollControllers = new List<ScrollController>();
ScrollController _scrollingController;
bool _scrollingActive = false;
SyncScrollController(List<ScrollController> controllers) {
controllers.forEach((controller) => registerScrollController(controller));
}
void registerScrollController(ScrollController controller) {
_registeredScrollControllers.add(controller);
}
void processNotification(ScrollNotification notification, ScrollController sender) {
if (notification is ScrollStartNotification && !_scrollingActive) {
_scrollingController = sender;
_scrollingActive = true;
return;
}
if (identical(sender, _scrollingController) && _scrollingActive) {
if (notification is ScrollEndNotification) {
_scrollingController = null;
_scrollingActive = false;
return;
}
if (notification is ScrollUpdateNotification) {
_registeredScrollControllers.forEach((controller) => {if (!identical(_scrollingController, controller)) controller..jumpTo(_scrollingController.offset)});
return;
}
}
}
}
The idea is that you register each widgets ScrollController
with that helper, so that it'll have a reference to each widget that should be scrolled. You can do this by passing an array of ScrollController
s to the SyncScrollController
s constructor, or later on by calling registerScrollController
and passing the ScrollController
as a parameter to the function.
You'll need to bind the processNotification
method to the event handler of the NotificationListener
. That could probably be all implemented in a widget itself, but I'm not experienced enough for that yet.
Using the class would look something like this:
Creating fields for the scollControllers
ScrollController _firstScroller = new ScrollController();
ScrollController _secondScroller = new ScrollController();
ScrollController _thirdScroller = new ScrollController();
SyncScrollController _syncScroller;
Initalize the SyncScrollController
@override
void initState() {
_syncScroller = new SyncScrollController([_firstScroller , _secondScroller, _thirdScroller]);
super.initState();
}
Example of a complete NotificationListener
NotificationListener<ScrollNotification>(
child: SingleChildScrollView(
controller: _firstScroller,
child: Container(
),
),
onNotification: (ScrollNotification scrollInfo) {
_syncScroller.processNotification(scrollInfo, _firstScroller);
}
),
Obiously implement the above example for each scrollable widget and edit the SyncController
(parameter controller:) and processNotification
scrollController parameter (above _firstScroller) accordingly. You might implement some more fail safes, like checking _syncScroller != null
etc., above PSA applies :)
if your UI is simple wrap both scrolling widgets - lets say two Column - with an opposite widget - like Row - then with a SingleChildScrollView like this
SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Row(
children: [
//first column
Column(),
//second column
Column(),
],
),
)
if it is complicated, you can use scroll controllers like this
late final ScrollController scrollController1;
late final ScrollController scrollController2;
@override
void initState() {
scrollController1 = ScrollController();
scrollController2 = ScrollController();
scrollController1.addListener(() {
if (scrollController1.offset != scrollController2.offset) {
scrollController2.jumpTo(scrollController1.offset);
}
});
scrollController2.addListener(() {
if (scrollController1.offset != scrollController2.offset) {
scrollController1.jumpTo(scrollController2.offset);
}
});
super.initState();
}
and then add each scrollcontroller to each scrolling widget you have like this
SingleChildScrollView(
scrollDirection: Axis.vertical,
controller: scrollController1,
child: Column(), // column 1
)
SingleChildScrollView(
scrollDirection: Axis.vertical,
controller: scrollController2,
child: Column(), // column 2
)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With