skip to Main Content

When I scroll through one List Wheel Scroll View, the other list either lags or does not scroll smoothly.
https://pub.dev/packages/linked_scroll_controller allows to sync lists but does not support FixedExtendScrollPhysics.

Output : –

https://media.giphy.com/media/tpUIY3KgavxZoOVmvP/giphy.gif

https://pub.dev/packages/linked_scroll_controller works perfectly if we are using ScrollPhysics but throws an error when used with a widget that uses FixedExtendScrollPhysics. I want both the list to move Synchronizing that is if I move green list I want red list to move simultaneously and vice versa

Code :

import 'dart:math';
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'List',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false,
      home: const List(),
    );
  }
}

class List extends StatefulWidget {
  const List({Key? key}) : super(key: key);
  @override
  _ListState createState() => _ListState();
}

class _ListState extends State<List> {
  final scrollController = FixedExtentScrollController();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text("List"),
          backgroundColor: Colors.green,
        ),
        body: Row(
          children: [
            SizedBox(
              height: 600,
              width: 300,
              child: ListWheelScrollView(
                  itemExtent: 100,
                  physics: const FixedExtentScrollPhysics(),
                  onSelectedItemChanged: (value) {
                    setState(() {
                      scrollController.animateToItem(value,
                          duration: const Duration(milliseconds: 200),
                          curve: Curves.easeInOut);
                    });
                  },
                  children: [
                    for (int i = 0; i < 5; i++) ...[
                      Container(
                        color: Colors.green,
                        height: 50,
                        width: 50,
                      )
                    ]
                  ]),
            ),
            SizedBox(
              height: 600,
              width: 300,
              child: ListWheelScrollView(
                  controller: scrollController,
                  physics: const FixedExtentScrollPhysics(),
                  itemExtent: 100,
                  children: [
                    for (int i = 0; i < 5; i++) ...[
                      Container(
                        color: Colors.red,
                        height: 50,
                        width: 50,
                      )
                    ]
                  ]),
            )
          ],
        ));
  }
}

4

