Синхронизация ListWheelScrollView во Flutter

Когда я прокручиваю один вид прокрутки колеса списка, другой список либо отстает, либо прокручивается не плавно.
https://pub.dev/packages/linked_scroll_controller позволяет синхронизировать списки, но не поддерживает FixedExtendScrollPhysics.

Выход : -

https://pub.dev/packages/linked_scroll_controller отлично работает, если мы используем ScrollPhysics, но выдает ошибку при использовании с виджетом, который использует FixedExtendScrollPhysics. Я хочу, чтобы оба списка перемещались Синхронизация, то есть, если я перемещаю зеленый список, я хочу, чтобы красный список перемещался одновременно, и наоборот

Код :

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

Основная причина, по которой ваша прокрутка не является гладкой, заключается в том, что вы используете setState(); с помощью setState, чтобы отображать все виджеты страниц, которые находятся внутри связанного класса, если страница содержит анимацию, анимация будет отставать. Попробуйте удалить setState() или отделить ListWheelScrollView() в другой класс, или вы можете попробовать использовать управление состоянием, например Provider, GetX или BLoC.

Septian Dika 28.11.2022 04:18

@SeptianDika Я использовал провайдера, но происходит то же самое, и я не думаю, что всего два виджета с использованием setState — плохая идея.

Ramji 28.11.2022 16:59
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
400
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

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

Этот код не работает с FixedExtendScrollPhysics

Ramji 30.12.2022 12:51

Нет, работает нормально. Я добавил реализацию ListWheelScrollView, удачи~~~

bakboem 31.12.2022 00:52

Это работает, но если я постепенно прокручиваю список, он отстает

Ramji 31.12.2022 04:43

Попробуйте разделить два controller и добавить слушателя следующим образом:

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

Список все еще отстает

Ramji 30.12.2022 12:57
Ответ принят как подходящий

Действительно интересный вопрос. Проблема заключалась в синхронизации обоих прокруток. Я внес несколько изменений в ваш код для достижения желаемого результата.

Основная идея состоит в том, чтобы удалить слушателя с другой прокрутки, прежде чем форсировать пиксели. После прокрутки добавьте того же слушателя. Но поскольку это происходит мгновенно, а фактическая прокрутка иногда происходит в будущем, они не полностью перекрываются.

Поэтому мне пришлось ввести CancelableCompleter из асинхронной библиотеки, чтобы убедиться, что операция добавления не произойдет, если произойдет другое событие прокрутки.

С forcePixels прокрутка к другому колесу не откладывается, поэтому CancelableCompleter не требуется.

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

Я использую защитную функцию-член forcePixels, поскольку Flutter не предоставляет никакого способа установить пиксели без анимации без создания подкласса ScrollPosition. Если у вас все в порядке с этим предупреждением линтера, все в порядке. Если нет, нам придется расширить ListWheelScrollView, чтобы использовать ScrollPosition, где мы могли бы вносить изменения по мере необходимости.

@Ramji сделал несколько изменений.

Rahul 30.12.2022 20:19

Интересный факт: вы используете BlueStacks для разработки, и я там работаю.

Rahul 30.12.2022 20:26

использование forcePixels влияет на производительность? Не могли бы вы добавить, как работает код? Мне нравится Bluestack. Я использую Bluestack с детства, потому что не могу позволить себе телефон.

Ramji 31.12.2022 04:41

Удален CancelableCompleter, так как он не требуется. Код намного проще. Я также отредактировал объяснение.

Rahul 31.12.2022 07:50

Есть ли способ включать и выключать параллелизм списка, то есть он может прокручиваться индивидуально и синхронно в соответствии с некоторыми условиями.

Ramji 16.02.2023 09:01

Вы можете удалить слушателей или перейти к другим слушателям и вернуть их обратно к предоставленным слушателям в зависимости от ваших условий.

Rahul 16.02.2023 20:08

Лучший подход — использовать linked_scroll_controller.

Виджеты прокрутки создадут контроллер прокрутки по умолчанию (класс ScrollController), если он не указан. Контроллер прокрутки создает ScrollPosition для управления состоянием отдельного виджета Scrollable.

Чтобы связать наши контроллеры прокрутки, мы будем использовать linked_scroll_controller, контроллер прокрутки, который позволяет синхронизировать два или более представления прокрутки.

late LinkedScrollControllerGroup _verticalControllersGroup;
late ScrollController _verticalController1;
late ScrollController _verticalController2;

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

Теперь используйте эти контроллеры в своих списках.

Ниже приведены два хороших примера, которые помогут вам использовать linked_scroll_controller в вашем случае.

Flutter: как создавать связанные виджеты прокрутки

Flutter: создание таблицы с прокруткой в ​​двух направлениях с фиксированным заголовком и столбцом

Создание прокрутки в двух направлениях с помощью linked_scroll_controller

linked_scroll_controller не поддерживает физику фиксированной расширенной прокрутки, и моему приложению требуется эта физика.

Ramji 23.02.2023 14:34

Другие вопросы по теме