通用 Flutter 日期时间选择器 (AppDatePicker)

License: MIT Platform: Flutter

一个功能强大、高度可定制、易于使用的 Flutter 日期和时间选择器组件。通过简单统一的 API,可以实现多种选择模式,满足绝大部分应用场景的需求。

✨ 特性

  • 多种选择模式:
    • 📅 编辑日期 (editDate): 完整选择年、月、日、时、分。
    • 编辑时间 (editTime): 仅选择时、分。
    • 🚀 开始/结束时间 (startTime/endTime): 仅选择年、月、日,常用于设置日期范围。
    • 🔍 筛选模式 (filterDate): 按 进行快速筛选。
  • 高度可定制:
    • 支持设置可选的最小开始时间 (startTime)。
    • 可限制选择范围,不允许选择未来时间 (showLaterTime: false)。
    • 支持设置筛选模式下的回溯年限 (yearsBack)。
  • 优秀的开发者体验:
    • 通过静态方法 AppDatePicker.show() 一行代码即可调用。
    • 统一的回调接口 onConfirm 获取最终选择结果。
    • 实时变化回调 onChange,方便监听选择过程。
  • 精心设计的 UI:
    • 简洁、现代化的设计,适配亮色模式。
    • 滚动选择器带有清晰的选中状态和动画效果。

📐 设计理念与实现原理

这个日期选择器在重构过程中遵循了几个核心设计原则:

  1. 分层与解耦 (Layering & Decoupling)

    • 底层 (CustomPicker): 这是最核心的滚动选择器引擎。它被设计成一个“哑组件”(Dumb Component),完全不关心业务逻辑。它只负责提供一个可滚动的列表,并通过 itemBuilder 回调将列表项的 UI 构建权完全交还给调用方。这使得 CustomPicker 具备极高的复用性,可以用于任何需要滚动选择的场景。
    • 业务逻辑层 (EditDate, FilterDate, EditTime): 这些组件基于 CustomPicker 构建,封装了各自的业务逻辑。例如,EditDate 负责处理年、月、日、时、分之间的联动和约束关系(如二月没有30号,选择的时间不能早于开始时间等)。
    • 入口/展示层 (AppDatePicker, DatePickerOverlay): 这是暴露给开发者的最顶层 API。AppDatePicker.show() 使用 Overlay 来创建一个全局浮层,将整个组件呈现在屏幕上,并管理其生命周期。它将复杂的实现细节完全隐藏,提供了一个极其简洁的调用方式。
  2. 统一状态管理 (Centralized State Management)

    • EditDate 组件中,我们摒弃了分散管理 selectedYear, selectedMonth, selectedDay 等多个状态变量的做法。取而代之的是,使用一个单一的 DateTime 对象 _selectedDate 来作为唯一的数据源 (Single Source of Truth)。
    • 当任何一个选择器(如年份选择器)的值发生变化时,我们会创建一个新的 DateTime 实例,然后通过一个统一的 _constrainDate 方法来修正这个值,确保其合法性,最后通过 setState 更新 _selectedDate。这种方式让状态管理更集中、逻辑更清晰、代码更健壮。
  3. 组合优于继承 (Composition over Inheritance)

    • 整个组件是 छोटे widgets 组合而成的。DatePickerOverlay 根据传入的 mode 动态地组合 EditDateFilterDateEditTime 组件,而不是通过继承来扩展功能。这种方式使得代码结构更加灵活和易于维护。

🔧 使用方法

1. 导入

import 'path/to/your/date_picker.dart';

2. 调用 AppDatePicker.show()

示例 1: 编辑日期 (可选择未来时间)

ElevatedButton(
  onPressed: () => AppDatePicker.show(
    context: context,
    mode: AppDatePickerMode.editDate,
    initialDateTime: DateTime(2024, 2, 18, 17, 12),
    showLaterTime: true, // 允许选择未来的时间
    onConfirm: (dateTime) {
      // dateTime 是一个 DateTime 对象
      print("确定时间: $dateTime");
    },
    onChange: (dateTime) {
      print("选择变化: $dateTime");
    },
  ),
  child: const Text("编辑日期"),
),

