基础入门 Flutter for OpenHarmony:TimePicker 时间选择器详解

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
🎯 欢迎来到 Flutter for OpenHarmony 社区!本文将深入讲解 Flutter 中 TimePicker 时间选择器的使用方法,带你从基础到精通,掌握时间选择、时间范围限制、自定义样式等常见交互模式。


一、TimePicker 组件概述

在移动应用开发中,时间选择是一种常见的交互需求。用户需要选择闹钟时间、预约时间、日程安排等,而良好的时间选择体验可以大大提升应用的易用性。Flutter 提供了 TimePicker 组件,专门用于实现时间选择功能,支持 Material Design 设计规范。

📋 TimePicker 组件特点

特点 说明
Material Design 遵循 Material Design 设计规范
两种模式 支持时钟模式和输入模式
24小时制/12小时制 灵活的时间格式支持
时间范围限制 支持设置可选时间范围
自定义样式 支持自定义颜色、形状等样式
响应式布局 适配不同屏幕尺寸

TimePicker 与其他时间选择方案对比

方案 优点 缺点
showTimePicker 开箱即用、Material 风格 样式定制有限
CupertinoTimerPicker iOS 风格 仅 iOS 风格
自定义时间选择器 完全自定义 开发成本高

💡 使用场景:TimePicker 适合需要用户选择时间的场景,如设置闹钟、预约时间、日程安排、倒计时设置等。


二、TimePicker 基础用法

TimePicker 通常通过 showTimePicker 函数来显示,这是一个异步函数,返回用户选择的时间。让我们从最基础的用法开始学习。

2.1 最简单的 TimePicker

最基础的 TimePicker 使用方式如下:

dart 复制代码
ElevatedButton(
  onPressed: () async {
    final TimeOfDay? time = await showTimePicker(
      context: context,
      initialTime: TimeOfDay.now(),
    );
    if (time != null) {
      print('选择的时间: ${time.hour}:${time.minute}');
    }
  },
  child: const Text('选择时间'),
)

代码解析:

  • showTimePicker:显示时间选择器的函数
  • context:构建上下文,必需参数
  • initialTime:初始显示的时间,类型为 TimeOfDay
  • 返回值:用户选择的时间,如果取消则返回 null

2.2 TimeOfDay 时间类

TimeOfDay 是 Flutter 中表示时间的类:

dart 复制代码
final now = TimeOfDay.now();
final specific = TimeOfDay(hour: 14, minute: 30);

print('小时: ${now.hour}');
print('分钟: ${now.minute}');
print('格式化: ${now.format(context)}');

TimeOfDay 常用属性和方法:

属性/方法 说明
hour 小时(0-23)
minute 分钟(0-59)
format() 根据本地设置格式化时间
now() 获取当前时间
replacing() 创建新的 TimeOfDay 实例

2.3 完整基础示例

下面是一个完整的可运行示例,展示了 TimePicker 的基础用法:

dart 复制代码
class TimePickerBasicExample extends StatefulWidget {
  const TimePickerBasicExample({super.key});

  @override
  State<TimePickerBasicExample> createState() => _TimePickerBasicExampleState();
}

class _TimePickerBasicExampleState extends State<TimePickerBasicExample> {
  TimeOfDay? _selectedTime;

  Future<void> _selectTime() async {
    final TimeOfDay? time = await showTimePicker(
      context: context,
      initialTime: _selectedTime ?? TimeOfDay.now(),
    );
    if (time != null) {
      setState(() {
        _selectedTime = time;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('TimePicker 基础示例')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              _selectedTime != null
                  ? '选择的时间: ${_selectedTime!.format(context)}'
                  : '请选择时间',
              style: const TextStyle(fontSize: 24),
            ),
            const SizedBox(height: 24),
            ElevatedButton.icon(
              onPressed: _selectTime,
              icon: const Icon(Icons.access_time),
              label: const Text('选择时间'),
            ),
          ],
        ),
      ),
    );
  }
}

三、showTimePicker 核心参数详解

showTimePicker 函数提供了丰富的参数来控制时间选择器的行为和外观。

3.1 基础参数

参数 类型 说明
context BuildContext 构建上下文(必需)
initialTime TimeOfDay 初始时间(必需)
initialEntryMode TimePickerEntryMode 初始输入模式
cancelText String? 取消按钮文本
confirmText String? 确认按钮文本
helpText String? 帮助文本
errorInvalidText String? 无效时间错误文本

3.2 initialEntryMode 输入模式

TimePicker 支持两种输入模式:

dart 复制代码
showTimePicker(
  context: context,
  initialTime: TimeOfDay.now(),
  initialEntryMode: TimePickerEntryMode.dial,
);

输入模式说明:

模式 说明
TimePickerEntryMode.dial 时钟表盘模式(默认)
TimePickerEntryMode.input 文本输入模式
TimePickerEntryMode.dialOnly 仅时钟模式
TimePickerEntryMode.inputOnly 仅输入模式

3.3 时间范围限制

