项目演示



一、概述
1.1 侧滑菜单的应用场景
在移动应用开发中,侧滑菜单(Swipe Menu)是一种非常常见的交互模式,用户可以通过左右滑动列表项来显示隐藏的操作按钮。这种交互模式在以下场景中尤为常见:
- 联系人管理:左滑删除联系人、置顶联系人
- 消息列表:左滑标记已读、删除消息
- 待办事项:左滑完成任务、删除任务
- 商品列表:左滑加入收藏、查看详情
侧滑菜单的优势在于:
- 节省屏幕空间:将次要操作隐藏在列表项后面
- 操作直观:用户通过滑动手势即可触发操作
- 交互流畅:配合动画效果提供良好的用户体验
1.2 鸿蒙ArkTS实现方案
在鸿蒙HarmonyOS NEXT开发中,官方提供了标准的侧滑菜单实现方案------List + ListItem + swipeAction。这套方案具有以下特点:
- 原生支持:ArkUI框架内置支持,无需第三方库
- 性能优异:基于声明式UI和虚拟滚动机制
- 功能完善:支持左右侧滑、滑动阈值、状态监听等
- API稳定:从API Level 9开始支持,持续迭代优化
1.3 本文目标
本文将深入探讨鸿蒙ArkTS中List+swipeAction侧滑菜单的实现原理和最佳实践,内容涵盖:
- List和ListItem组件的核心概念
- swipeAction属性的详细配置
- 完整的代码示例和运行效果
- 高级用法和性能优化策略
- API Level 24新增特性
二、核心组件概述
2.1 List组件
List是鸿蒙ArkUI中用于展示列表数据的核心容器组件,具有以下特性:
2.1.1 基本接口
typescript
List(value?: { space?: number | string; initialIndex?: number; scroller?: Scroller })
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| space | number | string | 否 | 子组件主轴方向的间隔,单位vp |
| initialIndex | number | 否 | 初次加载时视口起始位置显示的item索引 |
| scroller | Scroller | 否 | 滚动控制器,用于控制列表滚动行为 |
2.1.2 核心属性
| 属性名 | 说明 | API Level |
|---|---|---|
| listDirection | 设置列表排列方向(垂直/水平) | 7+ |
| divider | 设置列表项分割线样式 | 7+ |
| scrollBar | 设置滚动条显示模式 | 7+ |
| cachedCount | 设置列表缓存数量,优化滚动性能 | 14+ |
| chainAnimation | 设置链式滚动动画效果 | 10+ |
| multiSelectable | 设置是否支持多选 | 8+ |
2.1.3 布局原理
List组件采用虚拟滚动机制,只渲染当前视口可见的列表项,当列表项滚出视口时会被销毁或缓存,从而大幅提升长列表的性能表现。
2.2 ListItem组件
ListItem是List的子组件,用于定义每个列表项的内容和交互行为。
2.2.1 基本接口
typescript
ListItem(value?: ListItemOptions)
2.2.2 核心属性
| 属性名 | 说明 | API Level |
|---|---|---|
| selectable | 设置列表项是否可选中 | 8+ |
| selected | 设置列表项选中状态 | 10+ |
| swipeAction | 设置侧滑菜单配置 | 9+ |
2.2.3 侧滑相关特性
ListItem的侧滑功能主要通过swipeAction属性实现,支持以下特性:
- 左侧滑出(start)和右侧滑出(end)两种模式
- 自定义滑动阈值和触发区域
- 滑动状态监听(进入/退出操作区域)
- 滑动触发动作(滑动超过阈值后自动执行)
三、swipeAction属性详解
3.1 swipeAction接口定义
typescript
swipeAction(value: SwipeActionOptions)
其中SwipeActionOptions对象包含以下字段:
typescript
interface SwipeActionOptions {
start?: SwipeActionItem; // 左侧滑出配置
end?: SwipeActionItem; // 右侧滑出配置
}
3.2 SwipeActionItem配置
SwipeActionItem用于配置单侧滑出的具体行为:
typescript
interface SwipeActionItem {
builder: CustomBuilder; // 必填,划出组件的UI构造器
onAction?: () => void; // 滑动触发动作
actionAreaDistance?: number; // 滑动阈值,单位vp
onEnterActionArea?: () => void; // 进入有效操作区域时触发
onExitActionArea?: () => void; // 退出有效操作区域时触发
}
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| builder | CustomBuilder | 是 | 侧滑时显示的组件构造器,通常使用@Builder定义 |
| onAction | () => void | 否 | 滑动超过阈值后自动执行的回调函数 |
| actionAreaDistance | number | 否 | 触发onAction的滑动距离阈值,默认值根据设备计算 |
| onEnterActionArea | () => void | 否 | 手指滑动进入有效操作区域时触发 |
| onExitActionArea | () => void | 否 | 手指滑动退出有效操作区域时触发 |
3.3 swipeAction使用方式
swipeAction支持两种使用方式:
方式一:直接传入Builder引用(推荐)
typescript
.swipeAction({
end: this.SwipeButtons(item)
})
方式二:传入完整配置对象
typescript
.swipeAction({
end: {
builder: () => {
this.SwipeButtons(item);
},
onAction: () => {
// 滑动超过阈值时执行
this.deleteItem(item.id);
},
actionAreaDistance: 80,
onEnterActionArea: () => {
console.info('进入操作区域');
},
onExitActionArea: () => {
console.info('退出操作区域');
}
}
})
四、完整代码示例
4.1 项目结构
MyApplication24/
├── entry/
│ ├── src/
│ │ └── main/
│ │ ├── ets/
│ │ │ ├── entryability/
│ │ │ │ └── EntryAbility.ets
│ │ │ └── pages/
│ │ │ └── Index.ets
│ │ └── resources/
│ │ └── base/
│ │ ├── element/
│ │ │ ├── color.json
│ │ │ └── float.json
│ │ └── media/
│ │ └── startIcon.png
│ ├── build-profile.json5
│ ├── hvigorfile.ts
│ └── oh-package.json5
├── AppScope/
│ └── resources/
│ └── base/
│ └── element/
│ └── string.json
├── build-profile.json5
└── hvigorfile.ts
4.2 核心代码实现
typescript
@Entry
@Component
struct Index {
@State contactList: Array<Contact> = [
new Contact(1, '张三', '13800138001', '张'),
new Contact(2, '李四', '13800138002', '李'),
new Contact(3, '王五', '13800138003', '王'),
new Contact(4, '赵六', '13800138004', '赵'),
new Contact(5, '钱七', '13800138005', '钱'),
new Contact(6, '孙八', '13800138006', '孙'),
new Contact(7, '周九', '13800138007', '周'),
new Contact(8, '吴十', '13800138008', '吴')
];
deleteContact(id: number): void {
for (let i = 0; i < this.contactList.length; i++) {
if (this.contactList[i].id === id) {
this.contactList.splice(i, 1);
break;
}
}
}
pinContact(id: number): void {
let targetItem: Contact | null = null;
let targetIndex: number = -1;
for (let i = 0; i < this.contactList.length; i++) {
if (this.contactList[i].id === id) {
targetItem = this.contactList[i];
targetIndex = i;
break;
}
}
if (targetItem !== null && targetIndex > 0) {
this.contactList.splice(targetIndex, 1);
this.contactList.unshift(targetItem);
}
}
build() {
Column() {
Text('联系人列表')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin({ top: 40, bottom: 20 })
.width('100%')
.textAlign(TextAlign.Center);
Text('左滑查看操作按钮')
.fontSize(20)
.fontColor('#999999')
.margin({ bottom: 10 })
.width('100%')
.textAlign(TextAlign.Center);
List({ space: 10 }) {
ForEach(this.contactList, (item: Contact) => {
ListItem() {
Row() {
Text(item.avatar)
.width(60)
.height(60)
.borderRadius(30)
.backgroundColor('#4CAF50')
.fontColor('#FFFFFF')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.textAlign(TextAlign.Center);
Column() {
Text(item.name)
.fontSize(24)
.fontWeight(FontWeight.Medium);
Text(item.phone)
.fontSize(20)
.fontColor('#666666')
.margin({ top: 5 });
}
.margin({ left: 15 })
.alignItems(HorizontalAlign.Start);
Text('>')
.fontSize(28)
.fontColor('#CCCCCC')
.margin({ left: 15 });
}
.width('100%')
.height(80)
.padding({ left: 20, right: 20 })
.alignItems(VerticalAlign.Center)
.backgroundColor('#FFFFFF');
}
.swipeAction({
end: this.SwipeButtons(item)
});
}, (item: Contact) => item.id.toString());
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5');
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5');
}
@Builder
SwipeButtons(item: Contact) {
Row() {
Button('置顶')
.width(80)
.height(80)
.backgroundColor('#FF9800')
.fontColor('#FFFFFF')
.fontSize(22)
.borderRadius(0)
.onClick(() => {
this.pinContact(item.id);
});
Button('删除')
.width(80)
.height(80)
.backgroundColor('#F44336')
.fontColor('#FFFFFF')
.fontSize(22)
.borderRadius(0)
.onClick(() => {
this.deleteContact(item.id);
});
}
.width('auto')
.height(80);
}
}
class Contact {
id: number = 0;
name: string = '';
phone: string = '';
avatar: string = '';
constructor(id: number, name: string, phone: string, avatar: string) {
this.id = id;
this.name = name;
this.phone = phone;
this.avatar = avatar;
}
}
4.3 代码解析
4.3.1 数据模型设计
使用Contact类定义联系人数据结构:
typescript
class Contact {
id: number = 0;
name: string = '';
phone: string = '';
avatar: string = '';
constructor(id: number, name: string, phone: string, avatar: string) {
this.id = id;
this.name = name;
this.phone = phone;
this.avatar = avatar;
}
}
设计要点:
- 使用类而非匿名对象,符合ArkTS语法规范
- 字段初始化为默认值,避免类型推断问题
- 提供构造函数,便于创建实例
4.3.2 状态管理
使用@State装饰器管理列表数据:
typescript
@State contactList: Array<Contact> = [...]
状态管理原理:
@State标记的变量是组件内部状态- 状态变化时会自动触发UI更新
- 列表数据更新后,ForEach会自动重新渲染受影响的列表项
4.3.3 删除功能实现
typescript
deleteContact(id: number): void {
for (let i = 0; i < this.contactList.length; i++) {
if (this.contactList[i].id === id) {
this.contactList.splice(i, 1);
break;
}
}
}
实现要点:
- 通过id定位要删除的元素
- 使用
splice方法从数组中移除元素 @State会检测到数组变化并触发UI更新
4.3.4 置顶功能实现
typescript
pinContact(id: number): void {
let targetItem: Contact | null = null;
let targetIndex: number = -1;
for (let i = 0; i < this.contactList.length; i++) {
if (this.contactList[i].id === id) {
targetItem = this.contactList[i];
targetIndex = i;
break;
}
}
if (targetItem !== null && targetIndex > 0) {
this.contactList.splice(targetIndex, 1);
this.contactList.unshift(targetItem);
}
}
实现要点:
- 先找到目标元素及其索引
- 从原位置移除元素
- 使用
unshift方法将元素添加到数组开头
4.3.5 列表渲染
typescript
List({ space: 10 }) {
ForEach(this.contactList, (item: Contact) => {
ListItem() {
// 列表项内容
}
.swipeAction({
end: this.SwipeButtons(item)
});
}, (item: Contact) => item.id.toString());
}
渲染要点:
List({ space: 10 })设置列表项间距为10vpForEach遍历数组,第一个参数是数据源,第二个参数是生成器函数,第三个参数是唯一标识符ListItem定义每个列表项的内容和侧滑配置
4.3.6 侧滑按钮构建
typescript
@Builder
SwipeButtons(item: Contact) {
Row() {
Button('置顶')
.width(80)
.height(80)
.backgroundColor('#FF9800')
.fontColor('#FFFFFF')
.fontSize(22)
.borderRadius(0)
.onClick(() => {
this.pinContact(item.id);
});
Button('删除')
.width(80)
.height(80)
.backgroundColor('#F44336')
.fontColor('#FFFFFF')
.fontSize(22)
.borderRadius(0)
.onClick(() => {
this.deleteContact(item.id);
});
}
.width('auto')
.height(80);
}
Builder设计要点:
- 使用
@Builder装饰器定义可复用的UI片段 - 参数直接传递,不使用解构赋值(ArkTS语法约束)
- 按钮高度与列表项高度一致,确保视觉对齐
- 圆角设置为0,避免与列表项边框冲突
五、运行效果展示
5.1 默认状态
应用启动后,显示联系人列表,每个列表项包含:
- 圆形头像(显示姓氏)
- 联系人姓名
- 联系电话
- 右侧箭头指示(提示可滑动)
5.2 侧滑操作
当用户左滑列表项时:
- 列表项向右滑动
- 右侧显示"置顶"和"删除"两个操作按钮
- "置顶"按钮为橙色(#FF9800)
- "删除"按钮为红色(#F44336)
5.3 操作反馈
- 点击置顶:联系人移动到列表首位,带有平滑的动画过渡
- 点击删除:联系人从列表中移除,带有平滑的动画过渡
六、高级用法
6.1 双向侧滑
同时配置start和end实现双向侧滑:
typescript
.swipeAction({
start: {
builder: () => {
Row() {
Button('标记')
.width(80)
.height(80)
.backgroundColor('#2196F3')
.fontColor('#FFFFFF');
}
},
onAction: () => {
console.info('左滑触发');
}
},
end: {
builder: () => {
Row() {
Button('删除')
.width(80)
.height(80)
.backgroundColor('#F44336')
.fontColor('#FFFFFF');
}
},
onAction: () => {
console.info('右滑触发');
}
}
})
6.2 滑动阈值控制
通过actionAreaDistance精确控制滑动触发阈值:
typescript
.swipeAction({
end: {
builder: () => {
this.SwipeButtons(item);
},
actionAreaDistance: 100, // 滑动100vp后触发onAction
onAction: () => {
this.deleteContact(item.id);
}
}
})
6.3 滑动状态监听
监听滑动进入/退出操作区域的事件:
typescript
@State isInActionArea: boolean = false;
.swipeAction({
end: {
builder: () => {
this.SwipeButtons(item);
},
onEnterActionArea: () => {
this.isInActionArea = true;
console.info('进入操作区域');
},
onExitActionArea: () => {
this.isInActionArea = false;
console.info('退出操作区域');
}
}
})
6.4 控制侧滑展开与收起
从API Level 21开始,可以通过ListItemSwipeActionManager控制侧滑状态:
typescript
@State swipeActionManager: ListItemSwipeActionManager = new ListItemSwipeActionManager();
ListItem() {
// 列表项内容
}
.swipeAction({
end: this.SwipeButtons(item),
manager: this.swipeActionManager
});
// 在需要的地方调用
this.swipeActionManager.expand(ListItemSwipeActionDirection.END); // 展开
this.swipeActionManager.collapse(); // 收起
6.5 列表滚动控制
使用ListScroller控制列表滚动和侧滑状态:
typescript
private scroller: ListScroller = new ListScroller();
List({ space: 10, scroller: this.scroller }) {
// 列表内容
}
// 关闭所有侧滑菜单
this.scroller.closeAllSwipeActions();
七、性能优化策略
7.1 虚拟滚动优化
List组件默认启用虚拟滚动,只渲染可见区域的列表项。为进一步优化性能:
typescript
List({ space: 10 }) {
// 列表内容
}
.cachedCount(5) // 设置缓存数量
cachedCount参数控制预加载的列表项数量,适当增加可以减少快速滚动时的空白闪烁。
7.2 避免复杂布局
侧滑菜单中的组件应尽量简单,避免嵌套过深或使用复杂的布局组件:
typescript
// 推荐:简单的Row布局
@Builder
SwipeButtons(item: Contact) {
Row() {
Button('置顶').width(80).height(80);
Button('删除').width(80).height(80);
}
}
// 不推荐:复杂嵌套
@Builder
SwipeButtons(item: Contact) {
Column() {
Row() {
Column() {
Button('置顶');
}
}
}
}
7.3 动画优化
使用animateTo实现平滑的删除动画:
typescript
deleteContact(id: number): void {
this.getUIContext()?.animateTo({ duration: 300 }, () => {
for (let i = 0; i < this.contactList.length; i++) {
if (this.contactList[i].id === id) {
this.contactList.splice(i, 1);
break;
}
}
});
}
7.4 避免频繁状态更新
在侧滑操作中,避免在onEnterActionArea和onExitActionArea回调中进行频繁的状态更新:
typescript
// 不推荐
onEnterActionArea: () => {
this.updateState(); // 频繁调用会导致UI抖动
}
// 推荐:使用防抖或节流
八、API Level 24新增特性
8.1 新增属性和方法
HarmonyOS NEXT API Level 24在List和ListItem组件中新增了以下特性:
8.1.1 List组件新增
| 属性/方法 | 说明 |
|---|---|
editModeOptions |
编辑模式配置选项(API 23+) |
supportEmptyBranchInLazyLoading |
支持懒加载中的空分支(API 23+) |
8.1.2 ListItem组件新增
| 属性/方法 | 说明 |
|---|---|
ListItemSwipeActionManager |
侧滑菜单管理器(API 21+) |
expand |
展开侧滑菜单(API 21+) |
collapse |
收起侧滑菜单(API 21+) |
8.2 兼容性处理
在使用不同API Level的特性时,需要进行兼容性处理:
typescript
if (apiVersion >= 21) {
// 使用ListItemSwipeActionManager
this.swipeActionManager.expand(ListItemSwipeActionDirection.END);
} else {
// 降级处理
console.info('当前API版本不支持程序化控制侧滑');
}
九、常见问题与解决方案
9.1 侧滑菜单不显示
问题描述:滑动列表项时,侧滑菜单不显示。
解决方案:
- 检查
swipeAction配置是否正确 - 确保
@Builder方法参数传递正确(ArkTS不支持解构赋值) - 检查按钮高度是否与列表项高度匹配
typescript
// 正确:直接传递参数
@Builder
SwipeButtons(item: Contact) {
// ...
}
// 错误:使用解构赋值(ArkTS不支持)
@Builder
SwipeButtons({ item }: { item: Contact }) {
// ...
}
9.2 列表项数据类型报错
问题描述:使用匿名对象类型定义列表数据时报错。
解决方案:使用显式的类定义数据结构。
typescript
// 正确:使用类定义
class Contact {
id: number = 0;
name: string = '';
}
@State contactList: Array<Contact> = [new Contact()];
// 错误:使用匿名对象类型(ArkTS不支持)
@State contactList: Array<{ id: number; name: string }> = [];
9.3 删除操作后UI不更新
问题描述 :调用deleteContact方法后,列表UI没有更新。
解决方案:
- 确保使用
@State装饰器标记数据 - 使用
splice方法修改数组(直接赋值不会触发更新) - 确保数据类型正确(避免使用
any或unknown)
typescript
// 正确:使用splice修改数组
this.contactList.splice(index, 1);
// 错误:直接赋值不会触发响应式更新
this.contactList = newList;
9.4 侧滑按钮点击事件不生效
问题描述:侧滑菜单中的按钮点击事件没有响应。
解决方案:
- 确保按钮绑定了正确的
onClick事件 - 检查按钮是否被其他组件遮挡
- 确保按钮尺寸足够大(推荐最小44vp)
9.5 列表性能问题
问题描述:列表滚动时出现卡顿或闪烁。
解决方案:
- 设置合适的
cachedCount值 - 简化列表项布局结构
- 使用
LazyForEach替代ForEach(大数据量场景)
十、最佳实践总结
10.1 代码组织规范
- 数据模型独立:将数据结构定义为单独的类,便于维护和复用
- 状态集中管理 :使用
@State管理组件内部状态,避免状态分散 - UI逻辑分离 :将侧滑按钮布局抽取为
@Builder方法,提高代码可读性 - 方法职责单一:每个方法只负责一个功能,便于测试和调试
10.2 交互设计原则
- 操作反馈明确:侧滑按钮点击后应有明显的视觉反馈
- 滑动体验流畅:使用系统默认的滑动动画,保持交互一致性
- 操作可逆:重要操作(如删除)应提供确认机制或撤销功能
- 视觉层次清晰:侧滑按钮颜色应与列表项形成对比,便于识别
10.3 性能优化要点
- 虚拟滚动:利用List组件的虚拟滚动机制,优化长列表性能
- 缓存策略 :合理设置
cachedCount,平衡内存占用和滚动流畅度 - 避免重绘:减少不必要的状态更新,避免频繁触发UI重绘
- 组件复用 :使用
@Builder复用侧滑按钮布局,减少代码冗余
10.4 兼容性考虑
- API版本检测:使用新特性时,先检测API版本并提供降级方案
- 设备适配:考虑不同屏幕尺寸和分辨率的适配
- 交互适配:针对不同设备(手机、平板、车机)优化交互方式
十一、总结
11.1 核心要点回顾
本文详细介绍了鸿蒙ArkTS中List+swipeAction侧滑菜单的实现方案,核心要点包括:
- 组件协作:List作为容器,ListItem作为列表项,swipeAction提供侧滑能力
- 配置灵活:支持左右侧滑、滑动阈值、状态监听等多种配置
- 性能优异:基于虚拟滚动和声明式UI,提供流畅的交互体验
- API稳定:从API Level 9开始支持,持续迭代优化
11.2 实践建议
在实际开发中,建议:
- 先理解官方API文档,掌握swipeAction的完整配置选项
- 从简单示例开始,逐步扩展功能
- 关注性能优化,特别是长列表场景
- 遵循鸿蒙设计规范,保持交互一致性
11.3 未来展望
随着HarmonyOS NEXT的持续演进,侧滑菜单功能将更加完善:
- 更多的滑动动画效果
- 更精细的交互控制
- 更好的跨设备适配
- 更强的性能优化
通过掌握List+swipeAction侧滑菜单的实现方法,开发者可以为用户提供更加丰富和流畅的列表交互体验。