Flutter for OpenHarmony高级闹钟App实战:日历视图实现
DeepWake应用中的日历视图功能通过table_calendar库实现,以直观展示重复闹钟安排。该功能包含以下核心特点:1) 支持月/周视图切换和日期选择;2) 可标记有闹钟的日期;3) 高度可定制的样式设置。实现时使用状态变量管理当前聚焦和选中的日期,通过控制器加载闹钟数据,并采用上下分割布局:上半部分显示日历,下半部分展示选定日期的闹钟列表。当无闹钟时显示提示信息,整体界面简洁直观,解决了
日历视图是DeepWake的一个很实用的功能。想象一下,你设置了很多重复闹钟,有的周一到周五响,有的只在周末响,有的每天都响。如果用列表查看,很难一眼看出哪天有哪些闹钟。日历视图就是为了解决这个问题——用日历的形式直观地展示闹钟安排。
做这个功能的时候,我选择了table_calendar这个库。它功能强大,可定制性强,而且文档完善。通过这个库,我们可以快速实现一个漂亮的日历视图。
table_calendar库介绍
在开始编码之前,先了解一下table_calendar的基本用法。
核心功能:
- 显示月视图、周视图
- 支持日期选择
- 支持事件标记
- 高度可定制的样式
基本概念:
focusedDay:当前聚焦的日期,决定显示哪个月selectedDay:用户选中的日期eventLoader:加载每个日期的事件
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:table_calendar/table_calendar.dart';
import '../../controllers/alarm_controller.dart';
import '../../models/alarm.dart';
导入依赖:除了常用的Flutter和GetX,还导入了table_calendar和项目的控制器、模型。
table_calendar是第三方库,需要在pubspec.yaml中添加依赖。
class AlarmCalendarPage extends StatefulWidget {
const AlarmCalendarPage({super.key});
State<AlarmCalendarPage> createState() => _AlarmCalendarPageState();
}
class _AlarmCalendarPageState extends State<AlarmCalendarPage> {
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;
Widget build(BuildContext context) {
final controller = Get.find<AlarmController>();
状态变量:
_focusedDay当前聚焦的日期,初始化为今天_selectedDay用户选中的日期,初始为null
获取控制器:用Get.find获取AlarmController,用于加载闹钟数据。
日历组件配置
TableCalendar有很多配置选项,需要仔细设置。
return Scaffold(
appBar: AppBar(
title: const Text('日历视图'),
actions: [
IconButton(
icon: const Icon(Icons.today),
onPressed: () {
setState(() {
_focusedDay = DateTime.now();
_selectedDay = DateTime.now();
});
},
),
],
),
AppBar配置:
- 标题显示"日历视图"
- 右侧添加"今天"按钮,点击后跳转到今天
这个按钮很实用,当用户浏览其他月份时,可以快速回到今天。
body: Column(
children: [
TableCalendar(
firstDay: DateTime.utc(2020, 1, 1),
lastDay: DateTime.utc(2030, 12, 31),
focusedDay: _focusedDay,
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
},
日期范围:
firstDay最早日期,设置为2020年1月1日lastDay最晚日期,设置为2030年12月31日- 10年的范围足够使用
日期选择:
selectedDayPredicate判断某天是否被选中onDaySelected处理日期选择事件- 更新
_selectedDay和_focusedDay
isSameDay是table_calendar提供的工具函数,比较两个日期是否是同一天。
eventLoader: (day) => controller.getAlarmsForDate(day),
calendarStyle: CalendarStyle(
markerDecoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
todayDecoration: BoxDecoration(
color: Colors.orange.withOpacity(0.5),
shape: BoxShape.circle,
),
selectedDecoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
事件加载:
eventLoader为每个日期加载事件(闹钟)- 返回的列表长度决定标记的数量
样式配置:
markerDecoration事件标记的样式,蓝色圆点todayDecoration今天的样式,半透明橙色圆圈selectedDecoration选中日期的样式,蓝色圆圈
这些样式让日历更直观,用户一眼就能看出哪天有闹钟、哪天是今天、哪天被选中。
weekendTextStyle: TextStyle(color: Colors.red),
outsideDaysVisible: false,
),
headerStyle: HeaderStyle(
formatButtonVisible: false,
titleCentered: true,
titleTextStyle: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
),
更多样式:
weekendTextStyle周末日期用红色显示outsideDaysVisible隐藏不属于当前月的日期formatButtonVisible隐藏格式切换按钮titleCentered标题居中titleTextStyle标题字体样式
这些配置让日历更简洁美观。
const Divider(),
Expanded(
child: _selectedDay == null
? Center(
child: Text(
'选择日期查看闹钟',
style: TextStyle(fontSize: 16.sp),
),
)
: _buildAlarmList(controller),
),
],
),
);
}
下半部分布局:
- 分隔线分隔日历和列表
Expanded让列表占据剩余空间- 未选择日期时显示提示
- 选择日期后显示该日期的闹钟列表
这种上下分割的布局很常见,上面是日历,下面是详情。
闹钟列表显示
选中日期后,下半部分显示该日期的闹钟列表。
Widget _buildAlarmList(AlarmController controller) {
return Obx(() {
final alarms = controller.getAlarmsForDate(_selectedDay!);
if (alarms.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.alarm_off,
size: 64.sp,
color: Colors.grey,
),
SizedBox(height: 16.h),
Text(
'该日期没有闹钟',
style: TextStyle(
fontSize: 16.sp,
color: Colors.grey,
),
),
],
),
);
}
空状态处理:
- 用
Obx包裹,响应闹钟数据变化 - 获取选中日期的闹钟列表
- 如果列表为空,显示友好的空状态
空状态包含图标和文字,让用户知道不是出错了,只是没有数据。
return ListView.builder(
padding: EdgeInsets.all(16.w),
itemCount: alarms.length,
itemBuilder: (context, index) {
final alarm = alarms[index];
return Card(
margin: EdgeInsets.only(bottom: 12.h),
child: ListTile(
leading: Icon(Icons.alarm, size: 32.sp),
title: Text(
'${alarm.time.hour.toString().padLeft(2, '0')}:${alarm.time.minute.toString().padLeft(2, '0')}',
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
),
),
列表构建:
- 用
ListView.builder构建列表 - 每个闹钟用Card包裹
- ListTile显示闹钟信息
时间格式化:用padLeft(2, '0')确保两位数显示,比如"09:05"而不是"9:5"。
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(alarm.label.isEmpty ? '闹钟' : alarm.label),
SizedBox(height: 4.h),
Text(
_getRepeatText(alarm),
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey,
),
),
],
),
trailing: Switch(
value: alarm.enabled,
onChanged: (_) => controller.toggleAlarm(alarm.id),
),
),
);
},
);
});
}
副标题显示:
- 闹钟标签
- 重复规则
开关控制:用Switch让用户可以直接在日历视图中启用或禁用闹钟,无需进入详情页面。
重复规则显示
闹钟的重复规则需要友好地显示给用户。
String _getRepeatText(Alarm alarm) {
if (alarm.repeatDays.isEmpty) {
return '不重复';
}
if (alarm.repeatDays.length == 7) {
return '每天';
}
if (alarm.repeatDays.length == 5 &&
!alarm.repeatDays.contains(0) &&
!alarm.repeatDays.contains(6)) {
return '工作日';
}
if (alarm.repeatDays.length == 2 &&
alarm.repeatDays.contains(0) &&
alarm.repeatDays.contains(6)) {
return '周末';
}
特殊情况处理:
- 空列表:不重复
- 7天:每天
- 周一到周五:工作日
- 周六周日:周末
这些是常见的重复模式,用简短的文字表示。
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
final days = alarm.repeatDays.map((day) => weekDays[day]).toList();
return days.join('、');
}
一般情况处理:
- 将数字转换为星期文字
- 用顿号连接
比如[1, 3, 5]显示为"周一、周三、周五"。
闹钟数据加载
日历需要知道每个日期有哪些闹钟。
// 在AlarmController中
List<Alarm> getAlarmsForDate(DateTime date) {
return alarms.where((alarm) {
if (alarm.repeatDays.isEmpty) {
return false;
}
final weekday = date.weekday % 7;
return alarm.repeatDays.contains(weekday);
}).toList();
}
过滤逻辑:
- 只返回有重复规则的闹钟
- 检查日期的星期几是否在重复规则中
星期几的转换:date.weekday返回1-7(周一到周日),用% 7转换为0-6(周日到周六),与repeatDays的格式一致。
// 优化版本:缓存结果
class AlarmController extends GetxController {
final RxMap<String, List<Alarm>> _dateAlarmsCache = <String, List<Alarm>>{}.obs;
List<Alarm> getAlarmsForDate(DateTime date) {
final key = '${date.year}-${date.month}-${date.day}';
if (_dateAlarmsCache.containsKey(key)) {
return _dateAlarmsCache[key]!;
}
final result = alarms.where((alarm) {
if (alarm.repeatDays.isEmpty) {
return false;
}
final weekday = date.weekday % 7;
return alarm.repeatDays.contains(weekday);
}).toList();
_dateAlarmsCache[key] = result;
return result;
}
缓存优化:
- 用Map缓存每个日期的闹钟列表
- 避免重复计算
- 闹钟数据变化时清空缓存
这种优化在日历滚动时很有用,避免频繁计算。
Future<void> createAlarm(Alarm alarm) async {
alarms.add(alarm);
_dateAlarmsCache.clear();
await saveAlarms();
}
Future<void> updateAlarm(Alarm alarm) async {
final index = alarms.indexWhere((a) => a.id == alarm.id);
if (index != -1) {
alarms[index] = alarm;
_dateAlarmsCache.clear();
await saveAlarms();
}
}
}
缓存失效:闹钟数据变化时清空缓存,确保数据一致性。
日历样式定制
table_calendar提供了丰富的样式定制选项。
日期文字样式:
CalendarStyle(
defaultTextStyle: TextStyle(fontSize: 14.sp),
weekendTextStyle: TextStyle(fontSize: 14.sp, color: Colors.red),
selectedTextStyle: TextStyle(fontSize: 14.sp, color: Colors.white),
todayTextStyle: TextStyle(fontSize: 14.sp, color: Colors.white),
outsideTextStyle: TextStyle(fontSize: 14.sp, color: Colors.grey),
)
不同状态的文字样式:
defaultTextStyle普通日期weekendTextStyle周末日期selectedTextStyle选中日期todayTextStyle今天outsideTextStyle不属于当前月的日期
日期装饰样式:
CalendarStyle(
defaultDecoration: BoxDecoration(
shape: BoxShape.circle,
),
selectedDecoration: BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
todayDecoration: BoxDecoration(
color: Colors.orange.withOpacity(0.5),
shape: BoxShape.circle,
),
weekendDecoration: BoxDecoration(
shape: BoxShape.circle,
),
)
装饰样式:
- 都用圆形
- 选中日期蓝色背景
- 今天半透明橙色背景
- 普通日期和周末无背景
标记样式:
CalendarStyle(
markerDecoration: BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
markerSize: 6.w,
markerMargin: EdgeInsets.symmetric(horizontal: 1.w),
markersMaxCount: 3,
)
标记配置:
- 蓝色圆点
- 大小6个逻辑像素
- 标记之间间隔1个逻辑像素
- 最多显示3个标记
如果某天有多个闹钟,会显示多个圆点,但最多3个。
头部样式定制
日历头部显示月份和导航按钮。
HeaderStyle(
formatButtonVisible: false,
titleCentered: true,
leftChevronIcon: Icon(Icons.chevron_left, size: 24.sp),
rightChevronIcon: Icon(Icons.chevron_right, size: 24.sp),
titleTextStyle: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
headerPadding: EdgeInsets.symmetric(vertical: 8.h),
headerMargin: EdgeInsets.only(bottom: 8.h),
)
头部配置:
- 隐藏格式切换按钮
- 标题居中
- 自定义左右箭头图标
- 标题字体样式
- 内边距和外边距
这些配置让头部更简洁美观。
手势交互
table_calendar支持多种手势交互。
滑动切换月份:
TableCalendar(
// ... 其他配置
onPageChanged: (focusedDay) {
setState(() {
_focusedDay = focusedDay;
});
},
)
页面切换回调:用户滑动切换月份时,更新_focusedDay。
长按日期:
TableCalendar(
// ... 其他配置
onDayLongPressed: (selectedDay, focusedDay) {
Get.to(() => AlarmEditorPage(
initialDate: selectedDay,
));
},
)
长按创建闹钟:长按某个日期,跳转到闹钟编辑页面,并预填充该日期。这是个很方便的快捷操作。
双击日期:
TableCalendar(
// ... 其他配置
onDaySelected: (selectedDay, focusedDay) {
if (_selectedDay != null && isSameDay(_selectedDay, selectedDay)) {
// 双击同一天,显示详情
_showDayDetails(selectedDay);
} else {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
}
},
)
双击检测:如果点击的是已选中的日期,认为是双击,显示详情对话框。
性能优化
日历视图涉及大量日期计算,需要注意性能。
事件加载优化:
// 不好的做法:每次都遍历所有闹钟
List<Alarm> getAlarmsForDate(DateTime date) {
return alarms.where((alarm) {
// 复杂的过滤逻辑
}).toList();
}
// 好的做法:缓存结果
final _cache = <String, List<Alarm>>{};
List<Alarm> getAlarmsForDate(DateTime date) {
final key = _dateKey(date);
return _cache.putIfAbsent(key, () {
return alarms.where((alarm) {
// 复杂的过滤逻辑
}).toList();
});
}
缓存策略:
- 用Map缓存计算结果
- 避免重复计算
- 数据变化时清空缓存
Widget复用:
// 不好的做法:每次都创建新Widget
Widget _buildAlarmCard(Alarm alarm) {
return Card(
child: ListTile(
// ...
),
);
}
// 好的做法:用const构造函数
Widget _buildAlarmCard(Alarm alarm) {
return Card(
child: ListTile(
leading: const Icon(Icons.alarm),
// ...
),
);
}
const优化:能用const的地方就用const,Flutter会缓存const Widget,避免重复创建。
列表优化:
ListView.builder(
// 使用builder而不是直接传children
itemCount: alarms.length,
itemBuilder: (context, index) {
return _buildAlarmCard(alarms[index]);
},
)
builder的优势:只构建可见的Widget,不可见的不构建。这在列表很长时能显著提升性能。
用户体验优化
除了基本功能,还有很多细节提升体验。
今天按钮:AppBar右侧的今天按钮,让用户可以快速回到今天。这在浏览其他月份时很有用。
空状态提示:未选择日期或选中日期没有闹钟时,显示友好的提示。避免空白页面让用户困惑。
即时反馈:点击日期立即显示该日期的闹钟,不需要额外操作。
开关控制:可以直接在列表中启用或禁用闹钟,不需要进入详情页面。
长按快捷操作:长按日期可以快速创建闹钟,提升效率。
可访问性
日历视图也要考虑可访问性。
语义标签:
Semantics(
label: '日历',
child: TableCalendar(
// ...
),
)
为日历添加语义标签,让屏幕阅读器能正确读出。
触摸目标大小:
CalendarStyle(
cellMargin: EdgeInsets.all(4.w),
cellPadding: EdgeInsets.all(0),
)
确保日期单元格足够大,至少48x48逻辑像素,方便点击。
颜色对比度:选中日期和今天的颜色要与背景有足够对比度,确保可见性。
总结
日历视图通过直观的日历形式展示闹钟安排,让用户一眼就能看出哪天有哪些闹钟。通过table_calendar库,我们可以快速实现一个功能完善、样式美观的日历视图。
技术要点:
- 用table_calendar显示日历
- 用eventLoader加载每个日期的事件
- 用CalendarStyle定制样式
- 用缓存优化性能
- 用Obx响应数据变化
设计要点:
- 上下分割的布局
- 清晰的日期标记
- 友好的空状态
- 便捷的快捷操作
开发这个功能让我深刻体会到,选择合适的第三方库能大大提升开发效率。table_calendar提供了丰富的功能和灵活的定制选项,让我们可以专注于业务逻辑,而不是重复造轮子。
当然,使用第三方库也要注意版本兼容性、性能影响、许可证等问题。在选择库之前,要仔细评估是否适合项目需求。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)