可以通过在选择后验证时间来限制可选时间范围:

dart 复制代码
Future<void> _selectTime() async {
  final TimeOfDay? time = await showTimePicker(
    context: context,
    initialTime: TimeOfDay.now(),
  );
  if (time != null) {
    final hour = time.hour;
    if (hour >= 8 && hour < 18) {
      setState(() {
        _selectedTime = time;
      });
    } else {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('请选择工作时间 8:00-18:00')),
      );
    }
  }
}

常用时间验证示例:

dart 复制代码
bool _isWorkHour(TimeOfDay time) {
  return time.hour >= 9 && time.hour < 18;
}

bool _isNotPastTime(TimeOfDay time) {
  final now = TimeOfDay.now();
  if (time.hour > now.hour) return true;
  if (time.hour == now.hour && time.minute >= now.minute) return true;
  return false;
}

bool _isEvenNumberMinute(TimeOfDay time) {
  return time.minute % 15 == 0;
}

3.4 自定义文本

可以自定义按钮和帮助文本:

dart 复制代码
showTimePicker(
  context: context,
  initialTime: TimeOfDay.now(),
  cancelText: '取消',
  confirmText: '确定',
  helpText: '选择预约时间',
  errorInvalidText: '请选择有效时间',
);

四、TimePicker 主题定制

可以通过 Theme 或 TimePickerTheme 来自定义 TimePicker 的外观。

4.1 使用 TimePickerTheme

dart 复制代码
Theme(
  data: Theme.of(context).copyWith(
    timePickerTheme: TimePickerTheme(
      backgroundColor: Colors.white,
      hourMinuteTextColor: Colors.blue,
      hourMinuteColor: Colors.blue.withOpacity(0.1),
      dialHandColor: Colors.blue,
      dialBackgroundColor: Colors.grey[200],
      entryModeIconColor: Colors.blue,
    ),
  ),
  child: Builder(
    builder: (context) => ElevatedButton(
      onPressed: () => showTimePicker(
        context: context,
        initialTime: TimeOfDay.now(),
      ),
      child: const Text('选择时间'),
    ),
  ),
)

4.2 TimePickerTheme 属性详解

属性 说明
backgroundColor 背景颜色
hourMinuteTextColor 时分文本颜色
hourMinuteColor 时分背景颜色
hourMinuteShape 时分形状
dialHandColor 表针颜色
dialHandColor 表盘背景颜色
dialTextColor 表盘数字颜色
entryModeIconColor 切换模式图标颜色
dayPeriodTextColor AM/PM 文本颜色
dayPeriodColor AM/PM 背景颜色
shape 整体形状
dayPeriodShape AM/PM 形状

4.3 深色主题适配

dart 复制代码
Theme(
  data: Theme.of(context).copyWith(
    timePickerTheme: TimePickerTheme(
      backgroundColor: Colors.grey[900],
      hourMinuteTextColor: Colors.white,
      hourMinuteColor: Colors.blue.withOpacity(0.2),
      dialHandColor: Colors.blue,
      dialBackgroundColor: Colors.grey[800],
      dialTextColor: Colors.white,
      entryModeIconColor: Colors.blue,
    ),
  ),
  child: Builder(
    builder: (context) => ElevatedButton(
      onPressed: () => showTimePicker(
        context: context,
        initialTime: TimeOfDay.now(),
      ),
      child: const Text('选择时间'),
    ),
  ),
)

五、TimePicker 实际应用场景

TimePicker 在实际开发中有着广泛的应用,让我们通过具体示例来学习。

5.1 闹钟设置

使用 TimePicker 实现闹钟设置功能:

dart 复制代码
class AlarmSettingPage extends StatefulWidget {
  const AlarmSettingPage({super.key});

  @override
  State<AlarmSettingPage> createState() => _AlarmSettingPageState();
}

class _AlarmSettingPageState extends State<AlarmSettingPage> {
  TimeOfDay _alarmTime = const TimeOfDay(hour: 7, minute: 0);
  bool _alarmEnabled = true;
  final List<String> _repeatDays = [];
  String _alarmLabel = '闹钟';