Answers


  1. import 'package:flutter/material.dart';
    class MyWidget extends StatefulWidget {
      MyWidget({Key? key}) : super(key: key);
    
      @override
      State<MyWidget> createState() => _MyWidgetState();
    }
    
    class _MyWidgetState extends State<MyWidget> {
      late ScrollController _controllerA;
      late ScrollController _controllerB;
      ///   FixedExtentScrollController
      /// late FixedExtentScrollController _controllerA;
      /// late FixedExtentScrollController _controllerB;
      @override
      void initState() {
        super.initState();
        _controllerA = ScrollController();
        _controllerB = ScrollController();
        /// _controllerA = FixedExtentScrollController();
        /// _controllerB = FixedExtentScrollController();
        _controllerA.addListener(() {
          if (_controllerA.position.hasPixels) {
            _controllerB.jumpTo(_controllerA.offset);
          }
        });
        /// if you neet bind _controllerB to _controllerA
    
        /// _controllerB.addListener(() {
        ///  if (_controllerB.position.hasPixels) {
        ///    _controllerA.jumpTo(_controllerB.offset);
        ///  }
        ///});
      }
    
      @override
      void dispose() {
        _controllerA.dispose();
        _controllerB.dispose();
        super.dispose();
      }
    
      Widget _listView(Color color, ScrollController controller) {
        var width = MediaQuery.of(context).size.width / 2;
        return Container(
          width: width,
          child: ListView.builder(
              controller: controller,
              shrinkWrap: true,
              itemCount: 100,
              itemExtent: 50,
              itemBuilder: (context, index) {
                return Container(
                  decoration: BoxDecoration(
                      color: color, border: Border.all(color: Colors.white)),
                );
              }),
        );
      }
    
      /// ListWheelScrollView 
      /// Widget _listView(Color color, FixedExtentScrollController controller) {
      ///   var width = MediaQuery.of(context).size.width / 2;
      ///   return Container(
      ///     width: width,
      ///     child: ListWheelScrollView(
      ///         physics: FixedExtentScrollPhysics(),
      ///         controller: controller,
      ///         itemExtent: 50,
      ///         children: [
      ///           ...List.generate(
      ///               100,
      ///               (index) => Container(
      ///                     decoration: BoxDecoration(
      ///                         color: color,
      ///                         border: Border.all(color: Colors.white)),
      ///                   ))
      ///         ]),
      ///   );
      /// }
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
              body: Row(
            children: [
              _listView(Colors.red, _controllerA),
              _listView(Colors.yellow, _controllerB),
            ],
          )),
        );
      }
    }
    
    Login or Signup to reply.
  2. Try separate two controller and add listener like this:

    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'List',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          debugShowCheckedModeBanner: false,
          home: const List(),
        );
      }
    }
    
    class List extends StatefulWidget {
      const List({Key? key}) : super(key: key);
    
      @override
      _ListState createState() => _ListState();
    }
    
    class _ListState extends State<List> {
      final scrollController1 = FixedExtentScrollController();
      final scrollController2 = FixedExtentScrollController();
    
      @override
      void initState() {
        super.initState();
        scrollController1.addListener(() {
          if (scrollController1.position.hasPixels) {
            scrollController2.animateTo(
              scrollController1.offset,
              duration: const Duration(milliseconds: 10), //adjust delay you need
              curve: Curves.easeInOut,
            );
          }
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
              title: const Text("List"),
              backgroundColor: Colors.green,
            ),
            body: Row(
              children: [
                SizedBox(
                  height: 600,
                  width: 150,
                  child: ListWheelScrollView(
                      itemExtent: 100,
                      controller: scrollController1,
                      physics: const FixedExtentScrollPhysics(),
                      children: [
                        for (int i = 0; i < 5; i++) ...[
                          Container(
                            color: Colors.green,
                            height: 50,
                            width: 50,
                          )
                        ]
                      ]),
                ),
                SizedBox(
                  height: 600,
                  width: 150,
                  child: ListWheelScrollView(
                      controller: scrollController2,
                      physics: const FixedExtentScrollPhysics(),
                      itemExtent: 100,
                      children: [
                        for (int i = 0; i < 5; i++) ...[
                          Container(
                            color: Colors.red,
                            height: 50,
                            width: 50,
                          )
                        ]
                      ]),
                )
              ],
            ));
      }
    }
    
    Login or Signup to reply.
  3. Really interesting question. The problem was syncing both the scrollviews. I made few changes to your code to achieve the desired result.

    The basic idea is to remove listener to the other scroll before forcing pixels. After the scroll, add the same listener. But because it happens instantaneously and actual scroll happens sometimes in future, they don’t overlap perfectly.

    So I had to introduce CancelableCompleter from the async library to make sure add operation does not happen if another scroll event had happened.

    With forcePixels, the scrolling to other wheel is not deferred hence CancelableCompleter is not required.

    // ignore_for_file: invalid_use_of_protected_member
    
    import 'package:flutter/material.dart';
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'List',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          debugShowCheckedModeBanner: false,
          home: const List(),
        );
      }
    }
    
    class List extends StatefulWidget {
      const List({Key? key}) : super(key: key);
      @override
      _ListState createState() => _ListState();
    }
    
    class _ListState extends State<List> {
      final _firstScrollController = FixedExtentScrollController();
      final _secondScrollController = FixedExtentScrollController();
    
      @override
      void initState() {
        super.initState();
        _firstScrollController.addListener(_firstScrollListener);
        _secondScrollController.addListener(_secondScrollListener);
      }
    
      @override
      void dispose() {
        _firstScrollController
          ..removeListener(_firstScrollListener)
          ..dispose();
        _secondScrollController
          ..removeListener(_secondScrollListener)
          ..dispose();
        super.dispose();
      }
    
      void _firstScrollListener() {
        _secondScrollController.removeListener(_secondScrollListener);
        _secondScrollController.position.forcePixels(_firstScrollController.offset);
        _secondScrollController.addListener(_secondScrollListener);
      }
    
      void _secondScrollListener() {
        _firstScrollController.removeListener(_firstScrollListener);
        _firstScrollController.position.forcePixels(_secondScrollController.offset);
        _firstScrollController.addListener(_firstScrollListener);
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
              title: const Text("List"),
              backgroundColor: Colors.green,
            ),
            body: Row(
              children: [
                SizedBox(
                  height: 600,
                  width: 300,
                  child: ListWheelScrollView(
                      itemExtent: 100,
                      controller: _firstScrollController,
                      physics: const FixedExtentScrollPhysics(),
                      onSelectedItemChanged: (value) {
                        print('first wheel : item selected: $value');
                      },
                      children: [
                        for (int i = 0; i < 25; i++) ...[
                          Container(
                            color: Colors.green,
                            height: 50,
                            width: 50,
                          )
                        ]
                      ]),
                ),
                SizedBox(
                  height: 600,
                  width: 300,
                  child: ListWheelScrollView(
                      controller: _secondScrollController,
                      physics: const FixedExtentScrollPhysics(),
                      itemExtent: 100,
                      onSelectedItemChanged: (value) {
                        print('second wheel : item selected: $value');
                      },
                      children: [
                        for (int i = 0; i < 25; i++) ...[
                          Container(
                            color: Colors.red,
                            height: 50,
                            width: 50,
                          )
                        ]
                      ]),
                )
              ],
            ));
      }
    }
    
    

    I am using protective member function forcePixels as Flutter has not provided any way to set pixels without animation without creating subclass of ScrollPosition. If you are fine with this linter warning, it is all good. If not, we will have to extend ListWheelScrollView to use ScrollPosition where we could make changes as per need.

    Login or Signup to reply.
  4. Better Approach is to use linked_scroll_controller.

    Scrolling widgets will create a default scroll controller (ScrollController class) if none is provided. A scroll controller creates a ScrollPosition to manage the state specific to an individual Scrollable widget.

    To link our scroll controllers we’ll use linked_scroll_controller, a scroll controller that allows two or more scroll views to be in sync.

    late LinkedScrollControllerGroup _verticalControllersGroup;
    late ScrollController _verticalController1;
    late ScrollController _verticalController2;
    
    @override
    void initState() {
      super.initState();
      _verticalControllersGroup = LinkedScrollControllerGroup();
      _verticalController1 = _verticalControllersGroup.addAndGet();
      _verticalController2 = _verticalControllersGroup.addAndGet();
    }
    

    Now use these controllers on your listviews.

    below are two nice examples which will help you to use linked_scroll_controller in your case.

    Flutter: How to create linked scroll widgets

    Flutter: Creating a two-direction scrolling table with a fixed head and column

    Creating a two-direction scrolling with linked_scroll_controller

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search