Flutter 框架跨平台鸿蒙开发 - 打造一款精美的手机日历应用
本文从零开始,实现了一款功能完善的Flutter日历应用。日期计算:月份天数、星期计算、闰年判断网格布局:GridView实现7×6日历网格状态管理:日期选择、月份切换、日程数据交互设计:滚轮选择器、添加对话框、颜色标签视觉设计:今日高亮、周末标红、节日显示日历看似简单,实则涉及不少细节。希望这篇文章能帮你理解日历应用的开发思路,也欢迎在此基础上继续扩展更多功能!📅 完整源码已上传,欢迎Star
📅 Flutter + HarmonyOS 实战:打造一款精美的手机日历应用
运行效果图

📋 文章导读
| 章节 | 内容概要 | 预计阅读 |
|---|---|---|
| 一 | 日历应用需求分析与设计 | 3分钟 |
| 二 | 日期计算核心算法 | 8分钟 |
| 三 | 日历网格布局实现 | 10分钟 |
| 四 | 日程管理功能开发 | 8分钟 |
| 五 | 交互体验优化 | 5分钟 |
| 六 | 完整源码与总结 | 3分钟 |
💡 写在前面:日历是手机上使用频率最高的工具类应用之一。一个好的日历应用不仅要准确显示日期,还要支持日程管理、节日提醒等功能。本文将带你用Flutter从零实现一款功能完善、界面精美的日历应用,深入讲解日期计算、网格布局、状态管理等核心技术点。
一、需求分析与设计
1.1 功能需求
一款实用的日历应用需要具备以下核心功能:
1.2 界面设计
整体采用卡片式设计,分为三个主要区域:
| 区域 | 功能 | 设计要点 |
|---|---|---|
| 顶部导航 | 月份切换、年月选择 | 蓝色主题,圆角底部 |
| 日历网格 | 日期展示、选择 | 7列网格,今日/选中高亮 |
| 日程列表 | 当日日程展示 | 卡片样式,颜色标签 |
1.3 配色方案
| 元素 | 颜色 | 用途 |
|---|---|---|
| 主题色 | blue.shade600 |
顶部背景、选中日期 |
| 今日边框 | blue.shade400 |
今日日期标识 |
| 周末文字 | red.shade400 |
周六周日 |
| 节日文字 | orange.shade600 |
节日名称 |
| 非当月日期 | grey.shade400 |
上/下月日期 |
二、日期计算核心算法
2.1 日历的数学基础
要正确渲染日历,我们需要解决两个核心问题:
- 某月有多少天?
- 某月第一天是星期几?
计算某月天数
/// 获取某月的天数
int _daysInMonth(DateTime date) {
// 下个月的第0天 = 当月的最后一天
return DateTime(date.year, date.month + 1, 0).day;
}
这里用了一个小技巧:DateTime(year, month, 0) 会返回上个月的最后一天。所以 month + 1 的第0天就是当月的最后一天。
各月天数速查表
| 月份 | 天数 | 备注 |
|---|---|---|
| 1月 | 31 | |
| 2月 | 28/29 | 闰年29天 |
| 3月 | 31 | |
| 4月 | 30 | |
| 5月 | 31 | |
| 6月 | 30 | |
| 7月 | 31 | |
| 8月 | 31 | |
| 9月 | 30 | |
| 10月 | 31 | |
| 11月 | 30 | |
| 12月 | 31 |
闰年判断公式
isLeapYear={trueif yearmod 400=0falseif yearmod 100=0trueif yearmod 4=0falseotherwise \text{isLeapYear} = \begin{cases} \text{true} & \text{if } year \mod 400 = 0 \\ \text{false} & \text{if } year \mod 100 = 0 \\ \text{true} & \text{if } year \mod 4 = 0 \\ \text{false} & \text{otherwise} \end{cases} isLeapYear=⎩ ⎨ ⎧truefalsetruefalseif yearmod400=0if yearmod100=0if yearmod4=0otherwise
2.2 计算月份第一天的星期
/// 获取某月第一天是星期几(0=周日, 1=周一, ..., 6=周六)
int _firstDayOfMonth(DateTime date) {
return DateTime(date.year, date.month, 1).weekday % 7;
}
⚠️ 注意:Dart的
weekday返回 1-7(周一到周日),我们需要转换为 0-6(周日到周六)以匹配中国习惯的日历布局。
2.3 日历网格计算流程
2.4 完整的日期计算代码
Widget _buildCalendarGrid() {
final daysInMonth = _daysInMonth(_currentMonth);
final firstDay = _firstDayOfMonth(_currentMonth);
final prevMonth = DateTime(_currentMonth.year, _currentMonth.month - 1);
final daysInPrevMonth = _daysInMonth(prevMonth);
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 7, // 一周7天
),
itemCount: 42, // 6行 × 7列 = 42个格子
itemBuilder: (context, index) {
DateTime date;
bool isCurrentMonth = true;
if (index < firstDay) {
// 上个月的日期
final day = daysInPrevMonth - firstDay + index + 1;
date = DateTime(prevMonth.year, prevMonth.month, day);
isCurrentMonth = false;
} else if (index >= firstDay + daysInMonth) {
// 下个月的日期
final day = index - firstDay - daysInMonth + 1;
date = DateTime(_currentMonth.year, _currentMonth.month + 1, day);
isCurrentMonth = false;
} else {
// 当前月的日期
final day = index - firstDay + 1;
date = DateTime(_currentMonth.year, _currentMonth.month, day);
}
return _buildDayCell(date, isCurrentMonth);
},
);
}
三、日历网格布局
3.1 整体布局结构
3.2 星期头部实现
Widget _buildWeekHeader() {
final weekDays = ['日', '一', '二', '三', '四', '五', '六'];
return Row(
children: weekDays.asMap().entries.map((entry) {
final isWeekend = entry.key == 0 || entry.key == 6;
return Expanded(
child: Center(
child: Text(
entry.value,
style: TextStyle(
color: isWeekend ? Colors.red.shade400 : Colors.grey.shade600,
fontWeight: FontWeight.w600,
),
),
),
);
}).toList(),
);
}
3.3 日期单元格设计
每个日期单元格需要处理多种状态:
| 状态 | 样式 | 优先级 |
|---|---|---|
| 选中日期 | 蓝色填充背景 | 1 |
| 今天 | 蓝色边框 | 2 |
| 有日程 | 底部小圆点 | 3 |
| 节日 | 橙色小字 | 4 |
| 周末 | 红色文字 | 5 |
| 非当月 | 灰色文字 | 6 |
Widget _buildDayCell(DateTime date, bool isCurrentMonth) {
final isToday = _isToday(date);
final isSelected = _isSelected(date);
final isWeekend = _isWeekend(date);
final festival = _getFestival(date);
final hasEvents = _events[_dateKey(date)]?.isNotEmpty ?? false;
return GestureDetector(
onTap: () => setState(() => _selectedDate = date),
child: Container(
margin: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: isSelected
? Colors.blue.shade600
: isToday ? Colors.blue.shade50 : null,
borderRadius: BorderRadius.circular(12),
border: isToday && !isSelected
? Border.all(color: Colors.blue.shade400, width: 2)
: null,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 日期数字
Text(
'${date.day}',
style: TextStyle(
color: _getDayColor(isSelected, isCurrentMonth, isWeekend),
fontWeight: isToday || isSelected ? FontWeight.bold : null,
),
),
// 节日名称
if (festival != null)
Text(festival, style: TextStyle(fontSize: 9, color: Colors.orange)),
// 日程指示点
if (hasEvents && festival == null)
Container(
width: 6, height: 6,
decoration: BoxDecoration(
color: isSelected ? Colors.white : Colors.blue,
shape: BoxShape.circle,
),
),
],
),
),
);
}
四、日程管理功能
4.1 数据模型设计
/// 事件模型
class Event {
final String title; // 标题
final String time; // 时间
final Color color; // 颜色标签
Event(this.title, this.time, this.color);
}
使用 Map<String, List<Event>> 存储日程数据,key 为日期字符串:
final Map<String, List<Event>> _events = {};
String _dateKey(DateTime date) => '${date.year}-${date.month}-${date.day}';
void _addEvent(DateTime date, Event event) {
final key = _dateKey(date);
_events[key] = [...(_events[key] ?? []), event];
}
4.2 日程列表展示
Widget _buildEventItem(Event event) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 4),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: event.color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border(
left: BorderSide(color: event.color, width: 4), // 左侧颜色条
),
),
child: Row(
children: [
// 时间标签
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: event.color.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
),
child: Text(event.time, style: TextStyle(color: event.color)),
),
const SizedBox(width: 12),
// 标题
Expanded(child: Text(event.title)),
Icon(Icons.chevron_right, color: Colors.grey),
],
),
);
}
4.3 添加日程对话框
4.4 颜色标签选择器
Row(
children: [
const Text('颜色标签:'),
...[Colors.blue, Colors.orange, Colors.green, Colors.pink, Colors.purple]
.map((color) {
return GestureDetector(
onTap: () => setDialogState(() => selectedColor = color),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
width: 32,
height: 32,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: selectedColor == color
? Border.all(color: Colors.black, width: 3)
: null,
),
),
);
}),
],
)
五、交互体验优化
5.1 月份切换动画
void _changeMonth(int delta) {
setState(() {
_currentMonth = DateTime(
_currentMonth.year,
_currentMonth.month + delta,
);
});
}
5.2 年月选择器
使用 ListWheelScrollView 实现 iOS 风格的滚轮选择器:
void _showYearMonthPicker() {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) {
return Container(
height: 300,
child: Row(
children: [
// 年份滚轮
Expanded(
child: ListWheelScrollView.useDelegate(
itemExtent: 40,
perspective: 0.005,
onSelectedItemChanged: (index) {
setState(() {
_currentMonth = DateTime(2020 + index, _currentMonth.month);
});
},
childDelegate: ListWheelChildBuilderDelegate(
childCount: 20,
builder: (context, index) => Center(
child: Text('${2020 + index}年'),
),
),
),
),
// 月份滚轮
Expanded(
child: ListWheelScrollView.useDelegate(
itemExtent: 40,
perspective: 0.005,
onSelectedItemChanged: (index) {
setState(() {
_currentMonth = DateTime(_currentMonth.year, index + 1);
});
},
childDelegate: ListWheelChildBuilderDelegate(
childCount: 12,
builder: (context, index) => Center(
child: Text('${index + 1}月'),
),
),
),
),
],
),
);
},
);
}
5.3 节日数据配置
// 公历节日
final Map<String, String> _solarFestivals = {
'1-1': '元旦',
'2-14': '情人节',
'3-8': '妇女节',
'4-1': '愚人节',
'5-1': '劳动节',
'5-4': '青年节',
'6-1': '儿童节',
'7-1': '建党节',
'8-1': '建军节',
'9-10': '教师节',
'10-1': '国庆节',
'12-25': '圣诞节',
};
String? _getFestival(DateTime date) {
final key = '${date.month}-${date.day}';
return _solarFestivals[key];
}
六、完整源码与运行
6.1 项目结构
flutter_calendar/
├── lib/
│ └── main.dart # 日历应用主代码(约450行)
├── ohos/ # 鸿蒙平台配置
├── pubspec.yaml # 依赖配置
└── README.md # 项目说明
6.2 运行命令
# 获取依赖
flutter pub get
# 运行到模拟器/设备
flutter run
# 运行到鸿蒙设备
flutter run -d ohos
6.3 功能清单
| 功能 | 状态 | 说明 |
|---|---|---|
| 月视图展示 | ✅ | 完整的日历网格 |
| 月份切换 | ✅ | 左右箭头切换 |
| 年月选择器 | ✅ | 滚轮式选择 |
| 今日高亮 | ✅ | 边框+背景 |
| 日期选择 | ✅ | 点击选中 |
| 周末标红 | ✅ | 周六周日红色 |
| 节日显示 | ✅ | 公历节日 |
| 日程添加 | ✅ | 标题+时间+颜色 |
| 日程列表 | ✅ | 卡片式展示 |
| 快速回今天 | ✅ | 顶部按钮 |
七、扩展方向
7.1 功能扩展建议
7.2 农历转换思路
农历转换是一个复杂的问题,可以使用现成的库或查表法:
// 推荐使用 lunar 包
// pubspec.yaml: lunar: ^1.3.0
import 'package:lunar/lunar.dart';
String getLunarDate(DateTime date) {
final lunar = Lunar.fromDate(date);
return '${lunar.getMonthInChinese()}月${lunar.getDayInChinese()}';
}
7.3 本地通知实现
// 使用 flutter_local_notifications 包
// 在添加日程时设置提醒
Future<void> scheduleNotification(Event event, DateTime date) async {
await flutterLocalNotificationsPlugin.zonedSchedule(
0,
'日程提醒',
event.title,
tz.TZDateTime.from(date, tz.local),
const NotificationDetails(
android: AndroidNotificationDetails('calendar', '日历提醒'),
),
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
);
}
八、常见问题
Q1: 为什么日历要显示42个格子?日历网格固定为6行7列=42个格子,这是因为:
- 一个月最多31天
- 如果1号是周六,那么需要6行才能显示完整
- 固定行数可以避免月份切换时布局跳动
Dart的DateTime会自动处理跨年情况:
// 2026年1月 - 1个月 = 2025年12月
DateTime(2026, 0, 1) // 自动变成 2025-12-01
// 2025年12月 + 1个月 = 2026年1月
DateTime(2025, 13, 1) // 自动变成 2026-01-01
Q3: 如何实现日程数据持久化?
可以使用以下方案:
- SharedPreferences:适合简单数据
- SQLite (sqflite):适合复杂查询
- Hive:高性能NoSQL数据库
// 使用 shared_preferences 示例
Future<void> saveEvents() async {
final prefs = await SharedPreferences.getInstance();
final json = jsonEncode(_events.map((k, v) =>
MapEntry(k, v.map((e) => e.toJson()).toList())
));
await prefs.setString('events', json);
}
九、总结
本文从零开始,实现了一款功能完善的Flutter日历应用。核心技术点包括:
- 日期计算:月份天数、星期计算、闰年判断
- 网格布局:GridView实现7×6日历网格
- 状态管理:日期选择、月份切换、日程数据
- 交互设计:滚轮选择器、添加对话框、颜色标签
- 视觉设计:今日高亮、周末标红、节日显示
日历看似简单,实则涉及不少细节。希望这篇文章能帮你理解日历应用的开发思路,也欢迎在此基础上继续扩展更多功能!
📅 完整源码已上传,欢迎Star支持!
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)