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

Flutter中如何让showTimePicker支持超过24小时的时长选择?

实现思路与方案

因为原生showTimePicker的TimeOfDay仅支持0-23小时,要改造成支持0-99小时的时长选择器,有两种核心思路,兼顾你对原生UI的偏好:


思路1:替换原生小时选择组件(保留原生UI)

通过遍历showTimePicker的内部Widget树,替换原生的小时滚轮为支持0-99的自定义滚轮,同时维护独立的时长状态,最终返回Duration而非TimeOfDay。

关键步骤:

  1. 维护自定义时长状态:在调用showTimePicker前,初始化小时(0-99)和分钟(0-59)的状态变量。
  2. 遍历替换Widget:写一个Widget树遍历工具,找到原生的HourPicker组件,替换为自定义的小时选择滚轮(0-99选项)。
  3. 处理返回逻辑:忽略原生TimeOfDay的小时值,用自己维护的小时状态和原生返回的分钟值组合成最终时长。

示例代码片段:

// 自定义小时选择器替换工具
class HourPickerReplacer extends StatelessWidget {
  final int initialHour;
  final ValueChanged<int> onHourChanged;
  final Widget child;

  const HourPickerReplacer({
    super.key,
    required this.initialHour,
    required this.onHourChanged,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    // 遍历替换原生HourPicker(需根据Flutter版本调整布局匹配逻辑)
    if (child.runtimeType.toString().contains('HourPicker')) {
      return ListWheelScrollView.useDelegate(
        itemExtent: 40,
        controller: FixedExtentScrollController(initialItem: initialHour),
        onSelectedItemChanged: onHourChanged,
        childDelegate: ListWheelChildBuilderDelegate(
          builder: (_, index) => Center(
            child: Text(
              index.toString().padLeft(2, '0'),
              style: Theme.of(context).textTheme.titleLarge,
            ),
          ),
          childCount: 100,
        ),
      );
    }
    // 递归处理子Widget
    if (child is Column) {
      return Column(
        children: child.children.map((e) => HourPickerReplacer(
          initialHour: initialHour,
          onHourChanged: onHourChanged,
          child: e,
        )).toList(),
      );
    }
    if (child is Container) {
      return Container(child: HourPickerReplacer(
        initialHour: initialHour,
        onHourChanged: onHourChanged,
        child: child.child!,
      ));
    }
    return child;
  }
}

// 调用示例
Future<Duration?> showDurationPicker(BuildContext context) async {
  int selectedHours = 0;
  int selectedMinutes = 0;

  final timeResult = await showTimePicker(
    context: context,
    initialTime: TimeOfDay.now(),
    builder: (_, child) => MediaQuery(
      data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: true),
      child: HourPickerReplacer(
        initialHour: selectedHours,
        onHourChanged: (hour) => selectedHours = hour,
        child: child!,
      ),
    ),
  );

  if (timeResult != null) {
    return Duration(hours: selectedHours, minutes: timeResult.minute);
  }
  return null;
}

思路2:自定义Picker(完全匹配原生UI)

模仿原生showTimePicker的UI样式,从零实现一个支持0-99小时的时长选择器,完全可控且无版本兼容性问题。

关键步骤:

  1. 复刻原生UI:用AlertDialog包裹,内部用ListWheelScrollView实现小时(0-99)和分钟(0-59)的滚轮选择,样式对齐原生组件。
  2. 状态管理:用StatefulBuilder维护选择的小时和分钟状态。
  3. 返回时长:点击确认按钮时返回组合后的Duration对象。

示例代码片段:

Future<Duration?> showCustomDurationPicker(BuildContext context, Duration initialDuration) async {
  int selectedHours = initialDuration.inHours.clamp(0, 99);
  int selectedMinutes = initialDuration.inMinutes.remainder(60);

  return showDialog<Duration>(
    context: context,
    builder: (_) => AlertDialog(
      title: const Text('选择时长'),
      content: StatefulBuilder(
        builder: (context, setState) => Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            // 小时选择滚轮
            SizedBox(
              height: 150,
              child: ListWheelScrollView.useDelegate(
                itemExtent: 40,
                controller: FixedExtentScrollController(initialItem: selectedHours),
                onSelectedItemChanged: (index) => setState(() => selectedHours = index),
                childDelegate: ListWheelChildBuilderDelegate(
                  builder: (_, index) => Center(
                    child: Text(
                      '${index.toString().padLeft(2, '0')} 小时',
                      style: Theme.of(context).textTheme.titleLarge,
                    ),
                  ),
                  childCount: 100,
                ),
              ),
            ),
            const SizedBox(height: 16),
            // 分钟选择滚轮
            SizedBox(
              height: 150,
              child: ListWheelScrollView.useDelegate(
                itemExtent: 40,
                controller: FixedExtentScrollController(initialItem: selectedMinutes),
                onSelectedItemChanged: (index) => setState(() => selectedMinutes = index),
                childDelegate: ListWheelChildBuilderDelegate(
                  builder: (_, index) => Center(
                    child: Text(
                      '${index.toString().padLeft(2, '0')} 分钟',
                      style: Theme.of(context).textTheme.titleLarge,
                    ),
                  ),
                  childCount: 60,
                ),
              ),
            ),
          ],
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () => Navigator.pop(context, Duration(hours: selectedHours, minutes: selectedMinutes)),
          child: const Text('确认'),
        ),
      ],
    ),
  );
}

方案对比

  • 思路1:完全保留原生showTimePicker的交互和样式,但依赖Flutter版本的内部组件结构,可能在版本更新后失效。
  • 思路2:UI可控性更高,无版本兼容问题,仅需对齐原生样式即可实现一致体验。

推荐优先考虑思路2,稳定性和扩展性更好;如果必须完全复用原生组件,则选择思路1并针对目标Flutter版本调整Widget遍历逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 14:40:57