多个可滚动小部件同步滚动

20

简言之:

有没有一种方法可以使多个可滚动的小部件(例如SingleSchildScrollView)在一起同步滚动?


我只想要两个可滚动的小部件,当我滚动一个时,另一个也会滚动。

这样我就可以使用Stack将它们放在彼此上方,后面的一个可以跟随前面的一个滚动。

或者将它们放在另一组ColumnRow中,以便它们分开,但仍然可以通过滚动其中任何一个来滚动。

我尝试使用controller但它似乎不是我想象中的那样工作。


例如,请尝试下面的代码,“RIGHT”将在“LEFT”的前面,如果我尝试滚动它们,则只有RIGHT会移动,那么如何同时将它们移动??

请不要告诉我将堆栈放入ListView中,那不是我所需要的。

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'),
              ],)
            ),
          ])
      )
}}

我相信这个问题之前已经在多个论坛上提出过,但是没有人得出结论或解决方案。(请参见这里


1
为什么要使用如此奇怪的UX设计?为什么不使用一个单一的滚动视图呢? - pskink
1
@pskink,我只是想知道如何同时滚动两个小部件。最终,我想做出类似于这个的东西。 - Chris
@pskink,嘿,我似乎无法打开你的链接。你能否将内容粘贴到回答这个问题的地方? - Chris
1
你应该改变你的UI设计。 - pskink
@pskink 好的,谢谢你 :) 这仍然帮助我更好地理解Flutter。干杯,伙计!! - Chris
显示剩余3条评论
6个回答

14

1
我已经测试过了,如果所有可滚动的小部件具有相同的高度,那么效果很好。但是我的小部件高度不同,我需要的是一种不依赖实际像素计数而是滚动百分比的解决方案。如果有人有解决方案,请告诉我。 - Hyung Tae Carapeto Figur
关于如何使用 linked_scroll_controller 的详细信息,您可以查看此博客文章:https://www.flutter-demo.net/2022/12/how-to-create-linked-scroll-widgets.html - Paulo Belo

12

我通过使用多个可滚动控件的offset和它们的ScrollNotification来成功同步它们的滚动。

以下是一个简单的代码示例:

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()); 
          }
        )
      )
}}

基本上每个 SingleChildScrollView 都有它自己的 controller。 每个 controller 都有它们自己的 offset 值。 使用 NotificationListener<ScrollNotification> 来通知任何移动,无论何时滚动。

然后对于每个滚动手势(我相信这是基于逐帧的基础), 我们可以添加 jumpTo() 命令以任何我们喜欢的方式设置 offset

干杯!!

PS. 如果列表长度不同,则偏移量将不同,如果尝试滚动超过其限制,将导致堆栈溢出错误。确保添加一些异常或错误处理。(即 if else 等)。


1
@sivakumar请查看我的答案,这应该可以解决你的问题。 - Kimmax

5

感谢@Chris的答案,我遇到了同样的问题,并在您的解决方案基础上构建了我的解决方案。它可以在多个小部件上运行,并允许从“group”中的任何小部件进行同步滚动。


公告:这似乎很正常,但我刚开始尝试,这可能会以你想象不到的最棒的方式崩溃


它使用NotificationListener<ScrollNotification>和每个可滚动小部件的独立ScrollController来实现同步。
该类看起来像这样:

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;
      }
    }
  }
}

这个想法是在帮助类中注册每个小部件的 ScrollController ,以便它将引用应该被滚动的每个小部件。您可以通过将一组 ScrollController 传递给 SyncScrollController 的构造函数或稍后调用 registerScrollController 并将 ScrollController 作为参数传递给该函数来实现此操作。
您需要将 processNotification 方法绑定到 NotificationListener 的事件处理程序上。这可能完全可以在小部件本身中实现,但我还没有足够的经验。

使用该类的方式如下:

为滚动控制器创建字段

  ScrollController _firstScroller = new ScrollController();
  ScrollController _secondScroller = new ScrollController();
  ScrollController _thirdScroller = new ScrollController();

  SyncScrollController _syncScroller;

初始化 SyncScrollController

@override
void initState() {
  _syncScroller = new SyncScrollController([_firstScroller , _secondScroller, _thirdScroller]);
  super.initState();
}

完整 NotificationListener 示例

NotificationListener<ScrollNotification>(
  child: SingleChildScrollView(
    controller: _firstScroller,
    child: Container(
    ),
  ),
  onNotification: (ScrollNotification scrollInfo) {
    _syncScroller.processNotification(scrollInfo, _firstScroller);
  }
),

显然,要针对每个可滚动的小部件实施上述示例,并相应地编辑 SyncController (参数 controller:)和 processNotification scrollController参数(在_firstScroller 之上)。您可能还要实现一些更多的故障保护措施,例如检查 _syncScroller!= null 等等。以上PSA适用:)


1
继续沿着这条路走,我明白为什么这不是最优的选择,如果事情开始增长,性能显然会受到影响。 - Kimmax
1
你的解决方案听起来太棒了。我喜欢你摆脱递归滚动位置变化并将其作为单独的Sync类处理的方式。 - Олександр Бабіч
我需要为滚动控制器设置初始偏移量,但是linked_scroll_controller现在似乎不支持这一点。在我的情况下,这种方法很有效,因为我可以为每个ScrollController设置偏移量。谢谢! - Cleber Alcântara

4

如果您的用户界面很简单,只需将两个滚动小部件(例如两个Column)与相反的小部件(如Row)一起包装,然后使用SingleChildScrollView进行滚动,代码如下:

SingleChildScrollView(
  scrollDirection: Axis.vertical,
  child: Row(
    children: [
      //first column
      Column(),
      //second column
      Column(),
    ],
  ),
)

如果很复杂,你可以使用类似这样的滚动控制器

  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();
  }

然后将每个滚动控制器添加到您拥有的每个滚动小部件中,如下所示。
SingleChildScrollView(
  scrollDirection: Axis.vertical,
  controller: scrollController1,
  child: Column(), // column 1
)

SingleChildScrollView(
  scrollDirection: Axis.vertical,
  controller: scrollController2,
  child: Column(), // column 2
)

0
我知道这是一个古老的问题,但对某些人可能会有用.. linked_scroll_controller 这个包正是你所需要的,它的性能比 NotificationListener 或者给你的 scrollController 添加监听器并使用 .jump 要好得多..
  late ScrollController controller1;
  late ScrollController controller2;
  late LinkedScrollControllerGroup parentController;
  
  
  @override
  void initState() {
    super.initState();
    parentController = LinkedScrollControllerGroup();
    controller1 = parentController.addAndGet();
    controller2 = parentController.addAndGet();
  }

就这样,将controller1连接到第一个可滚动的小部件,将controller2连接到另一个小部件,它们就会同步起来。

-1

我找到了这个,对我有用。

static final tracking = TrackingScrollController();

// implementation
child: ListView(
  controller: tracking,

并且可以在多个页面视图上工作。


1
嗨@马利克库洛萨基,你能扩展一下你的答案吗?它不起作用。为什么你把它设为静态的呢? - Atamyrat Babayev
1
在使用PageView并且需要同步每个页面上的滚动视图时,这实际上是最好的答案。 - flocbit
这有意义吗?一个ScrollController实例不能用于多个滚动。 - Usama Karim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接