  Future<void> _selectAlarmTime() async {
    final TimeOfDay? time = await showTimePicker(
      context: context,
      initialTime: _alarmTime,
      helpText: '设置闹钟时间',
    );
    if (time != null) {
      setState(() {
        _alarmTime = time;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('设置闹钟')),
      body: ListView(
        children: [
          ListTile(
            title: const Text('时间'),
            subtitle: Text(_alarmTime.format(context)),
            trailing: const Icon(Icons.access_time),
            onTap: _selectAlarmTime,
          ),
          SwitchListTile(
            title: const Text('启用闹钟'),
            value: _alarmEnabled,
            onChanged: (value) {
              setState(() {
                _alarmEnabled = value;
              });
            },
          ),
          ListTile(
            title: const Text('标签'),
            subtitle: Text(_alarmLabel),
            trailing: const Icon(Icons.label),
            onTap: () async {
              final label = await showDialog<String>(
                context: context,
                builder: (context) => AlertDialog(
                  title: const Text('闹钟标签'),
                  content: TextField(
                    onChanged: (value) => _alarmLabel = value,
                    decoration: const InputDecoration(hintText: '输入标签'),
                  ),
                  actions: [
                    TextButton(
                      onPressed: () => Navigator.pop(context),
                      child: const Text('取消'),
                    ),
                    TextButton(
                      onPressed: () => Navigator.pop(context, _alarmLabel),
                      child: const Text('确定'),
                    ),
                  ],
                ),
              );
              if (label != null) {
                setState(() {
                  _alarmLabel = label;
                });
              }
            },
          ),
          const Divider(),
          const Padding(
            padding: EdgeInsets.all(16),
            child: Text('重复', style: TextStyle(fontWeight: FontWeight.bold)),
          ),
          Wrap(
            spacing: 8,
            children: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
                .map((day) => FilterChip(
                      label: Text(day),
                      selected: _repeatDays.contains(day),
                      onSelected: (selected) {
                        setState(() {
                          if (selected) {
                            _repeatDays.add(day);
                          } else {
                            _repeatDays.remove(day);
                          }
                        });
                      },
                    ))
                .toList(),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text('闹钟已设置: ${_alarmTime.format(context)}'),
            ),
          );
        },
        child: const Icon(Icons.save),
      ),
    );
  }
}

5.2 预约时间选择

使用 TimePicker 实现预约时间选择,限制工作时间:

dart 复制代码
class AppointmentPage extends StatefulWidget {
  const AppointmentPage({super.key});

  @override
  State<AppointmentPage> createState() => _AppointmentPageState();
}

class _AppointmentPageState extends State<AppointmentPage> {
  DateTime _selectedDate = DateTime.now();
  TimeOfDay? _selectedTime;
  String _serviceType = '普通咨询';

  final List<String> _serviceTypes = [
    '普通咨询',
    '专家咨询',
    'VIP 服务',
  ];

  Future<void> _selectDate() async {
    final DateTime? date = await showDatePicker(
      context: context,
      initialDate: _selectedDate,
      firstDate: DateTime.now(),
      lastDate: DateTime.now().add(const Duration(days: 30)),
    );
    if (date != null) {
      setState(() {
        _selectedDate = date;
        _selectedTime = null;
      });
    }
  }

  Future<void> _selectTime() async {
    final TimeOfDay? time = await showTimePicker(
      context: context,
      initialTime: _selectedTime ?? const TimeOfDay(hour: 9, minute: 0),
      helpText: '选择预约时间',
    );
    if (time != null) {
      setState(() {
        _selectedTime = time;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('预约服务')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text(
                    '选择日期',
                    style: TextStyle(fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 8),
                  ListTile(
                    title: Text(
                      '${_selectedDate.year}-${_selectedDate.month}-${_selectedDate.day}',
                    ),
                    trailing: const Icon(Icons.calendar_today),
                    onTap: _selectDate,
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text(
                    '选择时间(工作时间 9:00-18:00)',
                    style: TextStyle(fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 8),
                  ListTile(
                    title: Text(
                      _selectedTime != null
                          ? _selectedTime!.format(context)
                          : '请选择时间',
                    ),
                    trailing: const Icon(Icons.access_time),
                    onTap: _selectTime,
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text(
                    '服务类型',
                    style: TextStyle(fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 8),
                  Wrap(
                    spacing: 8,
                    children: _serviceTypes
                        .map((type) => ChoiceChip(
                              label: Text(type),
                              selected: _serviceType == type,
                              onSelected: (selected) {
                                if (selected) {
                                  setState(() {
                                    _serviceType = type;
                                  });
                                }
                              },
                            ))
                        .toList(),
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 24),
          ElevatedButton(
            onPressed: _selectedTime != null
                ? () {
                    ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(
                        content: Text(
                          '预约成功: ${_selectedDate.year}-${_selectedDate.month}-${_selectedDate.day} ${_selectedTime!.format(context)}',
                        ),
                      ),
                    );
                  }
                : null,
            child: const Text('确认预约'),
          ),
        ],
      ),
    );
  }
}

5.3 倒计时设置

使用 TimePicker 实现倒计时设置:

dart 复制代码
class CountdownSettingPage extends StatefulWidget {
  const CountdownSettingPage({super.key});

  @override
  State<CountdownSettingPage> createState() => _CountdownSettingPageState();
}

class _CountdownSettingPageState extends State<CountdownSettingPage> {
  TimeOfDay _duration = const TimeOfDay(hour: 0, minute: 5);
  bool _isRunning = false;
  int _remainingSeconds = 0;

  Future<void> _selectDuration() async {
    final TimeOfDay? time = await showTimePicker(
      context: context,
      initialTime: _duration,
      helpText: '设置倒计时时长',
      confirmText: '设置',
      cancelText: '取消',
    );
    if (time != null) {
      setState(() {
        _duration = time;
        _remainingSeconds = time.hour * 3600 + time.minute * 60;
      });
    }
  }

  void _startCountdown() {
    setState(() {
      _isRunning = true;
      _remainingSeconds = _duration.hour * 3600 + _duration.minute * 60;
    });
    _tick();
  }

  void _tick() {
    Future.delayed(const Duration(seconds: 1), () {
      if (_isRunning && _remainingSeconds > 0) {
        setState(() {
          _remainingSeconds--;
        });
        _tick();
      } else if (_remainingSeconds == 0) {
        setState(() {
          _isRunning = false;
        });
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('倒计时结束!')),
        );
      }
    });
  }

  void _stopCountdown() {
    setState(() {
      _isRunning = false;
    });
  }

  String _formatTime(int seconds) {
    final hours = seconds ~/ 3600;
    final minutes = (seconds % 3600) ~/ 60;
    final secs = seconds % 60;
    return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}';
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('倒计时')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              _formatTime(_remainingSeconds),
              style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 32),
            if (!_isRunning) ...[
              Text(
                '设置时长: ${_duration.format(context)}',
                style: const TextStyle(fontSize: 18),
              ),
              const SizedBox(height: 16),
              ElevatedButton.icon(
                onPressed: _selectDuration,
                icon: const Icon(Icons.timer),
                label: const Text('设置时长'),
              ),
              const SizedBox(height: 16),
              ElevatedButton.icon(
                onPressed: _startCountdown,
                icon: const Icon(Icons.play_arrow),
                label: const Text('开始'),
              ),
            ] else ...[
              ElevatedButton.icon(
                onPressed: _stopCountdown,
                icon: const Icon(Icons.stop),
                label: const Text('停止'),
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.red,
                ),
              ),
            ],
          ],
        ),
      ),
    );
  }
}

