如何在Flutter中实现与参考图效果一致的time range picker时间区间选择器
Flutter 时间区间选择器实现方案
实现逻辑
这种带双滑块的时间区间选择器核心基于自定义绘制+手势识别实现,不需要引入外部依赖,核心拆分三个模块即可完成:
- 时间轴绘制:将你需要的时间范围(通常是24小时)映射到Canvas的横向宽度,按指定步长(比如15分钟/1小时)绘制刻度、时间标签
- 双滑块交互:通过手势识别监听水平拖拽动作,分别处理开始时间滑块和结束时间滑块的位置更新,逻辑上限制左滑块不能超过右滑块,可自定义两个时间的最小间隔
- 状态同步:滑块位置变动时实时换算为对应的时间值,同步更新顶部的选中时间段文本显示,同时重绘两个滑块之间的高亮选中区域
核心代码示例
首先定义时间区间数据结构:
class TimeRange { final DateTime startTime; final DateTime endTime; const TimeRange({required this.startTime, required this.endTime}); // 可选:新增获取时间段时长的方法 Duration get duration => endTime.difference(startTime); }
然后实现自定义绘制类,负责画时间轴、滑块和选中区域:
class TimeRangePickerPainter extends CustomPainter { final double leftSliderOffset; final double rightSliderOffset; final Color backgroundColor; final Color highlightColor; final Color sliderColor; TimeRangePickerPainter({ required this.leftSliderOffset, required this.rightSliderOffset, this.backgroundColor = const Color(0xFFEEEEEE), this.highlightColor = const Color(0xFF2196F3), this.sliderColor = Colors.white, }); @override void paint(Canvas canvas, Size size) { final paint = Paint()..style = PaintingStyle.fill; // 绘制背景轨道 paint.color = backgroundColor; canvas.drawRRect( RRect.fromRectAndRadius(Rect.fromLTWH(0, 20, size.width, 40), const Radius.circular(8)), paint, ); // 绘制选中高亮区域 paint.color = highlightColor.withOpacity(0.3); canvas.drawRRect( RRect.fromRectAndRadius(Rect.fromLTWH(leftSliderOffset, 20, rightSliderOffset - leftSliderOffset, 40), const Radius.circular(8)), paint, ); // 绘制左滑块 paint.color = sliderColor; canvas.drawCircle(Offset(leftSliderOffset, 40), 14, paint); canvas.drawCircle(Offset(leftSliderOffset, 40), 10, Paint()..color = highlightColor); // 绘制右滑块 canvas.drawCircle(Offset(rightSliderOffset, 40), 14, paint); canvas.drawCircle(Offset(rightSliderOffset, 40), 10, Paint()..color = highlightColor); // 按需补充绘制刻度、时间标签逻辑 } @override bool shouldRepaint(covariant TimeRangePickerPainter oldDelegate) { return leftSliderOffset != oldDelegate.leftSliderOffset || rightSliderOffset != oldDelegate.rightSliderOffset; } }
最后实现交互逻辑,处理拖拽事件:
class TimeRangePicker extends StatefulWidget { const TimeRangePicker({super.key}); @override State<TimeRangePicker> createState() => _TimeRangePickerState(); } class _TimeRangePickerState extends State<TimeRangePicker> { double _leftPos = 0; double _rightPos = 200; // 按实际需求设置时间步长,这里设为15分钟 final int _stepMinutes = 15; // 总时间范围为24小时,换算为总分钟数 final int _totalMinutes = 24 * 60; // 将滑块位置换算为时间 DateTime _offsetToTime(double offset, double totalWidth) { final ratio = offset / totalWidth; final minutes = (ratio * _totalMinutes / _stepMinutes).round() * _stepMinutes; return DateTime(2024, 1, 1, 0, minutes); } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final totalWidth = constraints.maxWidth; // 计算当前选中的时间区间 final startTime = _offsetToTime(_leftPos, totalWidth); final endTime = _offsetToTime(_rightPos, totalWidth); return Column( mainAxisSize: MainAxisSize.min, children: [ // 顶部显示选中的时间 Text( '${startTime.hour.toString().padLeft(2, '0')}:${startTime.minute.toString().padLeft(2, '0')} - ${endTime.hour.toString().padLeft(2, '0')}:${endTime.minute.toString().padLeft(2, '0')}', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500), ), const SizedBox(height: 16), GestureDetector( onHorizontalDragUpdate: (details) { // 判断当前拖拽的是左滑块还是右滑块 final touchX = details.localPosition.dx; if ((touchX - _leftPos).abs() < (touchX - _rightPos).abs()) { // 拖拽左滑块,限制不超过右滑块,最小为0 setState(() { _leftPos = touchX.clamp(0, _rightPos - 40); }); } else { // 拖拽右滑块,限制不小于左滑块,最大为总宽度 setState(() { _rightPos = touchX.clamp(_leftPos + 40, totalWidth); }); } }, child: CustomPaint( painter: TimeRangePickerPainter( leftSliderOffset: _leftPos, rightSliderOffset: _rightPos, ), size: Size(totalWidth, 80), ), ), ], ); }, ); } }
优化建议
- 拖拽时添加步进吸附逻辑,让滑块自动对齐到最近的时间刻度,避免出现非步长的零散时间
- 可添加触觉反馈,拖拽滑块和选中时间时触发轻微震动,提升交互体验
- 如需支持跨天选择,只需在时间换算逻辑中判断结束时间小于开始时间时自动加1天即可,无需修改UI绘制逻辑
内容的提问来源于stack exchange,提问作者Abdullah La
相关产品推荐
相关产品推荐

