笔记卡片是记事本应用中最核心的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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