鸿蒙原生 ArkTS 布局之 List 编辑模式深度解析:多选 / 单选 / 拖拽排序


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、引言

在移动端应用中,列表是最基础也最复杂的信息展示容器。用户对列表的诉求早已超越"看"——他们需要批量操作(多选删除)、确认决策(单选下单)、个性化排序(拖拽调整)。HarmonyOS NEXT 的 ArkUI 框架为 List 组件提供了完善的编辑模式支持,通过一组简洁的声明式 API,开发者可以在不依赖第三方库的前提下,快速实现上述三种高频交互场景。

本文将从零开始,带领你构建一个完整的 List 编辑模式演示应用,涵盖 多选批量操作单选方案确认长按拖拽排序 三大模块,并提供详尽的 ArkTS 源码与逐行注释。


二、环境与准备工作

2.1 开发环境

项目 版本
操作系统 Windows 10 / 11
DevEco Studio 5.0+ (HarmonyOS NEXT)
SDK API 24 (HarmonyOS NEXT Release)
构建工具 hvigor
语言 ArkTS(声明式 UI + 强类型)

2.2 项目结构

entry/src/main/ets/pages/
├── Index.ets           # 首页 — 导航入口
└── ListEditDemo.ets    # 主演示页面 — 三大编辑模式

路由注册文件:

entry/src/main/resources/base/profile/main_pages.json

三、List 编辑模式核心 API 总览

在 HarmonyOS NEXT(API 24)中,List 编辑模式涉及以下关键属性与回调:

API 作用对象 用途
.editMode(editing: boolean) List 进入 / 退出编辑模式
.onMove((from, to) => void) ForEach / LazyForEach 拖拽排序回调(自 API 12 支持)
.selectable(boolean) ListItem 是否允许被选中(编辑模式下生效)
.onSelect((isSelected) => void) ListItem 选中状态变化监听
.selected(boolean) ListItem 受控选中属性(配合 $$ 双向绑定)

⚠️ 版本说明editMode 在 API 24 中标记为 deprecated,但仍是当前最简洁的编辑模式入口。替代方案是使用 multiSelectable + 自定义选中样式,但 editMode 在可预见的版本中仍会保持兼容,推荐新项目继续使用。


四、数据模型设计

在 ArkTS 中,类定义必须遵循严格规则:不能在构造函数参数中声明字段,必须显式在类体内声明。

class ListItemData {
  id: number = 0;
  name: string = '';
  desc: string = '';

  constructor(id: number, name: string, desc: string) {
    this.id = id;
    this.name = name;
    this.desc = desc;
  }
}

这种写法的优势在于:

  • 类型安全:每个字段的类型一目了然
  • 序列化友好:JSON.parse 后的对象可以安全转换为该类
  • 状态追踪:@State 装饰器能精准感知字段级别的变化

五、多选模式(批量操作)深度拆解

5.1 交互逻辑

多选模式的核心需求是:

  1. 开启编辑模式后,每个列表项变为可点击选中
  2. 选中项通过视觉反馈(高亮 + 标记)区分
  3. 顶部工具栏提供 全选 / 清空 / 删除选中 等批量操作
  4. 退出编辑模式时自动清空选中状态

5.2 状态管理

@State isMultiEdit: boolean = false;           // 编辑模式开关
@State multiSelectedIds: Set<number> = new Set(); // 选中项 id 集合

选用 Set<number> 而非 number[] 的原因:

  • 去重:即使 onSelect 重复触发也不会重复添加
  • 性能:has() / add() / delete() 均为 O(1)
  • 语义清晰:数学集合操作(交集、差集)天然适合"全选 / 反选"场景

5.3 List 配置

List({ space: 8 }) {
  ForEach(this.multiList, (item: ListItemData) => {
    ListItem() {
      this.MultiItemCard(item, this.multiSelectedIds.has(item.id))
    }
    .selectable(true)   // ← 关键:允许选中
    .onSelect((isSelected: boolean) => {
      // ← 选中回调
      const newSet = new Set(this.multiSelectedIds);
      if (isSelected) {
        newSet.add(item.id);
      } else {
        newSet.delete(item.id);
      }
      this.multiSelectedIds = newSet;
    })
  }, (item: ListItemData) => item.id.toString())
}
.editMode(this.isMultiEdit)   // ← 编辑模式入口

5.4 踩坑记录

