Flutter 自定义日期时间选取器

380 阅读1分钟

image.png

需求

  1. 支持选择开始时间、结束时间
  2. 支持多选星期
  3. 选择的结束时间不能比开始时间小
  4. 时间列表滚桶(车轮)式转动
import 'package:flutter/material.dart';

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

  @override
  _CustomDateTimePickerState createState() => _CustomDateTimePickerState();
}

class _CustomDateTimePickerState extends State<CustomDateTimePicker> {
  DateTime _startTime = DateTime.now();
  DateTime _endTime = DateTime.now();
  final List<bool> _selectedDays = List.generate(7, (index) => false);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Column(
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              Column(
                children: [
                  Text("开始时间"),
                  _buildTimePicker(true),
                ],
              ),
              Column(
                children: [
                  Text("结束时间"),
                  _buildTimePicker(false),
                ],
              ),
            ],
          ),
          SizedBox(height: 20),
          Text("选择星期"),
          Wrap(
            children: List.generate(
              7,
              (index) => ElevatedButton(
                onPressed: () {
                  setState(() {
                    _selectedDays[index] = !_selectedDays[index];
                  });
                },
                style: ElevatedButton.styleFrom(
                  primary: _selectedDays[index] ? Colors.blue : Colors.grey,
                ),
                child: Text(
                  _getDayAbbreviation(index),
                ),
              ),
            ),
          ),
          SizedBox(height: 20),
          Text("选择的时间: ${_formatTime(_startTime)} - ${_formatTime(_endTime)}"),
          Text("选择的星期: ${_getSelectedDays()}"),
        ],
      ),
    );
  }

  Widget _buildTimePicker(bool isStartTime) {
    DateTime selectedTime = isStartTime ? _startTime : _endTime;

    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        _buildWheelPicker(selectedTime.hour, 24, (value) {
          setState(() {
            if (isStartTime) {
              _startTime = DateTime(
                _startTime.year,
                _startTime.month,
                _startTime.day,
                value,
                _startTime.minute,
              );
            } else {
              _endTime = DateTime(
                _endTime.year,
                _endTime.month,
                _endTime.day,
                value,
                _endTime.minute,
              );
            }
            
            // 检查结束时间是否小于开始时间
              if (_endTime.isBefore(_startTime)) {
                print("选择的时间不对");
              }
              
          });
        }),
        const Text(":", style: TextStyle(fontSize: 18, color: Colors.blue)),
        _buildWheelPicker(selectedTime.minute, 60, (value) {
          setState(() {
            if (isStartTime) {
              _startTime = DateTime(
                _startTime.year,
                _startTime.month,
                _startTime.day,
                _startTime.hour,
                value,
              );
            } else {
              _endTime = DateTime(
                _endTime.year,
                _endTime.month,
                _endTime.day,
                _endTime.hour,
                value,
              );
            }
            
            // 检查结束时间是否小于开始时间
              if (_endTime.isBefore(_startTime)) {
                print("选择的时间不对");
              }
              
          });
        }),
      ],
    );
  }

  Widget _buildWheelPicker(int value, int maxValue, ValueChanged<int> onChanged) {
    return SizedBox(
      width: 50,
      height: 120,
      child: ListWheelScrollView(
        itemExtent: 40,
        //上下子控件数量
        squeeze: 1,
        physics: const FixedExtentScrollPhysics(),
        //圆柱投影透视图,类似OpenGLES中透视投影,理解为看圆柱的距离,为0时表示从无限远处看,1表示从无限近处看,值的范围(0,0.01],注意是左开右闭区间
        perspective: 0.007,
        //圆筒直径和主轴渲染窗口的尺寸的比,默认值是2,如果是垂直方向,主轴渲染窗口的尺寸是ListWheelScrollView的高。diameterRatio越小表示圆筒越圆。
        diameterRatio: 1.5,
        //水平偏离中心的程度
        offAxisFraction: 0,
        //是否启用放大镜
        useMagnifier: true,
        //放大倍率
        magnification: 1.5,
        onSelectedItemChanged: (index) {
          onChanged(index);
        },
        controller: FixedExtentScrollController(initialItem: value),
        children: List.generate(
          maxValue,
          (index) => Center(
            child: Text(
              index.toString().padLeft(2, '0'),
              style: TextStyle(
                fontSize: 18,
                color: index == value ? Colors.blue : Colors.grey,
              ),
            ),
          ),
        ),
      ),
    );
  }

  String _getDayAbbreviation(int index) {
    return ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][index];
  }

  String _formatTime(DateTime time) {
    return "${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}";
  }

  String _getSelectedDays() {
    List<String> selectedDays = [];
    for (int i = 0; i < _selectedDays.length; i++) {
      if (_selectedDays[i]) {
        selectedDays.add(_getDayAbbreviation(i));
      }
    }
    return selectedDays.join(", ");
  }
}