Scroll multiple scrollable widgets in sync
Asked Answered
F

6

21

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)

Femmine answered 25/2, 2019 at 5:6 Comment(6)
@pskink i just want to know how to scroll 2 widgets together. ultimately i wanted to make something like thisFemmine
@pskink hey, i cant seem to open your link. can you paste the content to answer this question instead?Femmine
@pskink hey!! your piece of code is helping me out. its kinda working, with my previous pieces of codes i tried.. so far its looking good, i will try to add this to my project and see if it works as expected.. WILL UPDATE SOON!!!!Femmine
as i said: you should not follow that way: its a bad workaroundAmoy
@Amoy but so far its the only solution that has been doing what i wanted.Femmine
@Amoy okay then. but thank you anyway :) it still helped me understand flutter better. cheers mate!!Femmine
F
12

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 offsets 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.)

Femmine answered 26/2, 2019 at 6:56 Comment(1)
@sivakumar check out my answer, that should take care of your problemHypercorrect
H
15

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 ScrollControllers (kept in sync) via LinkedScrollControllerGroup.addAndGet().

Hepsiba answered 29/3, 2020 at 14:56 Comment(3)
I tested it, and it is nice if all the scrollable widgets have the same height. But my widgets have different heights, and I need something that doesn't take the actual pixel count, but the scrolled percentage. If someone has a solution for me please let me know.Skean
For some details about using linked_scroll_controller you can check this blog post: flutter-demo.net/2022/12/…Moorland
That package is also third party, with all the issues involved with maintaining a project that relies on other people's code.Acetal
F
12

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 offsets 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.)

Femmine answered 26/2, 2019 at 6:56 Comment(1)
@sivakumar check out my answer, that should take care of your problemHypercorrect
C
6

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
)
Carrick answered 6/8, 2022 at 14:46 Comment(0)
H
5

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 ScrollControllers to the SyncScrollControllers 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 :)

Hypercorrect answered 30/6, 2019 at 13:20 Comment(3)
Continuing down the road, I see why this would be not so optimal, performance suffers clearly if things start to growHypercorrect
Man your solution with listening is so awesome. I like the way you've get rid of recursive scroll position changes and handled it as a separate Sync class.Hoeve
I needed to set the initial offset for the scroll controllers and the linked_scroll_controller does not seem to support that now. This approach works in my case since I can set the offset for each ScrollController. Thanks!Silma
R
1

I know that's an ancient question, but might be useful for someone .. the package linked_scroll_controller is exactly what you're looking for, it performs much better than the NotificationListener or adding a listener to your scrollController and using .jump ..

  late ScrollController controller1;
  late ScrollController controller2;
  late LinkedScrollControllerGroup parentController;
  
  
  @override
  void initState() {
    super.initState();
    parentController = LinkedScrollControllerGroup();
    controller1 = parentController.addAndGet();
    controller2 = parentController.addAndGet();
  }

and that's it, connect controller1 to first scrollable widget, and controller2 to the other, and they're synced.

Rhyolite answered 18/8, 2023 at 23:24 Comment(2)
Could you explain why the linked_scroll_controller package performs better than using NotificationListener ? It still uses .jumpTo. Unless I'm missing something it also doesn't handle cases where one scroll should move faster than the other.Acetal
Still looking for details. I see many notification listeners in its code, so why does it perform better?Acetal
M
-2

I found this one and worked for me

static final tracking = TrackingScrollController();

// implementation
child: ListView(
  controller: tracking,

and work on multiple pageview also.

Marqueritemarques answered 22/1, 2021 at 18:8 Comment(3)
Hi @malik kurosaki, could you expand your answer, it does't work. Why did you make it as static?Stater
This is actually the best answer when using PageView and scroll views on each page that should be synchronized.Mariellamarielle
Does it make any sense? One instance of a ScrollController can not be used for multiple scrollingSampling

© 2022 - 2025 — McMap. All rights reserved.