5.4 日程安排

使用 TimePicker 实现日程安排功能:

dart 复制代码
class SchedulePage extends StatefulWidget {
  const SchedulePage({super.key});

  @override
  State<SchedulePage> createState() => _SchedulePageState();
}

class _SchedulePageState extends State<SchedulePage> {
  final List<Schedule> _schedules = [];
  final TextEditingController _titleController = TextEditingController();

  Future<void> _addSchedule() async {
    final TimeOfDay? time = await showTimePicker(
      context: context,
      initialTime: TimeOfDay.now(),
      helpText: '选择日程时间',
    );
    if (time != null) {
      final title = await showDialog<String>(
        context: context,
        builder: (context) => AlertDialog(
          title: const Text('日程标题'),
          content: TextField(
            controller: _titleController,
            decoration: const InputDecoration(hintText: '输入日程标题'),
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('取消'),
            ),
            TextButton(
              onPressed: () => Navigator.pop(context, _titleController.text),
              child: const Text('确定'),
            ),
          ],
        ),
      );
      if (title != null && title.isNotEmpty) {
        setState(() {
          _schedules.add(Schedule(time: time, title: title));
          _schedules.sort((a, b) {
            final aMinutes = a.time.hour * 60 + a.time.minute;
            final bMinutes = b.time.hour * 60 + b.time.minute;
            return aMinutes.compareTo(bMinutes);
          });
        });
        _titleController.clear();
      }
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('日程安排')),
      body: _schedules.isEmpty
          ? const Center(child: Text('暂无日程,点击右下角添加'))
          : ListView.builder(
              itemCount: _schedules.length,
              itemBuilder: (context, index) {
                final schedule = _schedules[index];
                return ListTile(
                  leading: CircleAvatar(
                    child: Text(schedule.time.format(context)),
                  ),
                  title: Text(schedule.title),
                  trailing: IconButton(
                    icon: const Icon(Icons.delete),
                    onPressed: () {
                      setState(() {
                        _schedules.removeAt(index);
                      });
                    },
                  ),
                );
              },
            ),
      floatingActionButton: FloatingActionButton(
        onPressed: _addSchedule,
        child: const Icon(Icons.add),
      ),
    );
  }
}

class Schedule {
  final TimeOfDay time;
  final String title;

  Schedule({required this.time, required this.title});
}

六、CupertinoTimerPicker iOS 风格时间选择器

Flutter 还提供了 CupertinoTimerPicker,用于实现 iOS 风格的时间选择器。

6.1 基本用法

dart 复制代码
class CupertinoTimerPickerExample extends StatefulWidget {
  const CupertinoTimerPickerExample({super.key});

  @override
  State<CupertinoTimerPickerExample> createState() => _CupertinoTimerPickerExampleState();
}

class _CupertinoTimerPickerExampleState extends State<CupertinoTimerPickerExample> {
  Duration _duration = Duration.zero;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('iOS 风格时间选择器')),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(
            '${_duration.inHours.toString().padLeft(2, '0')}:${(_duration.inMinutes % 60).toString().padLeft(2, '0')}:${(_duration.inSeconds % 60).toString().padLeft(2, '0')}',
            style: const TextStyle(fontSize: 48),
          ),
          const SizedBox(height: 32),
          SizedBox(
            height: 200,
            child: CupertinoTimerPicker(
              mode: CupertinoTimerPickerMode.hms,
              initialTimerDuration: _duration,
              onTimerDurationChanged: (Duration newDuration) {
                setState(() {
                  _duration = newDuration;
                });
              },
            ),
          ),
        ],
      ),
    );
  }
}

