Omni Calendar View
OmniCalendarView 是一个为 Flutter 设计的功能强大、高度可定制且性能卓越的日历视图组件。它无缝集成了单日选择、日期范围选择、农历和节气显示,并通过一个直观的控制器(Controller)模式,让状态管理和外部交互变得前所未有的简单。
✨ 预览
核心特性
- 🚀 状态与视图分离: 采用 ChangeNotifier 控制器模式,UI (OmniCalendarView) 负责渲染,逻辑和状态 (OmniCalendarController) 负责管理,代码结构清晰,易于维护和扩展。
- 📅 灵活的日期选择:
- 单选: 轻触任意日期即可完成选择。
- 范围选择: 在已选中的日期上长按,即可开启范围选择模式,拖动到另一日期即可创建范围。再次长按范围端点可取消范围选择。
- 🏮 完整的农历支持: (仅限中文环境)
- 显示农历日期、中国传统节日(如春节、中秋节)、以及二十四节气。
- 支持一键切换公历/农历混合显示,切换过程带有平滑的动画效果。
- 🌐 国际化 (i18n): 内置中英双语支持,并且可以轻松扩展到其他语言。
- 🎨 高度可定制: 通过丰富的参数和回调函数,可以自定义日历的行为和外观。
- 🔄 流畅的交互体验:
- 使用 PageView 实现月份间的平滑滑动。
- 当选中非当前月份的日期时,日历会自动翻页到目标月份。
- UI 元素(如高度变化)均带有平滑的 AnimatedContainer 动画。
- ⚡️ 性能优化:
- 月份视图使用 PageView.builder 构建,仅渲染可见的页面。
- UI 更新由 AnimatedBuilder 驱动,实现局部刷新,避免不必要的 setState 调用,确保应用性能。
设计思路
OmniCalendarView 的核心设计哲学是 “关注点分离” (Separation of Concerns)。
状态中心 (OmniCalendarController)
- 我没有将所有状态(如当前显示的月份、选中的日期/范围、是否显示农历等)都放在 StatefulWidget 的 State 对象中。'
- 相反,我创建了一个独立的 OmniCalendarController,它继承自 ChangeNotifier。
- 这个控制器是日历的“大脑”和“唯一数据源”。任何状态的变更都通过调用控制器的方法来完成(例如 selectDate, jumpToDate)。
响应式UI (OmniCalendarView & AnimatedBuilder)
- 日历视图本身 (OmniCalendarView 及其子组件) 是“无状态”的。
- 它通过 AnimatedBuilder 来监听控制器的变化。
- 当控制器调用 notifyListeners() 时,只有监听它的 AnimatedBuilder 会被重建,从而以最高效的方式更新UI。
- 这种模式避免了在顶层组件上调用 setState() 导致的整个组件树的非必要重建。
组合优于继承
- 整个日历是由一系列小而专注的组件组合而成:
- CalendarHeader: 显示年月、农历开关和当前选择。
- WeekdayRow: 显示星期标题。
- CalendarGrid: 负责渲染单个月份的网格。
- _buildDayCell: 渲染单个日期单元格,通过一个巧妙的 Stack 结构,将范围背景、选中背景和文本内容分层绘制,实现了复杂的视觉效果。
- 直观的交互设计:
- 我认为长按是一个非常适合从“单选”模式切换到“范围选择”模式的交互手势,因为它不会与常规的点击事件冲突。
- handleLongPress 方法中的逻辑清晰地处理了范围的创建、修改和取消,为用户提供了流畅的体验。
开始使用
安装
在你的 pubspec.yaml 文件中添加依赖:
dependencies:
omni_calendar_view: ^1.0.0 # 请使用最新版本
然后运行 flutter pub get。
基本用法
import 'package:flutter/material.dart';
import 'package:omni_calendar_view/omni_calendar_view.dart';
class MyCalendarPage extends StatefulWidget {
const MyCalendarPage({super.key});
@override
State<MyCalendarPage> createState() => _MyCalendarPageState();
}
class _MyCalendarPageState extends State<MyCalendarPage> {
// 1. 创建控制器
late final OmniCalendarController _controller;
@override
void initState() {
super.initState();
// 可以传入 initialDate 来指定初始显示的月份和默认选中的日期
_controller = OmniCalendarController(initialDate: DateTime.now());
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Omni Calendar')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: OmniCalendarView(
// 2. 将控制器传递给视图
controller: _controller,
// 3. (可选) 设置初始属性和监听回调
showLunar: true, // 默认显示农历
locale: const Locale('zh', 'CN'), // 设置为中文
onDateSelected: (date) {
print('单选日期: $date');
},
onDateRangeSelected: (dateRange) {
print('范围选择: ${dateRange.start} - ${dateRange.end}');
},
onMonthChanged: (newMonth) {
print('月份改变: $newMonth');
},
),
),
);
}
}
外部控制
你可以通过 OmniCalendarController 从外部控制日历的状态。
// 跳转到指定日期所在的月份
_controller.jumpToDate(DateTime(2025, 10, 1));
// 手动选择一个日期
_controller.selectDate(DateTime(2025, 7, 26));
// 切换农历显示 (这也会被 OmniCalendarView 的 showLunar 属性覆盖)
_controller.toggleLunar(false);
API 概览
OmniCalendarView
| 属性 | 类型 | 描述 |
|---|---|---|
| controller | OmniCalendarController | 必需. 控制器,用于状态管理。 |
| showSurroundingDays | bool | 是否显示非本月的日期。默认为 true。 |
| showLunar | bool | 是否显示农历。默认为 false。 |
| locale | Locale | 语言环境,支持 zh 和 en。默认为 zh。 |
| onHeaderTap | VoidCallback? | 顶部年月被点击时的回调。 |
| onDateSelected | ValueChanged | 日期被单击时的回调。 |
| onDateRangeSelected | ValueChanged | 日期范围被选择时的回调。 |
| onMonthChanged | ValueChanged | 月份因滑动而改变时的回调。 |
OmniCalendarController
| 属性/方法 | 类型 | 描述 |
|---|---|---|
| displayedDate | DateTime | (Getter) 当前显示的月份。 |
| selectedDate | DateTime? | (Getter) 当前选中的单个日期。 |
| selectedDateRange | DateTimeRange? | (Getter) 当前选中的日期范围。 |
| showLunar | bool | (Getter) 当前是否显示农历。 |
| selectionType | SelectionType | (Getter) 当前的选择模式 (single 或 range)。 |
| jumpToDate(DateTime) | void | 跳转到指定日期所在的月份视图。 |
| selectDate(DateTime) | void | 编程方式选择一个日期。 |
| toggleLunar(bool) | void | 切换农历显示。 |
贡献
欢迎各种形式的贡献,包括提交问题 (Issues)、请求新功能 (Feature Requests) 和代码合并请求 (Pull Requests)。
许可证 本项目基于 MIT License 开源。