示例 2: 筛选时间 (按周)

ElevatedButton(
  onPressed: () => AppDatePicker.show(
    context: context,
    mode: AppDatePickerMode.filterDate,
    filterType: FilterType.week,
    initialDateTime: DateTime(2024, 12, 2, 17, 22),
    onConfirm: (dateRange) {
      // dateRange 是一个 Map<String, DateTime>
      // e.g., {startTime: 2024-12-02 00:00:00.000, endTime: 2024-12-08 00:00:00.000}
      print("确定范围: $dateRange");
    },
  ),
  child: const Text("筛选时间-周"),
),

示例 3: 设置结束时间 (带开始时间限制)

// 假设 startTime 是一个已选的 DateTime
DateTime startTime = DateTime(2024, 6, 20);
DateTime endTime = DateTime(2024, 7, 5);

ElevatedButton(
  onPressed: () {
    AppDatePicker.show(
      context: context,
      mode: AppDatePickerMode.endTime,
      initialDateTime: endTime,
      startTime: startTime, // 传入开始时间,结束时间不能早于它
      showLaterTime: false, // 不允许选择未来
      onConfirm: (date) {
        // date 是一个 DateTime 对象,时分秒为0
        setState(() {
          endTime = date;
        });
      },
    );
  },
  child: Text("选择结束时间"),
),

示例 4: 自定义筛选周UI和标题

ElevatedButton(
  onPressed: () {
    AppDatePicker.show(
      context: context,
      mode: AppDatePickerMode.filterDate,
      filterType: FilterType.week,
      title: "选择统计周期",
      onConfirm: (result) {},
      weekItemBuilder: (context, weekData, isSelected) {
        final dateRange = weekData.split('(').first;
        final isThisWeek = weekData.contains('本周');
        return Card(
          color: isSelected ? Colors.blue.shade50 : Colors.white,
          elevation: isSelected ? 2 : 0,
          child: Center(
            child: Row(
              mainAxisAlignment:MainAxisAlignment.center,
              children: [
                Text(
                  dateRange,
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
                    color: isSelected ? Colors.blue.shade800 : Colors.grey.shade700,
                  ),
                ),
                if(isThisWeek)
                  const Icon(Icons.star, color: Colors.amber, size: 16)
              ],
            ),
          ),
        );
      },
    );
  },
  child: const Text('筛选时间-周 (自定义UI)'),
),

📚 API 参考

AppDatePicker.show() 方法的主要参数:

参数 类型 描述 是否必须
context BuildContext 构建上下文
mode AppDatePickerMode 选择器模式
onConfirm Function(dynamic) 点击"确定"的回调
initialDateTime DateTime? 初始显示的日期时间,默认为 DateTime.now()
filterType FilterType? modefilterDate 时,指定筛选类型
startTime DateTime? 可选的最小开始时间,用于限制选择范围
showLaterTime bool? 是否允许选择未来的时间,默认为 true
yearsBack int? filterDate 模式下回溯的年数,默认 3
minYear int? 可选的最小年份,用于限制年份选择范围。未提供时使用默认逻辑
maxYear int? 可选的最大年份,用于限制年份选择范围。未提供时使用默认逻辑(showLaterTimetrue 时默认为当前年份+10年,否则为当前年份)
onChange Function(dynamic)? 选择值变化时的实时回调
onCancel Function()? 点击"取消"或遮罩时的回调
weekItemBuilder WeekItemBuilder? week子组件渲染函数
title String? 自定义标题

🚀 未来规划

  • 支持国际化 (i18n),如显示 "Year", "Month" 等。
  • 支持自定义主题颜色。
  • 增加更多的日期格式化选项。

📜 开源协议

该项目基于 MIT 协议开源。

Libraries

omni_date_picker