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

如何在Flutter中实现如图所示的可滑动时间范围选择器

自定义可滑动时间范围选择器实现方案(Flutter)

如果现有第三方包满足不了需求,完全可以自定义实现一个轻量的滑动时间范围选择器,核心依赖手势监听和布局计算,以下是具体实现步骤和代码示例:

核心实现思路

  • 用Stack层叠布局:底层放时间刻度、中间放选中范围的高亮背景、顶层放左右两个可拖动滑块
  • 通过GestureDetector监听拖动手势,实时更新滑块位置,并限制滑块边界(左滑块不能超过右滑块,且两者不能超出总范围)
  • 将滑块的像素偏移量映射为对应的时间值(比如总宽度对应24小时,按比例换算)

完整代码示例

import 'package:flutter/material.dart';

class SlidableTimeRangePicker extends StatefulWidget {
  final ValueChanged<TimeRange>? onRangeChanged;

  const SlidableTimeRangePicker({super.key, this.onRangeChanged});

  @override
  State<SlidableTimeRangePicker> createState() => _SlidableTimeRangePickerState();
}

class _SlidableTimeRangePickerState extends State<SlidableTimeRangePicker> {
  // 总宽度(初始化后由布局确定)
  double _totalWidth = 0;
  // 左右滑块的偏移量(单位:px)
  double _leftOffset = 40;
  double _rightOffset = 200;
  // 时间范围:0-24小时
  final int _totalHours = 24;
  // 当前选中的时间范围
  late TimeRange _selectedRange;

  @override
  void initState() {
    super.initState();
    _updateSelectedRange();
  }

  // 将偏移量转换为时间范围
  void _updateSelectedRange() {
    final startHour = (_leftOffset / _totalWidth) * _totalHours;
    final endHour = (_rightOffset / _totalWidth) * _totalHours;
    _selectedRange = TimeRange(
      start: TimeOfDay(hour: startHour.floor(), minute: ((startHour - startHour.floor()) * 60).round()),
      end: TimeOfDay(hour: endHour.floor(), minute: ((endHour - endHour.floor()) * 60).round()),
    );
    widget.onRangeChanged?.call(_selectedRange);
  }

  // 处理滑块拖动
  void _handlePanUpdate(DragUpdateDetails details, bool isLeftSlider) {
    if (_totalWidth == 0) return;

    setState(() {
      if (isLeftSlider) {
        _leftOffset += details.delta.dx;
        // 限制左滑块边界:不能小于0,不能超过右滑块
        _leftOffset = _leftOffset.clamp(0, _rightOffset - 20);
      } else {
        _rightOffset += details.delta.dx;
        // 限制右滑块边界:不能小于左滑块,不能超过总宽度
        _rightOffset = _rightOffset.clamp(_leftOffset + 20, _totalWidth);
      }
      _updateSelectedRange();
    });
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        if (_totalWidth == 0) {
          _totalWidth = constraints.maxWidth;
          // 初始化时确保滑块在合理范围
          _leftOffset = _leftOffset.clamp(0, _totalWidth - 40);
          _rightOffset = _rightOffset.clamp(_leftOffset + 20, _totalWidth);
          _updateSelectedRange();
        }

        return Column(
          children: [
            // 显示当前选中的时间范围
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 8.0),
              child: Text(
                '${_selectedRange.start.format(context)} - ${_selectedRange.end.format(context)}',
                style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
              ),
            ),
            // 核心滑动选择区域
            SizedBox(
              height: 60,
              child: Stack(
                alignment: Alignment.bottomCenter,
                children: [
                  // 时间刻度
                  Positioned(
                    bottom: 0,
                    left: 0,
                    right: 0,
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: List.generate(_totalHours + 1, (index) {
                        return SizedBox(
                          width: _totalWidth / _totalHours,
                          child: Text(
                            '$index',
                            textAlign: TextAlign.center,
                            style: const TextStyle(fontSize: 12),
                          ),
                        );
                      }),
                    ),
                  ),
                  // 选中范围高亮背景
                  Positioned(
                    bottom: 12,
                    left: _leftOffset,
                    right: _totalWidth - _rightOffset,
                    height: 20,
                    child: Container(
                      decoration: BoxDecoration(
                        color: Colors.blue.withOpacity(0.3),
                        borderRadius: BorderRadius.circular(10),
                      ),
                    ),
                  ),
                  // 左滑块
                  Positioned(
                    left: _leftOffset - 10,
                    bottom: 8,
                    child: GestureDetector(
                      onPanUpdate: (details) => _handlePanUpdate(details, true),
                      child: Container(
                        width: 20,
                        height: 28,
                        decoration: BoxDecoration(
                          color: Colors.blue,
                          borderRadius: BorderRadius.circular(10),
                          boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 2)],
                        ),
                      ),
                    ),
                  ),
                  // 右滑块
                  Positioned(
                    left: _rightOffset - 10,
                    bottom: 8,
                    child: GestureDetector(
                      onPanUpdate: (details) => _handlePanUpdate(details, false),
                      child: Container(
                        width: 20,
                        height: 28,
                        decoration: BoxDecoration(
                          color: Colors.blue,
                          borderRadius: BorderRadius.circular(10),
                          boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 2)],
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ],
        );
      },
    );
  }
}

// 时间范围模型类
class TimeRange {
  final TimeOfDay start;
  final TimeOfDay end;

  TimeRange({required this.start, required this.end});
}

// 使用示例
// SlidableTimeRangePicker(
//   onRangeChanged: (range) {
//     print('选中时间:${range.start.format(context)} - ${range.end.format(context)}');
//   },
// )

可自定义扩展的点

  • 修改滑块的样式(颜色、大小、阴影)
  • 调整选中区域的背景样式(渐变、圆角)
  • 支持分钟级精度的刻度显示
  • 添加点击定位滑块的功能
  • 限制可选择的最小时间跨度(比如最少1小时)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 09:18:29