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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.27 04:45:03