坑点 原因 解决方案
onSelect 不在 List 上面 API 24 中 onSelectListItem 的属性,而非 List 每个 ListItem 单独绑定
编辑模式下 ListItem 点击不响应 selectable 默认为 false 显式设置 .selectable(true)
@State Set 修改后 UI 不刷新 ArkTS 的 @StateSet 的检测有限 每次创建新 Set 赋值

5.5 批量删除实现

this.multiList = this.multiList.filter(
  item => !this.multiSelectedIds.has(item.id)
);
this.multiSelectedIds = new Set();

这里的关键细节是:先过滤数组,再清空 Set。如果顺序反了(先清空 Set 再过滤),会导致所有元素都被保留,删除操作失效。


六、单选模式(方案选择)深度拆解

6.1 交互逻辑

单选模式更贴近"表单确认"场景:

  1. 开启编辑模式后,点击某个选项即选中
  2. 选中另一项时,前一项自动取消选中(互斥)
  3. 底部显示当前选中的方案名称与描述
  4. 退出编辑模式时重置选中状态

6.2 受控选中属性

ListItem()
  .selectable(true)
  .selected(item.id === this.singleSelectedId)  // ← 受控属性
  .onSelect((isSelected: boolean) => {
    if (isSelected) {
      this.singleSelectedId = item.id;   // 选中当前
    } else {
      this.singleSelectedId = -1;        // 取消
    }
  })

.selected(boolean) 是一个受控属性,类似 Web 开发中 <input checked={condition} /> 的模式。它的优势在于:

  • 数据驱动 UI:状态完全由 singleSelectedId 决定
  • 可预测性:无论用户如何操作,UI 状态始终与数据源同步
  • 配合 $$ 双向绑定$$this.singleSelectedId 可实现自动同步

6.3 构建方法中的表达式限制

ArkTS 的 build() 方法有一个严格的语法约束:内部只能放置 UI 组件声明,不能包含赋值语句、函数调用等非 UI 表达式。

错误写法:

build() {
  Column() {
    if (condition) {
      const item = this.list.find(...); // ← 编译错误!
      Text(item.name);
    }
  }
}

正确写法(getter 提取):

get selectedSingleText(): string {
  const item = this.singleList.find(v => v.id === this.singleSelectedId);
  return item ? `已选方案:${item.name}${item.desc}` : '';
}

build() {
  Column() {
    if (this.isSingleEdit && this.singleSelectedId >= 0) {
      Text(this.selectedSingleText); // ← 只引用 getter
    }
  }
}

这个约束看似麻烦,实则强制开发者将逻辑层与 UI 层分离,是 ArkTS 声明式编程的最佳实践。


七、拖拽排序模式(长按拖动)深度拆解

7.1 交互逻辑

拖拽排序是最能体现"原生体验"的交互之一:

  1. 开启编辑模式后,列表项右下角出现拖拽把手图标
  2. 长按任意列表项,该条目浮起并跟随手指移动
  3. 拖动到目标位置时,其他条目自动让位(插入动画)
  4. 松手后数据完成重排,UI 同步刷新

7.2 onMove 的正确使用方式

这是整篇文章最容易踩坑的地方。在 API 24 中,onMove 不是 List 的属性,而是 ForEach 的属性!

List({ space: 8 }) {
  ForEach(this.dragList, (item, index) => {
    ListItem() {
      this.DragItemCard(item, (index ?? 0) + 1)
    }
    .selectable(false)
  }, (item) => item.id.toString())
  .onMove((from: number, to: number) => {   // ← 链式在 ForEach 上!
    const movedItem = this.dragList.splice(from, 1)[0];
    this.dragList.splice(to, 0, movedItem);
  })
}
.editMode(this.isDragEdit)
为什么是 ForEach 而不是 List?

这是 HarmonyOS 框架的设计决策:移动(move)操作是数据层的语义,而非视图层的语义。ForEach 负责数据迭代,它知道每个 item 的索引;List 负责布局展示,它不应该关心数据如何排列。将 onMove 放在 ForEach 上,语义更清晰,也便于配合 LazyForEach 做大数据集的增量更新。

splice 的两步操作详解
const movedItem = this.dragList.splice(from, 1)[0];  // ① 从原位置移除
this.dragList.splice(to, 0, movedItem);               // ② 插入到目标位置

Array.splice() 的返回值是被删除的元素组成的数组,所以 [0] 取到被移动的那个元素。两步操作完成后,@State dragList 被修改,触发 UI 重渲染,拖拽动画自动衔接。

