You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中禁止ListWheelScrollView首个项滚动的bug修复问题

ListWheelScrollView 禁用项滚动拦截修复

需求描述

实现ListWheelScrollView在遇到首个禁用项时停止滚动,不允许滚动到该禁用项,滚动索引不应包含首个禁用项。预期效果参考:
预期滚动效果

初始代码

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> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text("List"),
          backgroundColor: Colors.green,
        ),
        body: Center(
          child: Stack(
            alignment: AlignmentDirectional.center,
            children: [
              RotatedBox(
                quarterTurns: 1,
                child: SizedBox(
                  height: 600,
                  width: 800,
                  child: ListWheelScrollView(
                      itemExtent: 100,
                      physics: const FixedExtentScrollPhysics(),
                      onSelectedItemChanged: (value) {},
                      children: [
                        for (int i = 0; i < 4; i++) ...[
                          Container(
                            color: i != 3 ? Colors.green[200] : Colors.red[200],
                            height: 70,
                            width: 70,
                            alignment: Alignment.center,
                            child: RotatedBox(
                                quarterTurns: -1,
                                child: Text(i != 3 ? "Enabled" : "Disabled")),
                          )
                        ]
                      ]),
                ),
              ),
              const Positioned(top: 440, child: Icon(Icons.arrow_circle_up))
            ],
          ),
        ));
  }
}

存在Bug的解决方案

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

class ScrollingList extends StatefulWidget {
  const ScrollingList({Key? key}) : super(key: key);

  @override
  _ScrollingListState createState() => _ScrollingListState();
}

class _ScrollingListState extends State<ScrollingList> {
  final controller = FixedExtentScrollController();

  final children = [
    for (int i = 0; i < 6; i++) ScrollingListItem(enabled: i != 0)
  ];

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text("List"),
          backgroundColor: Colors.green,
        ),
        body: Center(
          child: Stack(
            alignment: AlignmentDirectional.center,
            children: [
              RotatedBox(
                quarterTurns: 1,
                child: SizedBox(
                  height: 600,
                  width: 800,
                  child: ListWheelScrollView(
                    controller: controller,
                    itemExtent: 100,
                    physics: const FixedExtentScrollPhysics(),
                    onSelectedItemChanged: (idx) {
                      if (!children[idx].enabled) {
                        controller.animateToItem(
                          idx + 1,
                          duration: const Duration(milliseconds: 500),
                          curve: Curves.linear,
                        );
                      }
                    },
                    children: children,
                  ),
                ),
              ),
              const Positioned(top: 440, child: Icon(Icons.arrow_circle_up))
            ],
          ),
        ));
  }
}

class ScrollingListItem extends StatelessWidget {
  final bool enabled;

  const ScrollingListItem({super.key, required this.enabled});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: enabled ? Colors.green[200] : Colors.red[200],
      height: 70,
      width: 70,
      alignment: Alignment.center,
      child: RotatedBox(
        quarterTurns: -1,
        child: Text(
          enabled ? "Enabled" : "Disabled",
        ),
      ),
    );
  }
}

Bug描述

该解决方案存在问题:当滚动到禁用项时,最左侧的最后一个元素会出现闪烁现象,效果参考:
闪烁Bug效果

问题原因分析

问题出在onSelectedItemChanged回调中触发animateToItem的逻辑:当选中禁用项时才触发滚动回退,此时禁用项已经被选中并显示在选中位置,然后才执行动画滚动到下一个有效项,这就导致了禁用项短暂显示,进而引发视觉闪烁。此外,原方案没有限制滚动的边界,允许滚动到禁用项的位置后再修正,这是触发闪烁的核心原因。

修复方案

我们需要自定义滚动物理类(FixedExtentScrollPhysics的子类),在滚动过程中直接拦截到禁用项的滚动动作,提前阻止滚动到禁用项位置,而不是在选中后再修正。具体步骤:

  • 自定义DisabledItemScrollPhysics,重写相关方法限制滚动范围,禁止滚动到禁用项的索引位置。
  • 调整控制器的初始位置,确保默认选中有效项。
  • 移除onSelectedItemChanged中的动画修正逻辑,改为通过物理规则提前拦截。

修复后的完整代码

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

class ScrollingList extends StatefulWidget {
  const ScrollingList({Key? key}) : super(key: key);

  @override
  _ScrollingListState createState() => _ScrollingListState();
}