6.2 CupertinoTimerPicker 模式

模式 说明
CupertinoTimerPickerMode.hms 时分秒
CupertinoTimerPickerMode.hm 时分
CupertinoTimerPickerMode.ms 分秒

6.3 配合 CupertinoActionSheet 使用

dart 复制代码
class CupertinoTimePickerSheet extends StatelessWidget {
  const CupertinoTimePickerSheet({super.key});

  void _showPicker(BuildContext context) {
    showCupertinoModalPopup(
      context: context,
      builder: (context) => Container(
        height: 300,
        color: CupertinoColors.systemBackground.resolveFrom(context),
        child: Column(
          children: [
            Container(
              height: 50,
              decoration: BoxDecoration(
                color: CupertinoColors.systemGrey5.resolveFrom(context),
              ),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  CupertinoButton(
                    onPressed: () => Navigator.pop(context),
                    child: const Text('取消'),
                  ),
                  CupertinoButton(
                    onPressed: () => Navigator.pop(context),
                    child: const Text('确定'),
                  ),
                ],
              ),
            ),
            Expanded(
              child: CupertinoTimerPicker(
                mode: CupertinoTimerPickerMode.hm,
                initialTimerDuration: Duration.zero,
                onTimerDurationChanged: (duration) {},
              ),
            ),
          ],
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('iOS 风格时间选择'),
      ),
      child: Center(
        child: CupertinoButton(
          onPressed: () => _showPicker(context),
          child: const Text('选择时间'),
        ),
      ),
    );
  }
}

七、最佳实践

7.1 用户体验优化

建议 说明
合理默认值 提供合理的初始时间
时间范围限制 根据业务限制可选时间
清晰提示 提供清晰的帮助文本
格式化显示 使用本地化格式显示时间

7.2 样式设计

建议 说明
主题一致 保持与应用整体主题一致
深色模式适配 支持深色模式
响应式布局 适配不同屏幕尺寸

7.3 交互设计

建议 说明
即时反馈 选择后即时显示结果
错误处理 处理无效时间选择
取消操作 允许用户取消选择

八、总结

TimePicker 是 Flutter 中用于时间选择的核心组件,提供了 Material Design 风格的时间选择体验。通过本文的学习,你应该已经掌握了:

  • TimePicker 的基本用法和核心概念
  • showTimePicker 函数的各种参数配置
  • TimePickerTheme 主题定制方法
  • 如何实现闹钟设置、预约时间、倒计时等实际应用
  • CupertinoTimerPicker iOS 风格时间选择器的使用
  • 时间选择的最佳实践

在实际开发中,TimePicker 常用于闹钟设置、预约系统、日程安排、倒计时等场景。结合时间范围限制和自定义样式,可以提供更好的用户体验。


九、完整示例代码

下面是一个完整的可运行示例,展示了 TimePicker 的各种用法:

dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'TimePicker 示例',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const TimePickerDemoPage(),
    );
  }
}

class TimePickerDemoPage extends StatefulWidget {
  const TimePickerDemoPage({super.key});

  @override
  State<TimePickerDemoPage> createState() => _TimePickerDemoPageState();
}

class _TimePickerDemoPageState extends State<TimePickerDemoPage> {
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('TimePicker 示例'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      drawer: Drawer(
        child: ListView(
          children: [
            const DrawerHeader(
              decoration: BoxDecoration(color: Colors.blue),
              child: Text(
                'TimePicker 示例',
                style: TextStyle(color: Colors.white, fontSize: 24),
              ),
            ),
            ListTile(
              leading: const Icon(Icons.access_time),
              title: const Text('基础时间选择'),
              selected: _selectedIndex == 0,
              onTap: () {
                setState(() => _selectedIndex = 0);
                Navigator.pop(context);
              },
            ),
            ListTile(
              leading: const Icon(Icons.alarm),
              title: const Text('闹钟设置'),
              selected: _selectedIndex == 1,
              onTap: () {
                setState(() => _selectedIndex = 1);
                Navigator.pop(context);
              },
            ),
            ListTile(
              leading: const Icon(Icons.event),
              title: const Text('预约时间'),
              selected: _selectedIndex == 2,
              onTap: () {
                setState(() => _selectedIndex = 2);
                Navigator.pop(context);
              },
            ),
            ListTile(
              leading: const Icon(Icons.timer),
              title: const Text('倒计时'),
              selected: _selectedIndex == 3,
              onTap: () {
                setState(() => _selectedIndex = 3);
                Navigator.pop(context);
              },
            ),
            ListTile(
              leading: const Icon(Icons.schedule),
              title: const Text('日程安排'),
              selected: _selectedIndex == 4,
              onTap: () {
                setState(() => _selectedIndex = 4);
                Navigator.pop(context);
              },
            ),
            ListTile(
              leading: const Icon(Icons.phone_iphone),
              title: const Text('iOS 风格选择器'),
              selected: _selectedIndex == 5,
              onTap: () {
                setState(() => _selectedIndex = 5);
                Navigator.pop(context);
              },
            ),
          ],
        ),
      ),
      body: _buildPage(),
    );
  }

  Widget _buildPage() {
    switch (_selectedIndex) {
      case 0:
        return const BasicTimePickerPage();
      case 1:
        return const AlarmSettingPage();
      case 2:
        return const AppointmentPage();
      case 3:
        return const CountdownSettingPage();
      case 4:
        return const SchedulePage();
      case 5:
        return const CupertinoTimerPickerExample();
      default:
        return const BasicTimePickerPage();
    }
  }
}

