Flutter for OpenHarmony轻量级开源记事本app实战——笔记卡片组件
笔记卡片是记事本应用中最核心的UI组件之一,它负责在列表中展示单个笔记的信息。一个设计良好的笔记卡片应该信息丰富、操作便捷、视觉美观。本文将详细介绍如何实现一个功能完整的笔记卡片组件,包括滑动操作、状态显示、选择模式等特性。
笔记卡片的设计理念
笔记卡片需要在有限的空间内展示尽可能多的信息。我们需要显示笔记的标题、内容预览、更新时间、标签等。同时还要通过图标来表示笔记的状态,比如是否置顶、是否收藏、是否锁定等。
在交互设计上,笔记卡片支持多种操作。点击卡片可以打开编辑页面,长按可以进入选择模式,滑动可以快速执行收藏或删除操作。这些交互方式让用户可以高效地管理笔记。
组件的基础结构
让我们从笔记卡片组件的基本代码开始:
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:intl/intl.dart';
import '../../../models/note.dart';
class NoteCard extends StatelessWidget {
final Note note;
笔记卡片组件需要导入几个依赖包。flutter_screenutil用于屏幕适配,intl用于日期格式化,Note是笔记的数据模型。
NoteCard是一个无状态Widget,因为它不需要管理自己的状态。所有的数据都通过参数传入,所有的操作都通过回调函数通知外部。这种设计让组件保持简单和可复用。
final bool isSelected;
final bool isSelectionMode;
final VoidCallback onTap;
final VoidCallback onLongPress;
final Function(DismissDirection) onDismissed;
const NoteCard({
super.key,
required this.note,
组件接收多个参数。note是必需的,包含笔记的所有信息。isSelected和isSelectionMode用于选择模式,onTap和onLongPress是点击回调,onDismissed是滑动回调。
这种参数化的设计让组件非常灵活。调用者可以根据需要传入不同的参数和回调,实现不同的功能。组件本身不关心这些操作的具体实现,只负责触发回调。
this.isSelected = false,
this.isSelectionMode = false,
required this.onTap,
required this.onLongPress,
required this.onDismissed,
});
Color? _parseColor(String? colorStr) {
if (colorStr == null) return null;
isSelected和isSelectionMode有默认值false,这样在不需要选择模式时可以省略这些参数。其他回调都是必需的,确保组件能够正常响应用户操作。
_parseColor是一个辅助方法,用于将颜色字符串转换为Color对象。笔记可以有自定义颜色,这个方法负责解析颜色值。
try {
return Color(int.parse(colorStr.replaceFirst('#', '0xFF')));
} catch (e) {
return null;
}
}
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
颜色字符串的格式是"#RRGGBB",我们需要将它转换为"0xFFRRGGBB"格式。使用try-catch确保即使颜色格式错误也不会导致应用崩溃。
build方法首先获取当前主题的亮度,用于后续的颜色处理。在深色模式下,我们需要使用不同的默认背景色。
Dismissible组件的使用
笔记卡片使用Dismissible包裹,实现滑动操作:
final bgColor = _parseColor(note.color);
return Dismissible(
key: Key(note.id),
background: Container(
alignment: Alignment.centerLeft,
padding: EdgeInsets.only(left: 20.w),
color: Colors.amber,
Dismissible需要一个唯一的key,我们使用笔记的ID。background定义向右滑动时显示的背景,我们使用琥珀色背景和星星图标,表示收藏操作。
Container的alignment设置为centerLeft,让图标显示在左侧。padding添加左边距,让图标不会紧贴边缘。这些细节让滑动操作看起来更加精致。
child: const Icon(Icons.star, color: Colors.white),
),
secondaryBackground: Container(
alignment: Alignment.centerRight,
padding: EdgeInsets.only(right: 20.w),
color: Colors.red,
child: const Icon(Icons.delete, color: Colors.white),
),
secondaryBackground定义向左滑动时显示的背景,我们使用红色背景和删除图标,表示删除操作。alignment设置为centerRight,让图标显示在右侧。
这种双向滑动的设计让用户可以快速执行两种常见操作。颜色的选择也很有意义:琥珀色表示收藏(类似星星的颜色),红色表示删除(警告色)。
confirmDismiss: (direction) async {
onDismissed(direction);
return direction == DismissDirection.endToStart;
},
child: Card(
color: bgColor ?? (isDark ? const Color(0xFF1E1E1E) : Colors.white),
margin: EdgeInsets.only(bottom: 8.h),
confirmDismiss回调在滑动完成时触发。我们调用onDismissed通知外部,然后根据滑动方向决定是否真正移除卡片。只有向左滑动(删除)时才返回true,向右滑动(收藏)返回false。
Card组件提供了卡片的基本样式,包括圆角、阴影等。颜色优先使用笔记的自定义颜色,如果没有就根据主题使用默认颜色。
InkWell的交互处理
Card内部使用InkWell处理点击事件:
child: InkWell(
onTap: onTap,
onLongPress: onLongPress,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: EdgeInsets.all(12.w),
child: Row(
InkWell提供了Material Design的水波纹点击效果。onTap和onLongPress分别处理点击和长按事件。borderRadius设置为12,与Card的圆角保持一致。
Container添加内边距,让内容与卡片边缘保持适当距离。Row用于水平排列选择图标和笔记内容,这样在选择模式下可以显示复选框。
children: [
if (isSelectionMode)
Padding(
padding: EdgeInsets.only(right: 12.w),
child: Icon(
isSelected ? Icons.check_circle : Icons.circle_outlined,
color: isSelected ? const Color(0xFF2196F3) : Colors.grey,
),
),
如果处于选择模式,就显示一个图标。选中时显示实心圆圈图标,未选中时显示空心圆圈图标。颜色也相应变化,选中时使用主题色,未选中时使用灰色。
这种条件渲染让组件可以适应不同的使用场景。在普通模式下,不显示选择图标,节省空间。在选择模式下,图标让用户清楚地看到哪些笔记被选中。
笔记内容的展示
笔记内容使用Column垂直排列:
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (note.isPinned)
Padding(
padding: EdgeInsets.only(right: 4.w),
Expanded让内容区域占据剩余的所有空间。Column的crossAxisAlignment设置为start,让内容左对齐。第一行是标题行,使用Row来排列状态图标和标题文本。
如果笔记被置顶,就显示一个图钉图标。Padding添加右边距,让图标与后面的内容保持间距。这种状态图标让用户可以快速识别笔记的特殊状态。
child: Icon(Icons.push_pin,
size: 14.sp, color: const Color(0xFF2196F3)),
),
if (note.isFavorite)
Padding(
padding: EdgeInsets.only(right: 4.w),
child: Icon(Icons.star,
size: 14.sp, color: Colors.amber),
),
如果笔记被收藏,就显示一个星星图标。图标大小设置为14sp,比标题文字小一些,不会过于突兀。颜色使用琥珀色,这是星星的典型颜色。
这些状态图标都是可选的,只在相应状态为true时显示。通过if条件,我们可以灵活地控制显示哪些图标。
if (note.isLocked)
Padding(
padding: EdgeInsets.only(right: 4.w),
child: Icon(Icons.lock,
size: 14.sp, color: Colors.grey),
),
Expanded(
child: Text(
note.title.isEmpty ? '无标题' : note.title,
如果笔记被锁定,就显示一个锁图标。锁定的笔记需要密码才能查看,这个图标提醒用户该笔记是受保护的。
标题文本使用Expanded包裹,让它占据剩余空间。如果标题为空,就显示"无标题"。这种友好的提示比显示空白更好。
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
标题使用16sp的字体大小,字重为w600(半粗体),让它看起来更加醒目。maxLines设置为1,overflow设置为ellipsis,长标题会被截断并显示省略号。
这种文本截断处理很重要,它确保了卡片的高度一致性。即使标题很长,也不会撑开卡片,影响列表的整齐度。
内容预览和元信息
标题下方显示内容预览和元信息:
SizedBox(height: 4.h),
Text(
note.content.isEmpty ? '暂无内容' : note.content,
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey,
),
maxLines: 2,
标题和内容之间添加4个逻辑像素的间距。内容文本使用14sp的字体大小,颜色为灰色,与标题形成对比。maxLines设置为2,最多显示两行内容。
内容预览让用户可以快速了解笔记的大致内容,而不需要打开笔记。两行的限制确保了卡片不会太高,同时又能显示足够的信息。
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 8.h),
Row(
children: [
Text(
DateFormat('MM-dd HH:mm').format(note.updatedAt),
style: TextStyle(
内容文本也使用ellipsis截断,长内容会显示省略号。内容和元信息之间添加8个逻辑像素的间距,比标题和内容之间的间距大一些。
元信息行使用Row排列更新时间和标签。更新时间使用DateFormat格式化为"月-日 时:分"的格式,这种格式简洁明了,适合在列表中显示。
fontSize: 12.sp,
color: Colors.grey,
),
),
if (note.tags.isNotEmpty) ...[
SizedBox(width: 8.w),
Expanded(
child: Text(
note.tags.map((t) => '#$t').join(' '),
时间文本使用12sp的小字体,颜色为灰色,不会过于突出。如果笔记有标签,就在时间后面显示标签列表。
标签使用展开运算符添加到children列表中。首先添加一个间距,然后是标签文本。标签文本使用Expanded包裹,让它占据剩余空间。
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF2196F3),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
],
),
],
),
),
],
),
),
),
),
);
}
}
标签文本使用主题色(蓝色),让它在视觉上与其他信息区分开。每个标签前面添加#号,这是标签的常见表示方式。多个标签用空格分隔。
标签文本也限制为一行,如果标签太多会被截断。这确保了卡片的高度一致性,即使笔记有很多标签也不会影响布局。
颜色解析的实现
_parseColor方法负责将颜色字符串转换为Color对象:
Color? _parseColor(String? colorStr) {
if (colorStr == null) return null;
try {
return Color(int.parse(colorStr.replaceFirst('#', '0xFF')));
} catch (e) {
return null;
}
}
方法首先检查颜色字符串是否为null,如果是就直接返回null。然后使用try-catch包裹转换逻辑,确保即使格式错误也不会崩溃。
颜色字符串的格式是"#RRGGBB",我们需要将#替换为0xFF,然后解析为整数。这个整数就是Color构造函数需要的参数。
如果解析失败,catch块会捕获异常并返回null。这种容错处理很重要,它让应用更加健壮,不会因为数据问题而崩溃。
滑动操作的处理
Dismissible组件实现了滑动操作,让我们深入了解它的工作原理:
return Dismissible(
key: Key(note.id),
background: Container(...),
secondaryBackground: Container(...),
confirmDismiss: (direction) async {
onDismissed(direction);
return direction == DismissDirection.endToStart;
},
child: Card(...),
);
Dismissible需要一个唯一的key来识别每个item。我们使用笔记的ID,因为它在整个应用中是唯一的。
background和secondaryBackground分别定义向右和向左滑动时显示的背景。这两个背景会在滑动时逐渐显示出来,给用户视觉反馈。
confirmDismiss是一个异步回调,返回true表示确认移除item,返回false表示取消。我们只在删除操作时返回true,收藏操作返回false,因为收藏不需要移除item。
选择模式的实现
笔记卡片支持选择模式,让用户可以批量操作笔记:
if (isSelectionMode)
Padding(
padding: EdgeInsets.only(right: 12.w),
child: Icon(
isSelected ? Icons.check_circle : Icons.circle_outlined,
color: isSelected ? const Color(0xFF2196F3) : Colors.grey,
),
),
选择模式通过isSelectionMode参数控制。当这个参数为true时,卡片左侧会显示一个选择图标。
图标的样式根据isSelected参数变化。选中时显示实心圆圈,颜色为主题色;未选中时显示空心圆圈,颜色为灰色。这种视觉反馈让用户清楚地知道哪些笔记被选中。
选择模式的实现很简单,但它大大提升了应用的可用性。用户可以一次选择多个笔记进行批量删除、移动或导出等操作。
状态图标的设计
笔记卡片使用多个图标来表示笔记的状态:
if (note.isPinned)
Padding(
padding: EdgeInsets.only(right: 4.w),
child: Icon(Icons.push_pin,
size: 14.sp, color: const Color(0xFF2196F3)),
),
if (note.isFavorite)
Padding(
padding: EdgeInsets.only(right: 4.w),
child: Icon(Icons.star,
size: 14.sp, color: Colors.amber),
),
if (note.isLocked)
Padding(
padding: EdgeInsets.only(right: 4.w),
child: Icon(Icons.lock,
size: 14.sp, color: Colors.grey),
),
每个状态图标都是可选的,只在相应状态为true时显示。图标的大小统一为14sp,比标题文字小一些,不会喧宾夺主。
图标的颜色经过精心选择。置顶图标使用主题色,表示这是一个重要的状态。收藏图标使用琥珀色,这是星星的典型颜色。锁定图标使用灰色,表示这是一个限制性的状态。
这些图标让用户可以一眼看出笔记的状态,不需要打开笔记就能了解它的属性。这种信息密度的平衡很重要,既要显示足够的信息,又不能让界面过于拥挤。
文本截断的处理
笔记卡片中的文本都使用了截断处理:
Text(
note.title.isEmpty ? '无标题' : note.title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
标题限制为一行,内容限制为两行,标签限制为一行。这种限制确保了卡片的高度一致性,让列表看起来整齐。
overflow设置为ellipsis,超出部分会显示省略号。这是一个标准的文本截断方式,用户可以通过省略号知道还有更多内容。
这种截断处理在移动应用中很常见,因为屏幕空间有限。我们需要在显示足够信息和保持界面整洁之间找到平衡。
日期格式化
更新时间使用DateFormat格式化:
Text(
DateFormat('MM-dd HH:mm').format(note.updatedAt),
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey,
),
),
格式化模式是’MM-dd HH:mm’,显示月-日 时:分。这种格式简洁明了,适合在列表中显示。如果需要显示年份,可以使用’yyyy-MM-dd HH:mm’。
DateFormat是intl包提供的类,支持多种日期格式和国际化。我们可以根据用户的地区设置来调整日期格式,提供更好的本地化体验。
日期格式的选择需要考虑信息的重要性和空间的限制。在笔记卡片中,我们更关心最近的更新时间,所以省略了年份。
标签的显示
标签列表使用特殊的格式显示:
if (note.tags.isNotEmpty) ...[
SizedBox(width: 8.w),
Expanded(
child: Text(
note.tags.map((t) => '#$t').join(' '),
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF2196F3),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
标签列表使用map方法为每个标签添加#前缀,然后用join方法用空格连接。这种格式化让标签看起来更像社交媒体中的话题标签。
标签文本使用主题色,让它在视觉上突出。这种颜色编码帮助用户快速识别不同类型的信息。
标签也限制为一行,如果标签太多会被截断。这是一个权衡,我们优先保证卡片的整齐度,而不是显示所有标签。
响应式布局
笔记卡片使用flutter_screenutil实现响应式布局:
padding: EdgeInsets.all(12.w),
margin: EdgeInsets.only(bottom: 8.h),
fontSize: 16.sp,
所有的尺寸都使用.w、.h、.sp等单位,这些单位会根据屏幕尺寸自动调整。这确保了卡片在不同设备上都有合适的大小。
这种响应式设计很重要,因为移动设备的屏幕尺寸差异很大。通过使用相对单位,我们可以让界面在所有设备上都保持良好的比例。
性能优化
笔记卡片的性能优化主要体现在几个方面。首先是使用const构造函数,对于不变的widget可以复用实例。
其次是文本的截断处理,限制maxLines可以避免渲染过多的文本。这在列表中尤其重要,因为可能有很多卡片同时显示。
第三是条件渲染,只在需要时才创建widget。比如状态图标和选择图标都是条件渲染的,不需要时不会创建。
总结
笔记卡片组件是记事本应用的核心UI组件,它需要在有限的空间内展示丰富的信息,同时提供便捷的操作方式。通过精心的设计和实现,我们创建了一个既美观又实用的笔记卡片。
组件的设计遵循了几个重要原则:信息的层次性、操作的便捷性、视觉的一致性。通过合理的布局、恰当的颜色和图标选择,我们让用户可以快速理解和操作笔记。
笔记卡片的实现展示了Flutter组件化开发的优势。通过参数化设计,组件可以在不同场景中复用。通过回调函数,组件可以与外部灵活交互。这种设计让代码更加清晰和可维护。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)