Flutter for OpenHarmony生活助手App实战:日历视图实现
本文介绍了生活助手App中日历视图功能的设计与实现。作者分析了日历视图的三个核心价值:增强时间感知、优化计划管理、方便回顾历史。功能设计采用周视图平衡信息密度,并集成快捷入口和今日待办事项。技术实现上使用Flutter的table_calendar包,通过状态管理实现日期选择交互,并详细说明了日历样式定制、快捷入口网格布局等关键代码实现。该设计将日历与生活管理功能有机结合,提升了用户的时间管理效率

说起日历,我自己以前总是用手机自带的日历App,但总觉得不够用。想看今天有什么待办,得切换到另一个App;想看这周的安排,又得翻来翻去。后来我就想,要是能把日历和待办事项结合起来就好了。所以在做这个生活助手App时,日历视图是生活管理页面的核心组件。
为什么日历视图这么重要
在开始写代码之前,我先想清楚了这个功能的价值。
第一个是时间感知。人对时间的感知是模糊的,今天是几号、星期几,很多人都要想一下。有个日历放在那里,一眼就能看到,时间感会更强。
第二个是计划管理。看着日历,能更好地规划未来。比如看到下周三有个重要会议,就可以提前准备。如果只是列表形式,就没有这种时间上的直观感受。
第三个是回顾过去。日历不仅能看未来,还能看过去。哪天完成了什么任务,哪天有什么重要事件,都能在日历上标记出来。这种回顾对个人成长很有帮助。
功能设计的思路
在设计这个功能时,我考虑了以下几个方面。
周视图的选择
日历有月视图、周视图、日视图三种。我选择了周视图,因为它是最平衡的。月视图信息太多,看不清细节;日视图信息太少,看不到全局。周视图刚刚好,一周的安排一目了然。
快捷入口的设计
日历下方放了6个快捷入口,分别是待办事项、购物清单、备忘录、日记本、倒计时、菜谱。这些都是生活管理的常用功能,点击就能进入对应页面。
今日待办的展示
日历下方还展示了今日待办事项,让用户不用点进去就能看到今天要做什么。这种信息前置的设计,能提高效率。
页面整体结构
先看页面的基本框架:
class LifePage extends StatefulWidget {
const LifePage({super.key});
State<LifePage> createState() => _LifePageState();
}
class _LifePageState extends State<LifePage> {
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[100],
appBar: AppBar(
title: const Text('生活管理'),
),
body: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildCalendar(),
SizedBox(height: 24.h),
_buildQuickActions(),
SizedBox(height: 24.h),
_buildTodayTasks(),
],
),
),
);
}
}
StatefulWidget的必要性
这里用了StatefulWidget,因为日历的选中日期会变化。用户点击不同的日期,界面要更新。如果用StatelessWidget,就没法响应用户的操作。
状态变量的设计
定义了两个状态变量:
_focusedDay:当前聚焦的日期,默认是今天_selectedDay:用户选中的日期,默认是null
这两个变量的区别是:_focusedDay决定日历显示哪一周,_selectedDay决定哪一天被高亮。
页面布局的三个部分
页面分成三块:日历、快捷入口、今日待办。用SizedBox隔开,间距设置为24,看起来不会太挤。
外层用SingleChildScrollView包裹,这样内容超出屏幕时可以滚动。背景色设置为Colors.grey[100],浅灰色,和白色卡片形成对比。
日历组件的实现
日历是这个页面的核心,我用了table_calendar包:
Widget _buildCalendar() {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
),
child: 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;
});
},
calendarFormat: CalendarFormat.week,
headerStyle: HeaderStyle(
formatButtonVisible: false,
titleCentered: true,
titleTextStyle: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
),
calendarStyle: CalendarStyle(
todayDecoration: BoxDecoration(
color: Colors.blue.withOpacity(0.3),
shape: BoxShape.circle,
),
selectedDecoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
),
),
);
}
TableCalendar的基本参数
firstDay和lastDay定义了日历的范围,从2020年到2030年。这个范围要足够大,避免用户查看历史或未来时超出范围。
focusedDay是当前聚焦的日期,决定日历显示哪一周。selectedDayPredicate是一个判断函数,返回某一天是否被选中。
日期选择的回调
onDaySelected是用户点击日期时的回调,传入两个参数:
selectedDay:用户点击的日期focusedDay:新的聚焦日期
在回调里调用setState,更新两个状态变量。这样界面就会重新渲染,选中的日期会高亮显示。
周视图的设置
calendarFormat: CalendarFormat.week设置为周视图。这样日历只显示一周,不会占用太多空间。
头部样式的定制
headerStyle定制了日历头部的样式:
formatButtonVisible: false:隐藏格式切换按钮,因为我们固定用周视图titleCentered: true:标题居中显示titleTextStyle:标题文字样式,16号字,加粗
日期样式的定制
calendarStyle定制了日期的样式:
todayDecoration:今天的装饰,蓝色半透明圆圈selectedDecoration:选中日期的装饰,蓝色实心圆圈
这两个装饰的区别是:今天是半透明的,选中的是实心的。这样用户能区分"今天"和"选中的日期"。
快捷入口的设计
日历下方是6个快捷入口,用网格布局展示:
Widget _buildQuickActions() {
final actions = [
{'title': '待办事项', 'icon': Icons.check_box, 'color': Colors.blue, 'page': const TodoListPage()},
{'title': '购物清单', 'icon': Icons.shopping_cart, 'color': Colors.orange, 'page': const ShoppingListPage()},
{'title': '备忘录', 'icon': Icons.note, 'color': Colors.purple, 'page': const NotesPage()},
{'title': '日记本', 'icon': Icons.book, 'color': Colors.pink, 'page': const DiaryPage()},
{'title': '倒计时', 'icon': Icons.timer, 'color': Colors.red, 'page': const CountdownPage()},
{'title': '菜谱', 'icon': Icons.restaurant_menu, 'color': Colors.green, 'page': const RecipePage()},
];
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 12.w,
mainAxisSpacing: 12.h,
childAspectRatio: 1,
),
itemCount: actions.length,
itemBuilder: (context, index) {
final action = actions[index];
return GestureDetector(
onTap: () => Get.to(() => action['page'] as Widget),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(action['icon'] as IconData, color: action['color'] as Color, size: 32.sp),
SizedBox(height: 8.h),
Text(
action['title'] as String,
style: TextStyle(fontSize: 12.sp),
textAlign: TextAlign.center,
),
],
),
),
);
},
);
}
数据结构的设计
每个快捷入口包含四个字段:
title:标题,比如"待办事项"icon:图标,用Material Iconscolor:颜色,每个入口有自己的主题色page:对应的页面,点击后跳转
这种数据驱动的方式很灵活,想加新的入口,只需要在数组里加一项。
GridView的布局
crossAxisCount: 3表示每行显示3个入口。crossAxisSpacing和mainAxisSpacing设置间距。childAspectRatio: 1让每个格子是正方形。
shrinkWrap: true让GridView不要自己处理滚动,交给外层的SingleChildScrollView。physics: const NeverScrollableScrollPhysics()禁用GridView自己的滚动。
点击跳转的实现
每个入口用GestureDetector包裹,点击时调用Get.to()跳转到对应页面。这里用了GetX的路由管理,比Navigator更简洁。
图标和文字的布局
每个入口是一个白色卡片,里面是图标和文字垂直排列。图标大小32,颜色是主题色。文字大小12,居中显示。
mainAxisAlignment: MainAxisAlignment.center让图标和文字在卡片中垂直居中。
今日待办的展示
日历和快捷入口下方,展示了今日待办事项:
Widget _buildTodayTasks() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'今日待办',
style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
),
SizedBox(height: 12.h),
_buildTaskCard('完成项目报告', false, Colors.red),
_buildTaskCard('去超市买菜', false, Colors.orange),
_buildTaskCard('健身房锻炼', true, Colors.green),
],
);
}
标题的设计
"今日待办"这个标题用18号加粗字体,让用户知道下面是什么内容。
任务列表的构建
这里硬编码了3个任务,实际项目中应该从数据库读取。每个任务调用_buildTaskCard方法构建卡片。
任务卡片的实现
Widget _buildTaskCard(String title, bool isCompleted, Color color) {
return Container(
margin: EdgeInsets.only(bottom: 12.h),
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: [
Icon(
isCompleted ? Icons.check_circle : Icons.radio_button_unchecked,
color: isCompleted ? Colors.green : color,
),
SizedBox(width: 12.w),
Expanded(
child: Text(
title,
style: TextStyle(
fontSize: 14.sp,
decoration: isCompleted ? TextDecoration.lineThrough : null,
),
),
),
],
),
);
}
图标的动态变化
任务卡片左边的图标根据完成状态变化:
- 未完成:
Icons.radio_button_unchecked,空心圆圈,颜色是任务的主题色 - 已完成:
Icons.check_circle,打勾的圆圈,颜色是绿色
文字的删除线
已完成的任务,文字会加上删除线:decoration: TextDecoration.lineThrough。这是一种常见的视觉表达,表示任务已经完成。
颜色的含义
每个任务有自己的颜色,表示优先级或类型:
- 红色:紧急重要
- 橙色:一般重要
- 绿色:已完成
这种颜色编码能帮助用户快速识别任务的重要程度。
日历事件的标记
实际项目中,日历上应该标记有事件的日期。比如某天有待办事项,就在日期下方加个小圆点。
table_calendar支持事件标记,需要提供一个事件映射:
Map<DateTime, List<Event>> _events = {
DateTime(2024, 1, 15): [Event('完成项目报告')],
DateTime(2024, 1, 16): [Event('团队会议'), Event('健身房锻炼')],
DateTime(2024, 1, 17): [Event('去超市买菜')],
};
TableCalendar(
// 其他参数...
eventLoader: (day) {
return _events[day] ?? [];
},
calendarBuilders: CalendarBuilders(
markerBuilder: (context, date, events) {
if (events.isNotEmpty) {
return Positioned(
bottom: 1,
child: Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
);
}
return null;
},
),
)
eventLoader的作用
eventLoader是一个函数,传入日期,返回该日期的事件列表。table_calendar会调用这个函数,获取每一天的事件。
markerBuilder的作用
markerBuilder是一个构建器,用来自定义事件标记的样式。这里我用了一个红色小圆点,放在日期下方。
如果某天有多个事件,可以显示多个圆点,或者显示数字。
日期选择的交互
用户点击日期后,应该有相应的交互。比如显示该日期的详细信息,或者跳转到该日期的待办列表。
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
// 显示该日期的事件
_showDayEvents(selectedDay);
},
void _showDayEvents(DateTime day) {
final events = _events[day] ?? [];
if (events.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('该日期没有事件')),
);
return;
}
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: EdgeInsets.all(16.w),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
DateFormat('yyyy年MM月dd日').format(day),
style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
),
SizedBox(height: 16.h),
...events.map((event) => ListTile(
leading: const Icon(Icons.event),
title: Text(event.title),
)),
],
),
),
);
}
底部弹窗的使用
点击日期后,用showModalBottomSheet弹出一个底部弹窗,显示该日期的所有事件。这种交互比跳转到新页面更轻量,用户可以快速查看后关闭。
空状态的处理
如果该日期没有事件,显示一个SnackBar提示用户。这比什么都不显示要好,让用户知道操作生效了。
周视图的切换
虽然我默认用周视图,但也可以支持切换到月视图。可以在AppBar加个按钮:
AppBar(
title: const Text('生活管理'),
actions: [
IconButton(
icon: Icon(_calendarFormat == CalendarFormat.week
? Icons.calendar_view_month
: Icons.calendar_view_week),
onPressed: () {
setState(() {
_calendarFormat = _calendarFormat == CalendarFormat.week
? CalendarFormat.month
: CalendarFormat.week;
});
},
),
],
)
点击按钮,在周视图和月视图之间切换。图标也会跟着变化,让用户知道当前是什么视图。
实际使用体验
这个日历视图我自己用了一段时间,感觉很方便。每天打开应用,第一眼就能看到今天是几号,星期几,有什么待办。
周视图的大小刚刚好,不会占用太多空间,又能看到一周的安排。如果想看更远的日期,可以左右滑动切换周。
快捷入口也很实用。想记个待办,点一下就进去了,不用在菜单里找。6个入口基本覆盖了日常需求。
今日待办的展示很贴心。不用点进去,就能看到今天要做什么。看到已完成的任务有删除线,会有成就感。
可以改进的地方
如果要做得更完善,可以考虑以下几点。
事件的颜色标记
不同类型的事件用不同颜色的圆点标记。比如待办事项用蓝色,日记用粉色,倒计时用红色。这样一眼就能看出某天有什么类型的事件。
长按添加事件
长按日期,弹出菜单,可以快速添加待办、日记等。这比先进入对应页面再添加要快。
拖拽调整事件
支持拖拽事件到其他日期,调整计划。这种交互很直观,比编辑日期要方便。
周数的显示
在日历头部显示当前是第几周,对于需要按周规划的用户很有用。
节假日的标记
在日历上标记节假日,用不同的颜色或图标。这样用户能提前知道哪天放假,方便安排。
小结
今天实现了日历视图功能,用到了table_calendar包、网格布局、列表展示等组件。核心是用周视图展示日历,配合快捷入口和今日待办,形成一个完整的生活管理页面。
这个功能虽然看起来简单,但对用户体验影响很大。日历是时间管理的基础,有了日历,用户才能更好地规划生活。
在实现过程中,我特别注重信息的层次。日历在最上面,最重要;快捷入口在中间,方便操作;今日待办在下面,提供详细信息。这种布局符合用户的视觉习惯。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)