Flutter如何限制FixedExtentScrollController滚动不超过指定索引
Flutter 双Cupertino时间选择器防倒序限制实现
核心思路
要实现结束时间不早于开始时间的限制,核心是统一时间度量做大小校验,动态监听滚动位置做越界纠正:
- 将小时、分钟统一换算为当日总分钟数做比较,避免时分分开判断的逻辑漏洞
- 监听所有选择器的滚动回调,一旦检测到结束时间小于开始时间,立刻通过滚动控制器纠正结束时间位置
- 对结束时间选择器做范围限制:当结束小时等于开始小时时,分钟选项最小只能选和开始分钟相等的档位;当结束小时大于开始小时时,分钟无额外限制;结束小时不能小于开始小时
- 修正原代码笔误:原分钟选择器调用
durationPicker时错误传了inHour=true,会导致分钟档位生成错误,需改为传false
完整实现代码
Widget durationPicker(bool inHour, FixedExtentScrollController controller, void Function(int) onSelected) { return CupertinoPicker( scrollController: controller, magnification: 1.1, children: List.generate( inHour ? 24 : 12, (index) => Container( alignment: Alignment.center, child: Text( inHour ? '$index' : '${index * 5}', ), ), ), itemExtent: 40, onSelectedItemChanged: onSelected, ); } class CupertinoDoubleTimePicker extends StatefulWidget { const CupertinoDoubleTimePicker({Key? key}) : super(key: key); @override State<CupertinoDoubleTimePicker> createState() => _CupertinoDoubleTimePickerState(); } class _CupertinoDoubleTimePickerState extends State<CupertinoDoubleTimePicker> { late final FixedExtentScrollController _hourScrollControllerStart; late final FixedExtentScrollController _minuteControllerStart; late final FixedExtentScrollController _hourScrollControllerEnd; late final FixedExtentScrollController _minuteControllerEnd; // 存储当前选中的时分索引 int _startHour = 0; int _startMinuteIdx = 0; int _endHour = 0; int _endMinuteIdx = 0; @override void initState() { super.initState(); _hourScrollControllerStart = FixedExtentScrollController(); _minuteControllerStart = FixedExtentScrollController(); _hourScrollControllerEnd = FixedExtentScrollController(); _minuteControllerEnd = FixedExtentScrollController(); } @override void dispose() { _hourScrollControllerStart.dispose(); _minuteControllerStart.dispose(); _hourScrollControllerEnd.dispose(); _minuteControllerEnd.dispose(); super.dispose(); } // 校验时间合法性,不合法时自动纠正结束时间 void _validateTimeRange() { final startTotal = _startHour * 60 + _startMinuteIdx * 5; final endTotal = _endHour * 60 + _endMinuteIdx * 5; if (endTotal < startTotal) { // 结束时间早于开始时间,自动对齐到开始时间 _endHour = _startHour; _endMinuteIdx = _startMinuteIdx; WidgetsBinding.instance.addPostFrameCallback((_) { _hourScrollControllerEnd.jumpToItem(_endHour); _minuteControllerEnd.jumpToItem(_endMinuteIdx); }); } } @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ // 开始时间选择器 SizedBox( width: MediaQuery.of(context).size.width * 0.3, height: MediaQuery.of(context).size.height * 0.2, child: Row( children: [ Expanded( child: durationPicker(true, _hourScrollControllerStart, (value) { _startHour = value; _validateTimeRange(); }), ), const Text(':'), Expanded( // 原代码此处inHour传了true,已修正为false child: durationPicker(false, _minuteControllerStart, (value) { _startMinuteIdx = value; _validateTimeRange(); }), ), ], ), ), // 结束时间选择器 SizedBox( width: MediaQuery.of(context).size.width * 0.3, height: MediaQuery.of(context).size.height * 0.2, child: Row( children: [ Expanded( child: durationPicker(true, _hourScrollControllerEnd, (value) { _endHour = value; // 如果结束小时等于开始小时,分钟索引不能小于开始分钟索引 if (_endHour == _startHour && _endMinuteIdx < _startMinuteIdx) { _endMinuteIdx = _startMinuteIdx; WidgetsBinding.instance.addPostFrameCallback((_) { _minuteControllerEnd.jumpToItem(_endMinuteIdx); }); } // 如果结束小时小于开始小时,直接对齐到开始小时 if (_endHour < _startHour) { _endHour = _startHour; WidgetsBinding.instance.addPostFrameCallback((_) { _hourScrollControllerEnd.jumpToItem(_endHour); }); } _validateTimeRange(); }), ), const Text(':'), Expanded( // 原代码此处inHour传了true,已修正为false child: durationPicker(false, _minuteControllerEnd, (value) { // 小时相等时,分钟不能小于开始分钟 if (_endHour == _startHour && value < _startMinuteIdx) { _endMinuteIdx = _startMinuteIdx; WidgetsBinding.instance.addPostFrameCallback((_) { _minuteControllerEnd.jumpToItem(_endMinuteIdx); }); return; } _endMinuteIdx = value; _validateTimeRange(); }), ), ], ), ), ], ); } }
实现效果参考

可选优化点
- 如果需要滚动过程中就无法越过限制位置,而不是滚动结束后再纠正,可以给控制器添加
addListener监听滚动偏移,实时计算当前位置是否越界,一旦越界立刻跳回最大允许位置 - 可以给不可选的时间档位置灰提示,只需要在生成
durationPicker的children时判断当前索引是否小于最小允许值,将对应Text的颜色改为灰色即可
内容的提问来源于stack exchange,提问作者Jacoo
相关产品推荐
相关产品推荐