class BasicTimePickerPage extends StatefulWidget {
  const BasicTimePickerPage({super.key});

  @override
  State<BasicTimePickerPage> createState() => _BasicTimePickerPageState();
}

class _BasicTimePickerPageState extends State<BasicTimePickerPage> {
  TimeOfDay? _selectedTime;
  TimePickerEntryMode _entryMode = TimePickerEntryMode.dial;

  Future<void> _selectTime() async {
    final TimeOfDay? time = await showTimePicker(
      context: context,
      initialTime: _selectedTime ?? TimeOfDay.now(),
      initialEntryMode: _entryMode,
      helpText: '选择时间',
      cancelText: '取消',
      confirmText: '确定',
    );
    if (time != null) {
      setState(() {
        _selectedTime = time;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(
            _selectedTime != null
                ? '选择的时间: ${_selectedTime!.format(context)}'
                : '请选择时间',
            style: const TextStyle(fontSize: 24),
          ),
          const SizedBox(height: 24),
          SegmentedButton<TimePickerEntryMode>(
            segments: const [
              ButtonSegment(
                value: TimePickerEntryMode.dial,
                label: Text('表盘'),
                icon: Icon(Icons.dialpad),
              ),
              ButtonSegment(
                value: TimePickerEntryMode.input,
                label: Text('输入'),
                icon: Icon(Icons.keyboard),
              ),
            ],
            selected: {_entryMode},
            onSelectionChanged: (Set<TimePickerEntryMode> selection) {
              setState(() {
                _entryMode = selection.first;
              });
            },
          ),
          const SizedBox(height: 24),
          ElevatedButton.icon(
            onPressed: _selectTime,
            icon: const Icon(Icons.access_time),
            label: const Text('选择时间'),
          ),
        ],
      ),
    );
  }
}

class AlarmSettingPage extends StatefulWidget {
  const AlarmSettingPage({super.key});

  @override
  State<AlarmSettingPage> createState() => _AlarmSettingPageState();
}

class _AlarmSettingPageState extends State<AlarmSettingPage> {
  TimeOfDay _alarmTime = const TimeOfDay(hour: 7, minute: 0);
  bool _alarmEnabled = true;
  String _alarmLabel = '闹钟';
  final Set<String> _repeatDays = {};

  Future<void> _selectAlarmTime() async {
    final TimeOfDay? time = await showTimePicker(
      context: context,
      initialTime: _alarmTime,
      helpText: '设置闹钟时间',
    );
    if (time != null) {
      setState(() {
        _alarmTime = time;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        ListTile(
          title: const Text('时间'),
          subtitle: Text(
            _alarmTime.format(context),
            style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
          ),
          trailing: const Icon(Icons.access_time),
          onTap: _selectAlarmTime,
        ),
        SwitchListTile(
          title: const Text('启用闹钟'),
          value: _alarmEnabled,
          onChanged: (value) {
            setState(() {
              _alarmEnabled = value;
            });
          },
        ),
        ListTile(
          title: const Text('标签'),
          subtitle: Text(_alarmLabel),
          trailing: const Icon(Icons.label),
          onTap: () async {
            final controller = TextEditingController(text: _alarmLabel);
            final label = await showDialog<String>(
              context: context,
              builder: (context) => AlertDialog(
                title: const Text('闹钟标签'),
                content: TextField(
                  controller: controller,
                  decoration: const InputDecoration(hintText: '输入标签'),
                ),
                actions: [
                  TextButton(
                    onPressed: () => Navigator.pop(context),
                    child: const Text('取消'),
                  ),
                  TextButton(
                    onPressed: () => Navigator.pop(context, controller.text),
                    child: const Text('确定'),
                  ),
                ],
              ),
            );
            if (label != null && label.isNotEmpty) {
              setState(() {
                _alarmLabel = label;
              });
            }
            controller.dispose();
          },
        ),
        const Divider(),
        const Padding(
          padding: EdgeInsets.all(16),
          child: Text('重复', style: TextStyle(fontWeight: FontWeight.bold)),
        ),
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: Wrap(
            spacing: 8,
            children: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
                .map((day) => FilterChip(
                      label: Text(day),
                      selected: _repeatDays.contains(day),
                      onSelected: (selected) {
                        setState(() {
                          if (selected) {
                            _repeatDays.add(day);
                          } else {
                            _repeatDays.remove(day);
                          }
                        });
                      },
                    ))
                .toList(),
          ),
        ),
      ],
    );
  }
}

