鸿蒙新特性:SwipeAction 滑动操作——构建任务收件箱与手势交互
滑动操作(Swipe Action)是移动端最自然、最高效的交互模式之一。从 iOS 邮件应用的"滑动归档"到微信的"滑动删除聊天",从 Gmail 的"滑动标记已读"到 Things 的"滑动安排日程"——滑动手势已经成为用户无需思考的肌肉记忆。HarmonyOS NEXT ArkUI 在 ListItem 组件上提供了 .swipeAction() 属性,让开发者用少量代码就能实现丰富的滑动交互菜单。
然而,SwipeAction 不只是"加几个按钮"那么简单。在哪一侧放置什么操作、操作区的视觉设计、滑动后状态的更新时机、与列表排序和筛选的配合——这些设计决策决定了滑动交互的可用性和流畅度。本文将通过一个"任务收件箱"实战案例,深入讲解 SwipeAction 的设计原理、实现细节和最佳实践。
关键词:HarmonyOS、ArkUI、SwipeAction、滑动操作、手势交互、任务管理
一、SwipeAction 组件基础
1.1 API 概述
SwipeAction 是 ListItem 的扩展属性,通过 .swipeAction() 方法配置。它支持在列表项的两侧各配置一组操作按钮,用户通过水平滑动手势将其"拉出":
ListItem() {
// 列表项内容
}
.swipeAction({
start: { // 起始侧(LTR 布局下为左侧)
builder: () => { /* 操作按钮 */ },
actionAreaDistance: 80 // 操作区宽度(vp)
},
end: { // 结束侧(LTR 布局下为右侧)
builder: () => { /* 操作按钮 */ },
actionAreaDistance: 160
}
})
核心属性说明:
- start:在阅读方向起始侧的操作区。从左到右(LTR)布局中这是左侧,右到左(RTL)布局中则是右侧。用户将列表项向右滑动时显露该区域。
- end:在阅读方向结束侧的操作区。LTR 中为右侧,用户将列表项向左滑动时显露。
- builder:一个
@Builder函数,构建操作区的 UI 内容。支持 Row、Text、自定义组件等。 - actionAreaDistance:操作区展开后的宽度(vp)。决定了用户需要滑多长距离才能完全显露出操作按钮。
1.2 两侧操作的设计哲学
在移动端设计中,左侧滑动和右侧滑动承载不同的语义:
左滑(显露 end 侧)——这是最符合右手拇指操作习惯的方向,通常放置破坏性操作(删除)和中性操作(归档、标记已读)。原因有三:一是这个方向的滑动最顺手,二是左滑是"推开"当前内容的隐喻(推开 = 移除/删除),三是用户的下意识滑动方向。Apple 的 Human Interface Guidelines 和 Google 的 Material Design 都推荐将主要操作放在左滑方向。
右滑(显露 start 侧)——右滑通常放置"优先"操作(置顶、收藏、标记重要)。这是"拉近"的隐喻(拉回 = 想要/需要),用户需要更大的意图来执行这个方向的手势(尤其是左手用户)。在邮件应用中,右滑通常用于"标记未读"或"移动到文件夹"。
在我们的"任务收件箱"中:
右滑(start 侧)→ 置顶 / 取消置顶
左滑(end 侧) → 完成 + 删除
这个布局符合上述设计哲学:左滑是最自然的操作,放置最常用的"完成"和"删除";右滑是次常用的"置顶",只有需要优先级调整时才触发。
1.3 操作区的视觉设计
操作区的设计直接影响用户的操作信心。一个设计糟糕的操作区(如颜色含义模糊、按钮过大或过小、缺少文字标注)会使用户犹豫甚至误操作。
本文的操作区设计原则:
绿色"完成"按钮(左滑第一位):使用 #00B894 背景,这是自然、安全、积极的颜色信号。放在最靠近内容的一侧(即滑动距离最小的位置),因为"完成"比"删除"更频繁使用,用户不必滑太远就能触发。
红色"删除"按钮(左滑第二位):使用 #FF4D4F 背景,这是全世界通用的"危险/删除"颜色信号。放在更远的位置需要用户付出额外的滑动距离,这种"摩擦"设计减少了误删的可能性。
蓝色"置顶"按钮(右滑):使用 #1677FF 背景,这是"操作/重要"的颜色信号。单独占用右滑方向,不与左滑操作竞争视觉注意力。
每个按钮使用 80vp 宽度(actionAreaDistance),两个左滑按钮共 160vp。这个宽度在"足够容纳文字"和"不会过度遮挡内容"之间取得了平衡。
1.4 与系统手势的协调
SwipeAction 与系统级的边缘滑动手势(如来福手势返回)需要协调。HarmonyOS 的 SwipeAction 已经处理了手势冲突——当列表项有滑动操作时,系统会优先响应列表项级别的手势。如果用户在列表区域内滑动,触发 SwipeAction;如果在屏幕边缘滑动,触发系统返回手势。
开发者无需额外处理手势冲突,这是框架层的默认行为。但如果列表项同时有自定义手势(如 .gesture()),需要注意手势优先级的管理。
二、任务收件箱的设计
2.1 功能规划
任务收件箱是一个完整的任务管理工具,融合了滑动操作、状态切换、优先级管理和列表筛选:
- 滑动操作:左滑显示"完成"+“删除”,右滑显示"置顶"
- 点击完成:勾选复选框切换完成/未完成状态
- 优先级标签:高/中/低三级,用颜色区分
- 置顶排序:已置顶任务自动排到列表顶部
- 分类筛选:全部 / 未完成 / 已完成
- 统计面板:全部任务 / 未完成 / 已完成数量
- 添加任务:快捷添加新任务到列表底部
- 空状态:筛选结果为空时显示相应提示
2.2 数据结构设计
interface TaskItem {
id: number;
title: string; // 任务标题
desc: string; // 任务描述
priority: string; // 'high' | 'medium' | 'low'
color: string; // 优先级对应的颜色
completed: boolean; // 是否已完成
pinned: boolean; // 是否置顶
}
关键字段的设计考量:
priority + color 组合:使用枚举式字符串(high/medium/low)标识优先级,同时存储对应的显示颜色。颜色随优先级确定(红→高、橙→中、绿→低),在任务列表中用作左侧色条。优先级的映射关系:
| 优先级 | 颜色 | 标签 | 含义 |
|---|---|---|---|
| high | #FF4D4F(红) | 高 | 需优先处理,通常包含截止日期 |
| medium | #E09F3E(橙) | 中 | 正常处理,按计划推进即可 |
| low | #00B894(绿) | 低 | 有空再处理,无紧急时限 |
pinned 置顶标记:布尔值,与 completed 互斥——已完成的任务不能置顶。这是合理的业务规则:已完成的任务在逻辑上不需要"优先级",置顶是对未完成任务的空间优待。
completed:布尔值,"标记完成"是任务管理中最核心的状态流转。完成后的视觉变化包括:标题变灰、添加删除线、优先级标签变灰、左侧色条变灰、pin 标记消失。
2.3 排序与筛选
任务列表的展示逻辑包含两个步骤:先排序,再筛选。
排序(getSorted):置顶的任务排在前面,未置顶的排在后面。同组内的任务保持原始插入顺序不变:
getSorted(): TaskItem[] {
let result = this.tasks.slice();
result.sort((a: TaskItem, b: TaskItem) => {
if (a.pinned && !b.pinned) return -1;
if (!a.pinned && b.pinned) return 1;
return 0;
});
return result;
}
注意:Array.sort() 是稳定排序(HarmonyOS 的 JS 引擎保证),所以同组任务保持原始顺序。这对用户体验很重要——如果排序不稳定,用户会觉得"列表在跳动"。
筛选(getFiltered):在排序后的数组上应用分类筛选——"全部"返回全部,"未完成"只返回 completed === false 的,"已完成"只返回 completed === true 的。
排序和筛选都不缓存——每次调用动态计算。对于任务收件箱这种规模(通常 < 50 条),实时计算性能完全够用,不需要引入缓存的复杂性。
2.4 统计面板
三个统计数字:全部任务(总数)、未完成(红色)、已完成(绿色)。
未完成用红色数字而非常规的深色——这是一种"焦虑信号"设计。红色数字会促使用户关注待办事项,而下方的文字标签"未完成"保持了中性灰色。颜色只在数字上使用,不像整行都是红色那样有压迫感,但足够引起注意。
已完成用绿色——这是一种"完成信号",给用户正向反馈:“看,你已经做了这么多”。绿色+数字的组合也是一种隐性的成就感激励。
三、核心功能实现
3.1 切换完成状态
点击复选框或左滑"完成"按钮触发的核心操作:
toggleComplete(id: number): void {
let idx = this.findIdx(id);
if (idx < 0) return;
let item = this.copyItem(this.tasks[idx]);
item.completed = !item.completed;
item.pinned = item.completed ? false : item.pinned;
this.applyChange(idx, item);
}
关键细节:当任务被标记为完成时,自动取消置顶(pinned = false)。已完成的任务没有理由占用顶部空间——这是数据完整性的保证。当任务被"反标记"(从已完成变回未完成),置顶状态保持不变(不会自动恢复置顶)——这是一种"保守"策略,避免意外恢复用户可能已经忘记的置顶状态。
copyItem 和 applyChange 方法实现了 ArkUI 的不可变状态更新:
copyItem(src: TaskItem): TaskItem {
return {
id: src.id, title: src.title, desc: src.desc,
priority: src.priority, color: src.color,
completed: src.completed, pinned: src.pinned
};
}
applyChange(idx: number, item: TaskItem): void {
let copy = this.tasks.slice();
copy[idx] = item;
this.tasks = copy;
}
这是我们在之前文章中详细讨论过的 @State array 响应式更新模式——新数组引用 + 新对象引用 = 触发重渲染。
3.2 删除任务
左滑"删除"按钮触发:
deleteTask(id: number): void {
let result: TaskItem[] = [];
for (let i = 0; i < this.tasks.length; i++) {
if (this.tasks[i].id !== id) {
result.push(this.tasks[i]);
}
}
this.tasks = result;
}
构建一个新数组,跳过被删除的项,赋值给 this.tasks。因为生成了全新的数组引用,@State 会检测到变化并重渲染。删除后的动画由 List 组件自动处理(列表项的移除动画),无需手动添加过渡效果。
3.3 置顶切换
右滑"置顶"按钮触发:
togglePin(id: number): void {
let idx = this.findIdx(id);
if (idx < 0) return;
let item = this.copyItem(this.tasks[idx]);
if (!item.completed) {
item.pinned = !item.pinned;
this.applyChange(idx, item);
}
}
!item.completed 守卫确保已完成任务不能置顶。切换置顶后,列表会因为 getSorted() 的排序逻辑自动将该项目移到顶部(如果是置顶操作)或回到原位(如果是取消置顶)。
这是数据驱动 UI 的优势——不需要手动操作 DOM 或调整列表位置,只需改变数据的状态,视图会自动根据排序规则响应。
3.4 SwipeAction 的实现
SwipeAction 使用 @Builder 定义操作区 UI:
@Builder
endActionBuilder(item: TaskItem) {
Row() {
if (!item.completed) {
Row() {
Text('完成')
.fontSize(13)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Medium)
}
.width(80)
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#00B894')
.borderRadius({ topLeft: 10, bottomLeft: 10 })
.onClick(() => { this.toggleComplete(item.id); })
}
Row() {
Text('删除')
.fontSize(13)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Medium)
}
.width(80)
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#FF4D4F')
.borderRadius({ topRight: 10, bottomRight: 10 })
.onClick(() => { this.deleteTask(item.id); })
}
.height('100%')
}
@Builder
startActionBuilder(item: TaskItem) {
Row() {
Row() {
Text(item.pinned ? '取消置顶' : '置顶')
.fontSize(13)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Medium)
}
.width(80)
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#1677FF')
.borderRadius(10)
.onClick(() => { this.togglePin(item.id); })
}
.height('100%')
}
在 ListItem 上应用:
ListItem() {
// ... 任务内容
}
.swipeAction({
end: {
builder: () => { this.endActionBuilder(item); },
actionAreaDistance: 160
},
start: {
builder: () => { this.startActionBuilder(item); },
actionAreaDistance: 80
}
})
几个值得注意的实现细节:
条件显示"完成"按钮:如果任务已经完成了,左滑菜单只显示"删除"(隐藏"完成")。这避免了"已完成"任务出现"再完成一次"的尴尬操作。对应地,actionAreaDistance 仍保持 160vp——即使"完成"按钮隐藏了,操作区宽度不变,保持列表项行为的一致性。
按钮宽度 80vp:两个按钮各 80vp 宽,是经过权衡的选择。太窄(< 60vp)文字放不下或需要缩小字号,太宽(> 100vp)过度遮挡列表内容,且滑动距离过长。80vp 足够容纳两个中文字符和左右内边距。
圆角处理:"完成"按钮使用 borderRadius({ topLeft: 10, bottomLeft: 10 })(左侧圆角),"删除"按钮使用 borderRadius({ topRight: 10, bottomRight: 10 })(右侧圆角),两个按钮拼在一起形成一个完整的圆角矩形。右侧的"置顶"按钮使用全圆角 borderRadius(10)。
按钮文字动态切换:置顶按钮根据当前状态显示"置顶"或"取消置顶",给予用户明确的操作预期。
3.5 复选框
除了 SwipeAction,任务还提供了点击复选框来切换完成状态:
Row() {
if (item.completed) {
Text('✓')
.fontSize(14)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
}
}
.width(22).height(22)
.justifyContent(FlexAlign.Center)
.borderRadius(11)
.border({ width: 2, color: item.completed ? '#00B894' : '#CCCCDD' })
.backgroundColor(item.completed ? '#00B894' : '#00000000')
.margin({ left: 12 })
.onClick(() => { this.toggleComplete(item.id); })
复选框的设计:
- 未完成:22x22 空心圆,灰色边框(
#CCCCDD),无背景色,无文字 - 已完成:22x22 实心圆,绿色背景(
#00B894),白色对勾 ✓
空心→实心的视觉转变明确、直接,不需要额外的动画。点击区域为 22x22+padding,符合 iOS 和 Material Design 推荐的最小触摸目标(44pt)。
四、完整交互流程
4.1 初始状态
页面加载后展示 9 条任务,涵盖三种优先级和两种完成状态。"准备技术分享"和"完成季度报告"两条任务带有 📌 置顶标记,排在列表顶部。
统计面板显示:全部任务 9、未完成 7、已完成 2。
4.2 左滑标记完成
在"修复登录模块 Bug"上左滑,展现绿色"完成"按钮和红色"删除"按钮。点击"完成"——
- 该任务左侧色条由红色变为灰色(
#CCCCCC) - 优先级标签由红色变为灰色
- 标题文字变灰并添加删除线
- 复选框由空心变为绿色实心对勾
- 统计面板:未完成 6、已完成 3
再次左滑该任务,只有"删除"按钮,不再显示"完成"(因为已经完成了)。
4.3 左滑删除
在"整理技术文档"上左滑,点击红色"删除"按钮。该任务从列表中移除。统计面板更新。
4.4 右滑置顶
在"对接第三方推送 SDK"上右滑,展现蓝色"置顶"按钮。点击——该任务立即跳到列表顶部(排在已有置顶项后面),📌 图标出现。
4.5 点击复选框
点击"更新 API 文档"左侧的复选框,直接切换完成/未完成(无需滑动)。这是滑动之外的另一条操作路径,适用于"快速勾掉多个任务"的场景。
4.6 筛选
点击筛选栏的"已完成",列表只显示已完成的任务。点击"未完成",只显示未完成的任务。切换回"全部"恢复完整列表。空状态时显示对应提示文字。
4.7 添加任务
点击标题栏的"添加任务",一条新任务追加到列表底部(优先级根据已有数量自动轮换红/橙/绿)。新任务可被正常滑动、勾选、置顶和删除。
五、SwipeAction 的设计指南
5.1 操作按钮的数量
每个方向(start/end)最多放 2 个按钮。超过 2 个会让操作区过宽(> 200vp),滑动距离过长,且用户在一瞥中难以快速抉择"点哪个"。如果确实需要 3 个以上的操作,考虑:
- 仅保留 2 个最常用的,其余放进"更多"菜单
- 让用户自定义常用操作
- 使用"长按"触发更多操作菜单
本文的 end 侧仅放 2 个按钮(完成 + 删除),start 侧仅 1 个(置顶),都是"扫一眼就能决定"的数量。
5.2 操作按钮的颜色
颜色是滑动操作的"情感编码",需要与操作的含义匹配:
- 红色系(#FF4D4F):删除、取消、拒绝——破坏性操作
- 绿色系(#00B894):完成、通过、确认——积极操作
- 蓝色系(#1677FF):标记、置顶、编辑——中性操作
- 灰色系(#9999AA):归档、隐藏——弱操作
- 橙色系(#E09F3E):提醒、稍后——延迟操作
永远不要在滑动操作中使用含义模糊的颜色(如紫色配删除、绿色配取消),这会造成认知失调。
5.3 操作按钮应有明确文字标注
仅依赖图标(如 🗑 = 删除)不够直观——不同文化背景的用户对图标的解读不同。永远在滑动操作按钮上同时使用图标和文字,或纯文字。本文选择纯文字方案,确保零歧义。
5.4 操作后是否自动关闭
SwipeAction 在点击按钮后是否自动收回列表项?答案是:取决于操作的性质。
- 如果操作会改变列表内容(删除、移动),SwipeAction 会自动关闭(因为 ListItem 已在列表中消失或重排)。
- 如果操作不改变列表内容(如"标记已读"但列表项仍在原位置),ArkUI 默认不会自动关闭 SwipeAction。我们需要考虑是否手动关闭。
在本文的实现中,所有操作都改变了数据状态(完成/删除/置顶 = 触发重渲染),列表项会在 @State 更新后重绘,SwipeAction 自然关闭。这是利用框架行为的干净方案。
如果需要在某些场景下手动关闭 SwipeAction,可以通过给 ListItem 的 key 强制变化来触发重绘(例如在操作后改变一个 counter 并作为 key 的一部分)。
5.5 反馈确认
对于破坏性操作(删除),在 SwipeAction 中直接执行删除通常是可以接受的——因为滑动手势本身已经是一种"确认"行为。用户在滑动时已经看到了红色的"删除"按钮,再点击它需要明确的意图。这与"在工具栏上按删除 — 弹确认框"的模式不同。
但如果删除的数据是不可恢复的(如删除账号、删除支付记录),还是应该在 SwipeAction 按钮点击后弹出二次确认。本文的 Demo 中的任务删除是可接受的无确认删除,因为:
- 任务数据是临时性的 Demo 数据
- 用户可以通过"添加任务"重新创建
在实际产品中,建议提供撤销能力(如底部 SnackBar “任务已删除,点击撤销”),这比弹确认框的用户体验更好。
5.6 适配暗黑模式
SwipeAction 的操作区按钮是自定义的 @Builder,不会自动适配暗黑模式。如果在暗黑模式下,按钮背景色需要调整——例如,红色删除按钮在暗黑模式下应使用稍暗的红色(#D9363E 而非 #FF4D4F),避免在黑色背景上过于刺眼。
六、进阶扩展方向
6.1 撤销删除
结合系统 SnackBar 或自定义 Toast 实现删除撤销:
deleteTask(id: number): void {
let deleted = this.tasks[this.findIdx(id)];
// 执行删除
// ...
// 显示撤销提示
this.showUndo(deleted);
}
showUndo(item: TaskItem): void {
// 显示底部 SnackBar:"「{item.title}」已删除 撤销"
// 点击"撤销" → 将 item 重新插入原位置
// 3 秒后自动消失
}
6.2 拖拽排序
结合 ListItem 的拖拽能力(.onDragStart、.onDrop),允许用户通过长按拖拽手动调整任务顺序。拖拽排序与置顶排序互不冲突——拖拽设置的是"手动顺序",置顶是额外的"优先级加权"。
6.3 分组视图
对于大量任务(> 20 条),引入分组维度——按项目、按优先级、按截止日期分组。SwipeAction 在每个分组内独立工作。
6.4 多选批量操作
长按进入多选模式,批量完成、批量删除、批量设置优先级。多选模式下,SwipeAction 应暂时禁用——列表项的手势区域被复选框占据,避免滑动操作与选择操作冲突。
6.5 自定义滑动阈值
虽然 actionAreaDistance 设置了操作区的宽度,但实际触发滑动需要用户滑动到什么程度?ArkUI 使用一个内置的阈值(通常是操作区宽度的 30%),超过后自动弹开到完整宽度。如果需要调整阈值,目前 API 不直接支持,但可以通过组合手势(.gesture() + .swipeAction())实现更精细的控制。
七、总结
本文通过"任务收件箱"这个实战案例,全面讲解了 SwipeAction 滑动操作组件的设计原理和实现细节。核心知识点包括:
- SwipeAction API:start/end 两侧、builder 构建器、actionAreaDistance 宽度控制
- 两侧操作的设计哲学:左滑(end)= 最常用操作(完成/删除),右滑(start)= 次要操作(置顶)
- 操作按钮的视觉设计:颜色语义编码(绿=完成、红=删除、蓝=置顶)、宽度权衡、圆角处理
- 状态管理:不可变更新(slice + copyItem + applyChange)、任务/完成/置顶状态流转
- 排序与筛选:置顶优先排序 + 分类筛选 + 统计面板
- SwipeAction 的可用性设计指南:按钮数量 ≤ 2、颜色与操作匹配、文字标注、撤销机制
SwipeAction 的魅力在于它符合直觉——滑动是人最自然的手势之一,将它用于任务管理是"顺势而为"。但好的滑动交互不止于"能滑动",更在于:在正确的方向放置正确的操作、用正确的颜色传递正确的情感、在正确的时机给用户正确的反馈。三个"正确"叠加,才能从"功能完善"做到"体验舒适"。
更多推荐


所有评论(0)