flutter_for_openharmony家庭相册app实战+日历Tab实现
本文介绍了如何实现一个家庭日历应用,用于管理家庭活动和日程。主要功能包括:月历视图显示及月份切换、日期点击查看活动、活动日期标记、添加新活动以及快捷入口。页面采用StatefulWidget构建,使用table_calendar库实现日历组件,通过EventProvider管理活动数据。界面布局分为上下两部分,上方是日历视图,下方是活动列表。日历支持月/周视图切换,能够标记有活动的日期,点击日期可

家庭生活中有很多需要记住的日子,孩子的家长会、老人的体检、全家的聚餐。
今天来实现日历Tab,用来管理家庭的各种活动和日程。
功能规划
日历Tab需要这些功能:
- 显示月历视图,可以切换月份
- 点击某一天显示当天的活动
- 有活动的日期要有标记
- 支持添加新活动
- 提供纪念日和待办事项的快捷入口
这个页面需要用到状态,所以用StatefulWidget。日历组件是页面的核心,需要处理日期选择、月份切换、活动标记等交互。活动列表根据选中日期动态更新,用户能快速查看某天的安排。整个页面的设计要简洁直观,让用户能轻松管理家庭日程。
页面状态定义
先定义页面需要的状态变量:
class CalendarTab extends StatefulWidget {
const CalendarTab({super.key});
State<CalendarTab> createState() => _CalendarTabState();
}
class _CalendarTabState extends State<CalendarTab> {
CalendarFormat _calendarFormat = CalendarFormat.month;
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;
_calendarFormat控制日历的显示格式,月视图还是周视图。
_focusedDay是当前聚焦的日期,用于控制日历显示哪个月。
_selectedDay是用户选中的日期,用于显示当天的活动列表。
CalendarFormat是table_calendar库提供的枚举类型,有month、twoWeeks、week三个值。month是月视图,显示整个月的日历;week是周视图,只显示一周;twoWeeks是两周视图,介于两者之间。_focusedDay用DateTime.now()初始化,表示当前日期。_selectedDay用可空类型,因为初始时可能没有选中日期,在initState里会设置为今天。
void initState() {
super.initState();
_selectedDay = _focusedDay;
}
初始化时把选中日期设为今天。
这样用户打开页面就能看到今天的活动。
initState是State的生命周期方法,在组件创建时调用一次。这里把_selectedDay设为_focusedDay,也就是今天。这样用户打开页面时,日历会高亮今天,下方的活动列表会显示今天的活动。这种默认行为符合用户预期,因为大多数情况下用户关心的是今天或最近几天的安排。
页面基础结构
搭建页面框架:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('家庭日历'),
actions: [
IconButton(
icon: const Icon(Icons.cake),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const AnniversaryListScreen()),
),
),
标题叫"家庭日历",突出家庭属性。
右上角放纪念日入口,用蛋糕图标,点击跳转到纪念日列表。
纪念日包括家人生日、结婚纪念日这些重要日子。
AppBar的标题用"家庭日历"而不是简单的"日历",强调这是家庭共享的日程管理工具。actions数组包含两个IconButton,第一个是纪念日入口。蛋糕图标很直观,用户一看就知道是生日或纪念日相关的功能。点击后用Navigator.push跳转到AnniversaryListScreen,这个页面会列出所有的纪念日,用户可以查看、添加、编辑纪念日。
IconButton(
icon: const Icon(Icons.checklist),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const TodoListScreen()),
),
),
],
),
待办事项入口用清单图标。
待办事项是一些需要完成的任务,比如准备家长会材料、预约体检。
第二个IconButton是待办事项入口,用checklist图标,表示清单或任务列表。点击跳转到TodoListScreen,用户可以查看、添加、完成待办事项。待办事项和日历活动是两个不同的概念,活动是有明确时间的事件,待办事项是需要完成的任务,可能没有具体时间。把两者分开管理,但都在日历Tab提供入口,方便用户快速访问。
主体布局
主体分上下两部分,上面是日历,下面是活动列表:
body: Consumer<EventProvider>(
builder: (context, provider, _) {
return Column(
children: [
_buildCalendar(provider),
Expanded(
child: _buildEventList(provider),
),
],
);
},
),
用
Column垂直排列,日历在上,活动列表在下。活动列表用
Expanded占据剩余空间。
Consumer监听EventProvider,活动数据变化时自动刷新。
Consumer是Provider的核心组件,监听EventProvider的变化。当用户添加、删除、修改活动时,Provider会调用notifyListeners,Consumer收到通知后重新执行builder函数,UI就更新了。Column垂直排列日历和活动列表,日历的高度由内容决定,活动列表用Expanded占据剩余空间。这样无论屏幕多高,活动列表都能填满剩余区域,不会留下空白。
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => AddEventScreen(selectedDate: _selectedDay)),
),
child: const Icon(Icons.add),
),
);
}
悬浮按钮用来添加新活动。
把当前选中的日期传给添加页面,这样新活动默认就是这一天的。
FloatingActionButton是Material Design的悬浮操作按钮,通常用于页面的主要操作。这里用来添加新活动,点击后跳转到AddEventScreen。selectedDate参数传入_selectedDay,这样添加页面的日期选择器会默认选中这个日期,用户不需要再手动选择,减少操作步骤。如果_selectedDay是null,AddEventScreen应该默认选择今天。这种智能默认值的设计能显著提升用户体验。
日历组件实现
日历用table_calendar这个库:
Widget _buildCalendar(EventProvider provider) {
return TableCalendar(
firstDay: DateTime.utc(2020, 1, 1),
lastDay: DateTime.utc(2030, 12, 31),
focusedDay: _focusedDay,
calendarFormat: _calendarFormat,
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
eventLoader: (day) => provider.getEventsByDate(day),
firstDay和lastDay设置日历的范围,2020到2030年。
selectedDayPredicate判断某天是否被选中,用于高亮显示。
eventLoader返回某天的活动列表,有活动的日期会显示标记点。
TableCalendar是一个功能强大的日历组件,支持多种视图格式和自定义样式。firstDay和lastDay定义日历的可选范围,这里设为2020到2030年,覆盖了大部分使用场景。focusedDay是当前聚焦的日期,决定日历显示哪个月。selectedDayPredicate是个判断函数,接收一个日期,返回bool表示是否选中。isSameDay是table_calendar提供的工具函数,比较两个日期是否是同一天,忽略时分秒。eventLoader也是个函数,接收日期,返回该日期的活动列表。provider.getEventsByDate查询指定日期的活动,返回List。如果列表不为空,日历会在该日期下方显示标记点,提示用户这天有活动。
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
},
onFormatChanged: (format) {
setState(() {
_calendarFormat = format;
});
},
onDaySelected处理日期点击,更新选中状态。
onFormatChanged处理格式切换,比如从月视图切到周视图。都需要调用
setState来触发界面刷新。
onDaySelected是日期点击回调,接收两个参数:selectedDay是用户点击的日期,focusedDay是日历应该聚焦的日期。大多数情况下两者相同,但在某些边界情况下可能不同,比如点击上个月或下个月的日期。setState更新_selectedDay和_focusedDay,触发UI重建。日历会高亮新选中的日期,下方的活动列表会显示新日期的活动。onFormatChanged是格式切换回调,用户点击日历头部的格式切换按钮时触发。setState更新_calendarFormat,日历会切换到新格式。这两个回调都需要调用setState,因为它们改变了State的状态,需要重新构建UI。
calendarStyle: CalendarStyle(
selectedDecoration: const BoxDecoration(
color: Color(0xFFE91E63),
shape: BoxShape.circle,
),
todayDecoration: BoxDecoration(
color: const Color(0xFFE91E63).withOpacity(0.3),
shape: BoxShape.circle,
),
markerDecoration: const BoxDecoration(
color: Color(0xFFE91E63),
shape: BoxShape.circle,
),
),
选中日期用粉色圆形背景。
今天用浅粉色,和选中状态区分开。
活动标记点也用粉色,保持颜色统一。
calendarStyle配置日历的视觉样式。selectedDecoration是选中日期的装饰,用粉色圆形背景,颜色是0xFFE91E63,这是Material Design的pink色系。shape设为circle,让背景是圆形而不是方形。todayDecoration是今天的装饰,用浅粉色,透明度30%,和选中状态区分开。如果今天被选中,会同时应用两个装饰,视觉上会更突出。markerDecoration是活动标记点的装饰,也用粉色圆形,尺寸比日期小,显示在日期下方。这种统一的颜色方案让界面协调美观,用户能快速识别不同状态的日期。
headerStyle: const HeaderStyle(
formatButtonVisible: true,
titleCentered: true,
),
);
}
头部显示格式切换按钮,标题居中。
用户可以在月视图和周视图之间切换。
headerStyle配置日历头部的样式。formatButtonVisible设为true,显示格式切换按钮,用户可以点击切换月视图和周视图。titleCentered设为true,让月份标题居中显示,视觉上更平衡。头部还包含左右箭头按钮,用于切换月份。这些都是table_calendar内置的功能,不需要额外实现。
活动列表实现
根据选中日期显示活动列表:
Widget _buildEventList(EventProvider provider) {
final events = _selectedDay != null
? provider.getEventsByDate(_selectedDay!)
: <FamilyEvent>[];
if (events.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.event_available,
size: 64.sp,
color: Colors.grey[300],
),
先获取选中日期的活动列表。
如果没有活动,显示空状态提示。
用一个大图标加文字,比空白页面友好。
_buildEventList构建活动列表区域。首先判断_selectedDay是否为null,如果不为null,调用provider.getEventsByDate获取该日期的活动列表;如果为null,返回空列表。events.isEmpty检查是否有活动,如果没有,显示空状态。Center让内容居中显示,Column垂直排列图标和文字。event_available图标表示"有空闲时间",尺寸64像素,颜色用浅灰色。这种空状态设计友好,让用户知道不是出错了,只是这天没有安排活动。
SizedBox(height: 16.h),
Text(
'当天没有活动',
style: TextStyle(
fontSize: 16.sp,
color: Colors.grey,
),
),
],
),
);
}
提示文字用灰色,不抢眼但能看到。
提示文字"当天没有活动"说明当前状态,字号16像素,颜色用灰色。这种中性的提示不会让用户感到焦虑,只是陈述事实。如果想更友好一点,可以加个副标题,比如"点击右下角+号添加活动",引导用户下一步操作。
return ListView.builder(
padding: EdgeInsets.all(16.w),
itemCount: events.length,
itemBuilder: (context, index) {
final event = events[index];
return _buildEventCard(context, event);
},
);
}
有活动时用
ListView.builder展示。每个活动用卡片的形式显示。
ListView.builder是懒加载的列表组件,只构建可见区域的item,性能好。padding给列表四周添加16像素的内边距,避免内容紧贴屏幕边缘。itemCount是活动数量,itemBuilder是构建函数,接收context和index,返回对应的Widget。这里调用_buildEventCard构建活动卡片,传入event对象。
活动卡片设计
每个活动用卡片展示:
Widget _buildEventCard(BuildContext context, FamilyEvent event) {
return Card(
margin: EdgeInsets.only(bottom: 12.h),
child: ListTile(
leading: Container(
width: 48.w,
height: 48.w,
decoration: BoxDecoration(
color: _getColorForEventType(event.type).withOpacity(0.1),
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(
_getIconForEventType(event.type),
color: _getColorForEventType(event.type),
),
),
左边放一个图标容器,根据活动类型显示不同图标和颜色。
学校活动用蓝色学校图标,聚会用橙色庆祝图标。
背景用10%透明度的主色。
Card提供Material Design的卡片效果,margin设为底部12像素,和下一个卡片保持间距。ListTile是列表项的标准组件,leading放左边的图标,title放标题,subtitle放副标题,trailing放右边的内容,onTap处理点击事件。leading用Container包装Icon,给图标加个背景色。背景色用活动类型对应的颜色,透明度10%,图标本身用该颜色。这种设计让不同类型的活动一眼就能区分,视觉上很清晰。
title: Text(
event.title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (event.location != null)
Text(
event.location!,
style: TextStyle(fontSize: 12.sp),
),
Text(
DateFormat('HH:mm').format(event.date),
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey,
),
),
],
),
标题显示活动名称,加粗突出。
副标题显示地点和时间,地点可能为空所以加了判断。
时间用
DateFormat格式化成"HH:mm"的形式。
title显示活动标题,字号16像素,fontWeight设为w500,稍微加粗,让标题更突出。subtitle是个Column,垂直排列地点和时间。地点可能为null,用if判断,只有不为null时才显示。Text的style设置字号12像素,比标题小。时间用DateFormat格式化,'HH:mm’表示24小时制的时分格式,比如"14:30"。颜色用灰色,作为辅助信息。这种层次分明的设计让信息易于阅读,用户能快速抓住重点。
trailing: event.isReminder
? const Icon(Icons.notifications_active, color: Color(0xFFE91E63))
: null,
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => EventDetailScreen(event: event)),
),
),
);
}
如果开启了提醒,右边显示一个铃铛图标。
点击卡片跳转到活动详情页。
trailing是ListTile右边的内容,这里根据isReminder字段决定是否显示铃铛图标。如果开启了提醒,显示notifications_active图标,颜色用粉色;如果没开启,trailing设为null,不显示任何内容。onTap处理点击事件,跳转到EventDetailScreen查看活动详情。详情页可以显示活动的完整信息,包括描述、参与人员、提醒设置等,用户还可以编辑或删除活动。
类型颜色和图标映射
根据活动类型返回对应的颜色:
Color _getColorForEventType(String type) {
switch (type) {
case '学校':
return Colors.blue;
case '聚会':
return Colors.orange;
case '医疗':
return Colors.red;
case '户外':
return Colors.green;
case '探亲':
return Colors.purple;
default:
return Colors.grey;
}
}
不同类型用不同颜色,一眼就能区分。
医疗用红色比较醒目,户外用绿色代表自然。
_getColorForEventType根据活动类型返回对应的颜色。switch语句匹配type字符串,返回相应的颜色。学校活动用蓝色,代表知识和学习;聚会用橙色,代表欢乐和热情;医疗用红色,代表紧急和重要;户外用绿色,代表自然和健康;探亲用紫色,代表亲情和温馨。default返回灰色,处理未知类型。这种颜色映射让活动类型一目了然,用户不需要读文字就能大致判断活动性质。
根据活动类型返回对应的图标:
IconData _getIconForEventType(String type) {
switch (type) {
case '学校':
return Icons.school;
case '聚会':
return Icons.celebration;
case '医疗':
return Icons.local_hospital;
case '户外':
return Icons.park;
case '探亲':
return Icons.home;
default:
return Icons.event;
}
}
图标选择也很直观,学校用学校图标,医疗用医院图标。
默认用通用的日历图标。
_getIconForEventType根据活动类型返回对应的图标。图标选择很直观,学校用school图标,聚会用celebration图标,医疗用local_hospital图标,户外用park图标,探亲用home图标。default返回event图标,这是个通用的日历图标。图标和颜色配合使用,让活动类型的识别度更高。用户看到蓝色学校图标,立即知道这是学校相关的活动;看到红色医院图标,知道这是医疗相关的活动。这种视觉设计能显著提升用户体验,减少认知负担。
小结
日历Tab的核心是TableCalendar组件,配合活动列表展示。
选中日期后显示当天的活动,有活动的日期会有标记点。
不同类型的活动用不同颜色和图标区分,视觉上很清晰。
悬浮按钮提供快速添加活动的入口,默认选中当前日期,减少操作步骤。AppBar提供纪念日和待办事项的快捷入口,方便用户管理不同类型的日程。空状态设计友好,让用户知道当前状态,不会产生困惑。整个页面的交互流畅,功能完整,是家庭相册App的重要组成部分。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
月份切换动画
为月份切换添加平滑动画效果:
PageController _pageController = PageController();
Widget _buildCalendarWithAnimation() {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.1, 0),
end: Offset.zero,
).animate(animation),
child: child,
),
);
},
child: TableCalendar(
key: ValueKey(_focusedDay),
firstDay: DateTime(2020),
lastDay: DateTime(2030),
focusedDay: _focusedDay,
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
onDaySelected: _onDaySelected,
onPageChanged: (focusedDay) {
setState(() => _focusedDay = focusedDay);
},
),
);
}
AnimatedSwitcher组件在月份切换时提供淡入淡出和滑动效果。FadeTransition控制透明度变化,SlideTransition控制位置移动。Tween定义从右侧0.1的偏移量滑动到原位,创造出平滑的切换动画。
这种动画效果让月份切换更加流畅自然,提升了用户体验。300毫秒的动画时长既不会太快让用户感觉突兀,也不会太慢影响操作效率。ValueKey确保每次月份变化时都会触发动画。
活动统计卡片
显示当月活动统计信息:
Widget _buildMonthStatistics() {
final monthEvents = _getMonthEvents(_focusedDay);
final eventsByType = <String, int>{};
for (var event in monthEvents) {
eventsByType[event.type] = (eventsByType[event.type] ?? 0) + 1;
}
return Container(
margin: EdgeInsets.all(16.w),
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF2196F3), Color(0xFF64B5F6)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${_focusedDay.month}月统计',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
'共${monthEvents.length}个活动',
style: TextStyle(
fontSize: 14.sp,
color: Colors.white.withOpacity(0.9),
),
),
],
),
SizedBox(height: 12.h),
Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: eventsByType.entries.map((entry) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(16.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getIconForEventType(entry.key),
size: 14.sp,
color: Colors.white,
),
SizedBox(width: 4.w),
Text(
'${entry.key} ${entry.value}',
style: TextStyle(fontSize: 12.sp, color: Colors.white),
),
],
),
);
}).toList(),
),
],
),
);
}
List<Event> _getMonthEvents(DateTime month) {
return _events.where((event) {
return event.date.year == month.year && event.date.month == month.month;
}).toList();
}
统计卡片使用蓝色渐变背景,显示当月活动总数和各类型活动数量。使用Map统计每种类型的活动数量,然后用Wrap组件展示。每个类型标签显示图标和数量,使用半透明白色背景。
这个功能让用户能够快速了解当月的活动分布情况,比如学校活动有几个、聚会有几个等。统计信息帮助用户更好地规划时间,避免某类活动过于集中。
快速日期跳转
提供快速跳转到特定日期的功能:
Widget _buildQuickJumpButton() {
return IconButton(
icon: const Icon(Icons.today),
onPressed: _showDatePicker,
tooltip: '跳转到指定日期',
);
}
Future<void> _showDatePicker() async {
final picked = await showDatePicker(
context: context,
initialDate: _selectedDay ?? DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2030),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: const ColorScheme.light(
primary: Color(0xFF2196F3),
onPrimary: Colors.white,
surface: Colors.white,
onSurface: Colors.black,
),
),
child: child!,
);
},
);
if (picked != null) {
setState(() {
_selectedDay = picked;
_focusedDay = picked;
});
}
}
快速跳转按钮调用系统日期选择器,用户可以直接选择任意日期。选择后日历自动跳转到该日期并选中。Theme配置确保日期选择器使用应用的主题色。
这个功能在用户需要查看很久之前或很久之后的活动时特别有用,避免了一个月一个月地翻页。比如要查看半年后的活动安排,直接跳转比翻页快得多。
活动提醒设置
为活动设置提前提醒:
Widget _buildReminderSetting(Event event) {
return ListTile(
leading: Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(Icons.notifications, color: Colors.orange),
),
title: const Text('活动提醒'),
subtitle: Text(
event.reminderEnabled ? '提前${event.reminderMinutes}分钟提醒' : '未设置提醒',
),
trailing: Switch(
value: event.reminderEnabled,
onChanged: (value) {
if (value) {
_showReminderDialog(event);
} else {
_disableReminder(event);
}
},
activeColor: Colors.orange,
),
);
}
void _showReminderDialog(Event event) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('设置提醒时间'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [15, 30, 60, 120].map((minutes) {
return ListTile(
title: Text('提前$minutes分钟'),
onTap: () {
_setReminder(event, minutes);
Navigator.pop(context);
},
);
}).toList(),
),
),
);
}
void _setReminder(Event event, int minutes) {
setState(() {
event.reminderEnabled = true;
event.reminderMinutes = minutes;
});
// 这里应该调用本地通知服务设置提醒
Get.snackbar(
'提醒已设置',
'将在活动开始前$minutes分钟提醒您',
snackPosition: SnackPosition.BOTTOM,
);
}
提醒设置功能让用户可以为每个活动单独设置提前提醒时间。开关控制是否启用提醒,点击开关弹出时间选择对话框。提供15分钟、30分钟、1小时、2小时四个选项。
这个功能确保用户不会错过重要活动。比如医疗预约需要提前出发,可以设置提前30分钟提醒;学校活动可能需要准备,可以设置提前1小时提醒。
活动搜索功能
支持搜索历史活动:
Widget _buildSearchButton() {
return IconButton(
icon: const Icon(Icons.search),
onPressed: _showSearchDialog,
);
}
void _showSearchDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('搜索活动'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
decoration: const InputDecoration(
hintText: '输入活动标题或描述',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
onChanged: (value) {
_searchEvents(value);
},
),
SizedBox(height: 16.h),
SizedBox(
height: 200.h,
child: _buildSearchResults(),
),
],
),
),
);
}
List<Event> _searchResults = [];
void _searchEvents(String query) {
if (query.isEmpty) {
setState(() => _searchResults = []);
return;
}
setState(() {
_searchResults = _events.where((event) {
return event.title.toLowerCase().contains(query.toLowerCase()) ||
(event.description?.toLowerCase().contains(query.toLowerCase()) ?? false);
}).toList();
});
}
Widget _buildSearchResults() {
if (_searchResults.isEmpty) {
return const Center(child: Text('暂无搜索结果'));
}
return ListView.builder(
itemCount: _searchResults.length,
itemBuilder: (context, index) {
final event = _searchResults[index];
return ListTile(
leading: Icon(
_getIconForEventType(event.type),
color: _getColorForEventType(event.type),
),
title: Text(event.title),
subtitle: Text(DateFormat('yyyy-MM-dd').format(event.date)),
onTap: () {
Navigator.pop(context);
setState(() {
_selectedDay = event.date;
_focusedDay = event.date;
});
},
);
},
);
}
搜索功能支持按标题或描述搜索活动。输入框实时搜索,结果列表显示匹配的活动。点击搜索结果自动跳转到对应日期。
这个功能在活动很多时特别有用。比如用户记得几个月前有个重要聚会但忘了具体日期,可以搜索"聚会"快速找到。搜索不区分大小写,提高了匹配成功率。
活动导出功能
支持导出活动到日历文件:
Widget _buildExportButton() {
return IconButton(
icon: const Icon(Icons.file_download),
onPressed: _exportEvents,
);
}
Future<void> _exportEvents() async {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const Center(child: CircularProgressIndicator()),
);
try {
final buffer = StringBuffer();
buffer.writeln('BEGIN:VCALENDAR');
buffer.writeln('VERSION:2.0');
buffer.writeln('PRODID:-//家庭相册//日历//CN');
for (var event in _events) {
buffer.writeln('BEGIN:VEVENT');
buffer.writeln('UID:${event.id}@familyalbum.com');
buffer.writeln('DTSTAMP:${_formatDateTime(DateTime.now())}');
buffer.writeln('DTSTART:${_formatDateTime(event.date)}');
buffer.writeln('SUMMARY:${event.title}');
if (event.description != null) {
buffer.writeln('DESCRIPTION:${event.description}');
}
buffer.writeln('END:VEVENT');
}
buffer.writeln('END:VCALENDAR');
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/events.ics');
await file.writeAsString(buffer.toString());
Navigator.pop(context);
Get.snackbar(
'导出成功',
'活动已导出到: ${file.path}',
snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 3),
);
// 可以调用系统分享功能分享文件
Share.shareFiles([file.path], text: '我的家庭活动日历');
} catch (e) {
Navigator.pop(context);
Get.snackbar('导出失败', e.toString(), snackPosition: SnackPosition.BOTTOM);
}
}
String _formatDateTime(DateTime dt) {
return '${dt.year}${dt.month.toString().padLeft(2, '0')}${dt.day.toString().padLeft(2, '0')}T${dt.hour.toString().padLeft(2, '0')}${dt.minute.toString().padLeft(2, '0')}00';
}
导出功能将所有活动导出为标准的iCalendar格式(.ics文件)。这种格式可以被大多数日历应用识别和导入。导出后可以通过系统分享功能发送给家人或导入到其他设备。
iCalendar格式包含活动的标题、日期、描述等信息。VCALENDAR是日历容器,VEVENT是单个活动。UID是活动的唯一标识符,DTSTAMP是时间戳,DTSTART是开始时间,SUMMARY是标题,DESCRIPTION是描述。
周视图切换
提供周视图和月视图的切换:
CalendarFormat _calendarFormat = CalendarFormat.month;
Widget _buildFormatToggle() {
return IconButton(
icon: Icon(_calendarFormat == CalendarFormat.month ? Icons.view_week : Icons.view_module),
onPressed: () {
setState(() {
_calendarFormat = _calendarFormat == CalendarFormat.month
? CalendarFormat.week
: CalendarFormat.month;
});
},
tooltip: _calendarFormat == CalendarFormat.month ? '切换到周视图' : '切换到月视图',
);
}
周视图只显示一周的日期,占用空间更少,适合查看近期活动。月视图显示整月日期,适合查看全月安排。用户可以根据需要自由切换。
在TableCalendar组件中设置calendarFormat属性即可切换视图模式。图标也会相应变化,月视图用view_module图标,周视图用view_week图标,让用户清楚当前的视图模式。
总结
日历Tab通过TableCalendar组件实现了完整的日历功能,支持日期选择、活动展示、月份切换等基础功能。扩展功能包括活动统计、快速跳转、提醒设置、搜索导出等,大大提升了实用性。
月份切换动画让交互更流畅,统计卡片让用户了解活动分布,快速跳转节省翻页时间,提醒设置避免错过重要活动,搜索功能快速定位历史活动,导出功能支持数据迁移和分享,周视图切换满足不同查看需求。
整个页面的设计既美观又实用,是家庭相册App中管理家庭活动的重要工具。通过日历视图,用户可以清晰地看到家庭的活动安排,合理规划时间,记录美好时光。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)