class AppointmentPage extends StatefulWidget {
  const AppointmentPage({super.key});

  @override
  State<AppointmentPage> createState() => _AppointmentPageState();
}

class _AppointmentPageState extends State<AppointmentPage> {
  DateTime _selectedDate = DateTime.now();
  TimeOfDay? _selectedTime;
  String _serviceType = '普通咨询';

  final List<String> _serviceTypes = ['普通咨询', '专家咨询', 'VIP 服务'];

  Future<void> _selectDate() async {
    final DateTime? date = await showDatePicker(
      context: context,
      initialDate: _selectedDate,
      firstDate: DateTime.now(),
      lastDate: DateTime.now().add(const Duration(days: 30)),
    );
    if (date != null) {
      setState(() {
        _selectedDate = date;
        _selectedTime = null;
      });
    }
  }

  Future<void> _selectTime() async {
    final TimeOfDay? time = await showTimePicker(
      context: context,
      initialTime: _selectedTime ?? const TimeOfDay(hour: 9, minute: 0),
      helpText: '选择预约时间(工作时间 9:00-18:00)',
    );
    if (time != null) {
      setState(() {
        _selectedTime = time;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        Card(
          child: ListTile(
            title: const Text('选择日期'),
            subtitle: Text(
              '${_selectedDate.year}-${_selectedDate.month.toString().padLeft(2, '0')}-${_selectedDate.day.toString().padLeft(2, '0')}',
            ),
            trailing: const Icon(Icons.calendar_today),
            onTap: _selectDate,
          ),
        ),
        const SizedBox(height: 16),
        Card(
          child: ListTile(
            title: const Text('选择时间'),
            subtitle: Text(
              _selectedTime != null
                  ? _selectedTime!.format(context)
                  : '请选择时间(工作时间 9:00-18:00)',
            ),
            trailing: const Icon(Icons.access_time),
            onTap: _selectTime,
          ),
        ),
        const SizedBox(height: 16),
        Card(
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text(
                  '服务类型',
                  style: TextStyle(fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 8),
                Wrap(
                  spacing: 8,
                  children: _serviceTypes
                      .map((type) => ChoiceChip(
                            label: Text(type),
                            selected: _serviceType == type,
                            onSelected: (selected) {
                              if (selected) {
                                setState(() {
                                  _serviceType = type;
                                });
                              }
                            },
                          ))
                      .toList(),
                ),
              ],
            ),
          ),
        ),
        const SizedBox(height: 24),
        ElevatedButton(
          onPressed: _selectedTime != null
              ? () {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content: Text(
                        '预约成功: ${_selectedDate.year}-${_selectedDate.month}-${_selectedDate.day} ${_selectedTime!.format(context)}',
                      ),
                    ),
                  );
                }
              : null,
          child: const Text('确认预约'),
        ),
      ],
    );
  }
}

class CountdownSettingPage extends StatefulWidget {
  const CountdownSettingPage({super.key});

  @override
  State<CountdownSettingPage> createState() => _CountdownSettingPageState();
}

class _CountdownSettingPageState extends State<CountdownSettingPage> {
  TimeOfDay _duration = const TimeOfDay(hour: 0, minute: 5);
  bool _isRunning = false;
  int _remainingSeconds = 300;

  Future<void> _selectDuration() async {
    final TimeOfDay? time = await showTimePicker(
      context: context,
      initialTime: _duration,
      helpText: '设置倒计时时长',
    );
    if (time != null) {
      setState(() {
        _duration = time;
        _remainingSeconds = time.hour * 3600 + time.minute * 60;
      });
    }
  }

  void _startCountdown() {
    setState(() {
      _isRunning = true;
      _remainingSeconds = _duration.hour * 3600 + _duration.minute * 60;
    });
    _tick();
  }

  void _tick() {
    Future.delayed(const Duration(seconds: 1), () {
      if (_isRunning && _remainingSeconds > 0) {
        setState(() {
          _remainingSeconds--;
        });
        _tick();
      } else if (_remainingSeconds == 0) {
        setState(() {
          _isRunning = false;
        });
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('倒计时结束!')),
        );
      }
    });
  }

  void _stopCountdown() {
    setState(() {
      _isRunning = false;
    });
  }

  String _formatTime(int seconds) {
    final hours = seconds ~/ 3600;
    final minutes = (seconds % 3600) ~/ 60;
    final secs = seconds % 60;
    return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}';
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(
            _formatTime(_remainingSeconds),
            style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 32),
          if (!_isRunning) ...[
            Text(
              '设置时长: ${_duration.format(context)}',
              style: const TextStyle(fontSize: 18),
            ),
            const SizedBox(height: 16),
            ElevatedButton.icon(
              onPressed: _selectDuration,
              icon: const Icon(Icons.timer),
              label: const Text('设置时长'),
            ),
            const SizedBox(height: 16),
            ElevatedButton.icon(
              onPressed: _startCountdown,
              icon: const Icon(Icons.play_arrow),
              label: const Text('开始'),
            ),
          ] else ...[
            ElevatedButton.icon(
              onPressed: _stopCountdown,
              icon: const Icon(Icons.stop),
              label: const Text('停止'),
              style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
            ),
          ],
        ],
      ),
    );
  }
}

