news 2026/7/3 21:35:12

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
鸿蒙原生 ArkTS 布局之 List 编辑模式深度解析:多选 / 单选 / 拖拽排序

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




一、引言

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

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


二、环境与准备工作

2.1 开发环境

项目版本
操作系统Windows 10 / 11
DevEco Studio5.0+ (HarmonyOS NEXT)
SDKAPI 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 中,类定义必须遵循严格规则:不能在构造函数参数中声明字段,必须显式在类体内声明。

classListItemData{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 状态管理

@StateisMultiEdit:boolean=false;// 编辑模式开关@StatemultiSelectedIds:Set<number>=newSet();// 选中项 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)=>{// ← 选中回调constnewSet=newSet(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=newSet();

这里的关键细节是:先过滤数组,再清空 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){constitem=this.list.find(...);// ← 编译错误!Text(item.name);}}}

正确写法(getter 提取):

getselectedSingleText():string{constitem=this.singleList.find(v=>v.id===this.singleSelectedId);returnitem?`已选方案:${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 上!constmovedItem=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 的两步操作详解
constmovedItem=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:模式开关卡片

@BuilderModeSection(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-declsclassItem{constructor(publicid:number,publicname:string){}}// ✅ 正确写法classItem{id:number=0;name:string='';constructor(id:number,name:string){this.id=id;this.name=name;}}

9.2 build() 内只能放 UI 组件

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

9.3 router 需要显式导入

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

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

9.4 FontWeight 的枚举值

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

十、完整源码逐段解读

10.1 页面入口与数据准备

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

@Entry@Componentstruct ListEditDemo{@StatemultiList:ListItemData[]=[...];@StatesingleList:ListItemData[]=[...];@StatedragList:ListItemData[]=[...];@StateisMultiEdit:boolean=false;@StateisSingleEdit:boolean=false;@StateisDragEdit:boolean=false;@StatemultiSelectedIds:Set<number>=newSet();@StatesingleSelectedId: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 版本editModeonMove 位置推荐度
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},()=>{constmovedItem=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=newSet([...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 模拟器。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/29 0:40:30

爬虫数据清洗与存储——从爬下来到用得上的最后一公里

爬虫最难的不是把数据爬下来&#xff0c;而是把乱七八糟的原始数据洗干净存好。这一篇讲爬虫后处理的完整流程——清洗、去重、存储。 一、脏数据的常见问题 爬下来的原始数据基本是这样的&#xff1a; data [{"title": " iPhone 15 ", "price&quo…

作者头像 李华
网站建设 2026/6/29 0:28:15

OpenSpec 最佳实践:从“凭感觉”到“照单执行”

OpenSpec 在 Trae 与 Cursor 上的最佳实践&#xff1a;从“凭感觉”到“照单执行” 引言&#xff1a;AI 编程的“凭感觉”困境 想象一下这个场景&#xff1a;周四下午&#xff0c;产品经理找到你&#xff1a;“帮忙加个用户管理功能吧&#xff0c;就是基本的增删改查&#xff0…

作者头像 李华
网站建设 2026/6/29 1:20:31

会展展具租赁选型参考与线下营销落地总包服务适用人群

会展展具租赁中的常见误区与成本控制企业在筹备展会时&#xff0c;往往容易将注意力集中在视觉设计或宣传物料上&#xff0c;而忽视了基础硬件的选型。会展展具租赁作为线下活动的基础设施&#xff0c;其品质直接影响参会体验。在实际操作中&#xff0c;许多策划方存在两个主要…

作者头像 李华
网站建设 2026/6/29 0:28:16

m3u8 视频在线提取,打开浏览器就能用

文章目录m3u8 视频在线提取&#xff0c;打开浏览器就能用m3u8 视频在线提取&#xff0c;打开浏览器就能用 GitHub 上有一个 m3u8 视频下载工具&#xff0c;Star 数超过 7000。 m3u8 是一种常见的视频格式&#xff0c;原理是把完整视频拆成多个 .ts 碎片文件&#xff0c;再用一…

作者头像 李华