class _ScrollingListState extends State<ScrollingList> {
  final controller = FixedExtentScrollController(initialItem: 1); // 默认选中第一个有效项

  final children = [
    for (int i = 0; i < 6; i++) ScrollingListItem(enabled: i != 0)
  ];

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text("List"),
          backgroundColor: Colors.green,
        ),
        body: Center(
          child: Stack(
            alignment: AlignmentDirectional.center,
            children: [
              RotatedBox(
                quarterTurns: 1,
                child: SizedBox(
                  height: 600,
                  width: 800,
                  child: ListWheelScrollView(
                    controller: controller,
                    itemExtent: 100,
                    // 使用自定义物理类拦截禁用项滚动
                    physics: DisabledItemScrollPhysics(
                      disabledIndices: const {0},
                      parent: const FixedExtentScrollPhysics(),
                    ),
                    onSelectedItemChanged: (idx) {},
                    children: children,
                  ),
                ),
              ),
              const Positioned(top: 440, child: Icon(Icons.arrow_circle_up))
            ],
          ),
        ));
  }
}

class ScrollingListItem extends StatelessWidget {
  final bool enabled;

  const ScrollingListItem({super.key, required this.enabled});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: enabled ? Colors.green[200] : Colors.red[200],
      height: 70,
      width: 70,
      alignment: Alignment.center,
      child: RotatedBox(
        quarterTurns: -1,
        child: Text(
          enabled ? "Enabled" : "Disabled",
        ),
      ),
    );
  }
}

// 自定义滚动物理类,拦截禁用项的滚动
class DisabledItemScrollPhysics extends FixedExtentScrollPhysics {
  final Set<int> disabledIndices;

  const DisabledItemScrollPhysics({
    required this.disabledIndices,
    super.parent,
  });

  @override
  DisabledItemScrollPhysics applyTo(ScrollPhysics? ancestor) {
    return DisabledItemScrollPhysics(
      disabledIndices: disabledIndices,
      parent: buildParent(ancestor),
    );
  }

  @override
  double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
    final itemExtent = (position as FixedExtentMetrics).itemExtent;
    final currentIndex = position.itemIndex;
    final targetIndex = (position.pixels + offset) ~/ itemExtent;

    // 如果目标索引是禁用项,拦截滚动
    if (disabledIndices.contains(targetIndex)) {
      // 计算偏移量,确保停留在最近的有效索引位置
      final closestValidIndex = disabledIndices.contains(currentIndex)
          ? currentIndex + 1
          : currentIndex;
      final targetPixels = closestValidIndex * itemExtent;
      return targetPixels - position.pixels;
    }

    return super.applyPhysicsToUserOffset(position, offset);
  }

  @override
  ScrollPosition createScrollPosition(
    ScrollPhysics physics,
    ScrollContext context,
    ScrollPosition? oldPosition,
  ) {
    return _DisabledItemScrollPosition(
      physics: physics,
      context: context,
      oldPosition: oldPosition,
      disabledIndices: disabledIndices,
    );
  }
}

class _DisabledItemScrollPosition extends FixedExtentScrollPosition {
  final Set<int> disabledIndices;

  _DisabledItemScrollPosition({
    required super.physics,
    required super.context,
    super.oldPosition,
    required this.disabledIndices,
  });

  @override
  double setPixels(double newPixels) {
    final itemExtent = this.itemExtent;
    final targetIndex = newPixels ~/ itemExtent;

    // 如果目标索引是禁用项,修正到最近的有效索引
    if (disabledIndices.contains(targetIndex)) {
      final currentIndex = this.itemIndex;
      final validIndex = disabledIndices.contains(currentIndex)
          ? currentIndex + 1
          : currentIndex;
      return super.setPixels(validIndex * itemExtent);
    }

    return super.setPixels(newPixels);
  }
}

修复说明

  1. 自定义滚动物理类:通过DisabledItemScrollPhysics和对应的ScrollPosition,在滚动过程中直接判断目标索引是否为禁用项,提前修正滚动位置,避免禁用项被选中显示。
  2. 初始位置设置:将控制器初始项设为1(第一个有效项),确保默认状态不会显示禁用项。
  3. 移除事后修正逻辑:删除了onSelectedItemChanged中的动画回退代码,完全通过物理规则拦截滚动,从根源消除闪烁现象。

内容的提问来源于stack exchange,提问作者Ramji

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 16:42:01