class SchedulePage extends StatefulWidget {
  const SchedulePage({super.key});

  @override
  State<SchedulePage> createState() => _SchedulePageState();
}

class _SchedulePageState extends State<SchedulePage> {
  final List<Schedule> _schedules = [];

  Future<void> _addSchedule() async {
    final TimeOfDay? time = await showTimePicker(
      context: context,
      initialTime: TimeOfDay.now(),
      helpText: '选择日程时间',
    );
    if (time != null) {
      final controller = TextEditingController();
      final title = await showDialog<String>(
        context: context,
        builder: (context) => AlertDialog(
          title: const Text('日程标题'),
          content: TextField(
            controller: controller,
            decoration: const InputDecoration(hintText: '输入日程标题'),
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('取消'),
            ),
            TextButton(
              onPressed: () => Navigator.pop(context, controller.text),
              child: const Text('确定'),
            ),
          ],
        ),
      );
      if (title != null && title.isNotEmpty) {
        setState(() {
          _schedules.add(Schedule(time: time, title: title));
          _schedules.sort((a, b) {
            final aMinutes = a.time.hour * 60 + a.time.minute;
            final bMinutes = b.time.hour * 60 + b.time.minute;
            return aMinutes.compareTo(bMinutes);
          });
        });
      }
      controller.dispose();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _schedules.isEmpty
          ? const Center(child: Text('暂无日程,点击右下角添加'))
          : ListView.builder(
              itemCount: _schedules.length,
              itemBuilder: (context, index) {
                final schedule = _schedules[index];
                return ListTile(
                  leading: CircleAvatar(
                    child: Text(schedule.time.format(context)),
                  ),
                  title: Text(schedule.title),
                  trailing: IconButton(
                    icon: const Icon(Icons.delete),
                    onPressed: () {
                      setState(() {
                        _schedules.removeAt(index);
                      });
                    },
                  ),
                );
              },
            ),
      floatingActionButton: FloatingActionButton(
        onPressed: _addSchedule,
        child: const Icon(Icons.add),
      ),
    );
  }
}

class Schedule {
  final TimeOfDay time;
  final String title;

  Schedule({required this.time, required this.title});
}

class CupertinoTimerPickerExample extends StatefulWidget {
  const CupertinoTimerPickerExample({super.key});

  @override
  State<CupertinoTimerPickerExample> createState() => _CupertinoTimerPickerExampleState();
}

class _CupertinoTimerPickerExampleState extends State<CupertinoTimerPickerExample> {
  Duration _duration = const Duration(hours: 0, minutes: 5, seconds: 0);

  String _formatDuration(Duration d) {
    return '${d.inHours.toString().padLeft(2, '0')}:${(d.inMinutes % 60).toString().padLeft(2, '0')}:${(d.inSeconds % 60).toString().padLeft(2, '0')}';
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(
          _formatDuration(_duration),
          style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 32),
        SizedBox(
          height: 200,
          child: CupertinoTimerPicker(
            mode: CupertinoTimerPickerMode.hms,
            initialTimerDuration: _duration,
            onTimerDurationChanged: (Duration newDuration) {
              setState(() {
                _duration = newDuration;
              });
            },
          ),
        ),
      ],
    );
  }
}

参考资料

相关推荐
哈__2 小时前
基础入门 Flutter for OpenHarmony:app_settings 系统设置跳转详解
flutter
键盘鼓手苏苏2 小时前
Flutter for OpenHarmony 实战:Envied — 环境变量与私钥安全守护者
开发语言·安全·flutter·华为·rust·harmonyos
2501_921930832 小时前
基础入门 Flutter for OpenHarmony:RangeSlider 范围滑块组件详解
flutter
早點睡3903 小时前
基础入门 Flutter for OpenHarmony:InteractiveViewer 交互式查看器详解
flutter
Bowen_J3 小时前
Flutter 为什么能运行在 HarmonyOS 上
flutter·架构·harmonyos
2501_921930834 小时前
基础入门 Flutter for OpenHarmony:图片处理工作流实战
flutter
2501_921930835 小时前
基础入门 Flutter for OpenHarmony:SearchAnchor 搜索锚点组件详解
flutter
早點睡3905 小时前
基础入门 Flutter for OpenHarmony:ReorderableListView 可排序列表详解
flutter
早點睡3907 小时前
基础入门 Flutter for OpenHarmony:DataTable 数据表格组件详解
flutter