7.3 拖拽过程中的视觉反馈

系统在拖拽过程中的默认行为:

  • 被拖拽的 ListItem 浮起(z-index 提升)
  • 目标位置出现插入占位
  • 相邻项自动退让
  • 松手时插入动画平滑过渡

开发者无需额外编写动画代码,这就是原生编辑模式的核心价值。


八、@Builder 组件化设计

8.1 为什么用 @Builder 而不是自定义组件?

对比维度 @Builder 自定义 @Component
状态独立 ❌ 共享父组件状态 ✅ 独立状态管理
代码量 少(约 15 行) 多(约 30+ 行)
复用范围 当前组件内 全局
参数传递 简单参数 复杂对象

对于卡片 UI,其逻辑仅为"根据参数渲染样式",不涉及独立状态,使用 @Builder 是最简洁的选择。

8.2 ModeSection:模式开关卡片

@Builder
ModeSection(title: string, isEditing: boolean, onToggle: () => void) {
  Row() {
    Text(title)
      .fontSize(17)
      .fontWeight(FontWeight.Medium)
      .layoutWeight(1)
    Toggle({ type: ToggleType.Switch, isOn: isEditing })
      .onChange(() => onToggle())
  }
  .width('100%')
  .padding({ top: 12, bottom: 8 })
}

8.3 三种列表项卡片的设计思路

卡片 视觉特征 选中态反馈
MultiItemCard 左侧 4px 竖条 + 文字 蓝色高亮条 + #E3F2FD 背景
SingleItemCard 圆形指示器 + 对勾 实心蓝圆 + ✓ 符号
DragItemCard 序号 + 把手图标 拖拽浮起(系统动画)

每种卡片都用不同的视觉语言传达同一种状态,这是移动端设计的基本原则:同一个交互状态,用同一个视觉符号,避免用户混淆。


九、ArkTS 语法注意事项

在编写过程中,我遇到了多个 ArkTS 的语法约束,在此汇总:

9.1 构造函数不允许参数声明字段

// ❌ 编译错误:arkts-no-ctor-prop-decls
class Item {
  constructor(public id: number, public name: string) {}
}

// ✅ 正确写法
class Item {
  id: number = 0;
  name: string = '';
  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }
}

9.2 build() 内只能放 UI 组件

build() {
  Column() {
    const x = 1;          // ❌ 编译错误
    this.doSomething();   // ❌ 编译错误
    Text('hello');        // ✅
  }
}

9.3 router 需要显式导入

不同于 TextList 等全局可用的组件,router@kit.ArkUI 导出的模块,需要显式导入:

import { router } from '@kit.ArkUI';

9.4 FontWeight 的枚举值

FontWeight.Bold       // ✅ 700
FontWeight.Medium     // ✅ 500(注意:不是 SemiBold!)
// FontWeight.SemiBold // ❌ API 24 中不存在

十、完整源码逐段解读

10.1 页面入口与数据准备

文件 ListEditDemo.ets@Entry @Component 装饰器标识主页面:

@Entry
@Component
struct ListEditDemo {
  @State multiList: ListItemData[] = [...];
  @State singleList: ListItemData[] = [...];
  @State dragList: ListItemData[] = [...];

  @State isMultiEdit: boolean = false;
  @State isSingleEdit: boolean = false;
  @State isDragEdit: boolean = false;

  @State multiSelectedIds: Set<number> = new Set();
  @State singleSelectedId: number = -1;
}

这里使用了 7 个 @State 变量,分别管理三类列表数据和三组编辑状态。所有状态都遵循"最小化原则"——只存储用户交互的结果,不存储冗余的中间状态。

10.2 底部导航

Row() {
  Button('← 返回首页')
    .fontColor('#1E88E5')
    .backgroundColor(Color.White)
    .borderRadius(20)
    .onClick(() => {
      router.back();
    })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding({ top: 8, bottom: 16 })

router.back() 是 API 24 推荐的返回方式,虽然标记为 deprecated,但替代方案 Router.back()(注意首字母大写)目前还不稳定。

10.3 路由注册

{
  "src": [
    "pages/Index",
    "pages/ListEditDemo"
  ]
}

main_pages.json 中注册新页面后,才能通过 router.pushUrl() 跳转。


十一、性能优化建议

11.1 使用 LazyForEach 替代 ForEach

对于超过 50 条的数据,应该使用 LazyForEach 搭配 IDataSource 进行懒加载渲染。LazyForEach 只渲染当前可见区域的项的项,配合 cachedCount 属性可以显著提升滚动流畅度。

LazyForEach(this.dataSource, (item: ListItemData) => {
  ListItem() { ... }
}, (item: ListItemData) => item.id.toString())
.onMove((from, to) => {
  this.dataSource.moveData(from, to);  // LazyForEach 内置 moveData 方法
})

11.2 editMode 与 onMove 的版本兼容

API 版本 editMode onMove 位置 推荐度
API 12–19 支持 List ⚠️ 旧版
API 20–23 支持(deprecated) ForEach ✅ 当前
API 24+ 支持(deprecated) ForEach ✅ 推荐

11.3 选中状态的数据结构选择

场景 推荐结构 原因
多选(< 100 项) Set<number> O(1) 查找,去重
多选(> 100 项) Map<number, boolean> 批量操作方便
单选 number 一个变量足矣

十二、视觉风格解析

12.1 色彩体系

角色 色值 用途
主色调 #1E88E5 标题、选中态、按钮
背景 #F5F5F5 页面背景
白底 #FFFFFF 卡片背景
高亮背景 #E3F2FD 选中项背景
弱化文字 #888 / #999 描述文本
删除色 #E53935 批量删除按钮

12.2 卡片阴影

.shadow({
  radius: 4,
  color: '#0D000000',  // 5% 透明度黑色
  offsetY: 2
})

编辑模式下阴影颜色略微加深(#1A1E88E5),给用户一种"这些卡片可以被操作"的心理暗示。

12.3 圆角与间距

  • 卡片圆角:10(统一视觉)
  • List 容器圆角:12(略大于卡片,形成容器感)
  • 列表项间距:8(舒适合适)

十三、常见问题 FAQ

Q1: 为什么开启了 editMode,列表项却不能点击选中?

A: 检查是否给 ListItem 设置了 .selectable(true)。默认值为 false,必须在每个 ListItem 上显式开启。

Q2: onMove 回调不触发怎么办?

A: 确认 onMove 是链式调用在 ForEach(...) 之后,而非 List(...) 之后。这是一个非常容易犯的错误。

Q3: 如何在拖拽时添加自定义动画?

A: 系统已内置默认的插入动画。如果需要自定义,可以在 onMove 中使用 animateTo() 包裹数据修改:

animateTo({ duration: 200 }, () => {
  const movedItem = this.dragList.splice(from, 1)[0];
  this.dragList.splice(to, 0, movedItem);
});

Q4: @State Set 修改后 UI 不刷新怎么办?

A: ArkTS 的 @StateSet 的深层修改监控有限。解决方案是每次修改时创建新 Set

// ❌ 不会刷新
this.multiSelectedIds.add(id);

// ✅ 会刷新
this.multiSelectedIds = new Set([...this.multiSelectedIds, id]);

Q5: 编辑模式下如何禁用 List 的滚动?

A: 可以使用 Scroll 容器的 scrollEnabled 属性或 List 的 nestedScroll 属性:

List()
  .editMode(this.isEdit)
  .nestedScroll({
    scrollForward: NestedScrollMode.SELF_ONLY,
    scrollBackward: NestedScrollMode.SELF_ONLY
  })

十四、结语

本文详细拆解了 HarmonyOS NEXT(API 24)中 List 组件编辑模式的三种核心交互——多选批量操作、单选方案确认、长按拖拽排序,并提供了完整的 ArkTS 源码与逐行注释。

回顾全文,核心要点可以浓缩为四句话:

  1. .editMode(true) 是进入编辑模式的总开关
  2. ListItem.onSelect + .selectable(true) 是选中交互的基础组合
  3. ForEach.onMove 是拖拽排序的唯一入口(注意不在 List 上)
  4. @Builder + getter 计算属性 是 ArkTS 组件化与逻辑提取的最佳实践

HarmonyOS NEXT 的声明式 UI 框架正在快速演进,虽然部分 API 还有版本兼容的阵痛,但其原生编辑模式的体验已经可以媲美甚至超越主流移动端框架。希望本文能为你的鸿蒙开发之路提供一份扎实的参考。


本文代码已通过 hvigorw assembleHap 编译验证,运行于 HarmonyOS NEXT API 24 模拟器。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