鸿蒙HarmonyOS列表体验框架实战 —— 先选 Pattern,再写 Row
一、前言:列表不是样式,是内容组织系统
先看一个真实翻车现场。
产品经理说:「做个设置页,参考系统设置。」开发者写了:
List() {
ForEach(this.items, (item: SettingItem) => {
ListItem() {
Row() {
Text(item.title)
Blank()
Text(item.value)
Image($r('sys.media.ohos_ic_public_arrow_right'))
}
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.margin({ bottom: 8 })
}
})
}
看起来「很卡片、很现代」,但上真机后问题立刻暴露:
-
信息密度失控:每一行都是独立圆角卡片,页面又高又碎,不像系统设置的分组感。
-
后缀语义混乱:有的行该是开关,有的该是当前值,有的该是进入下一级——全用箭头糊弄。
-
管理列表缺动作:文件/消息列表只有「点进详情」,删除、收藏要绕一层。
-
侧滑与滚动打架:好不容易接了
swipeAction,纵向一滑,侧滑条还开着。 -
没有刷新位:远程数据变了,用户只能退出再进。
根因不是「List API 不会用」,而是:
List 不是一种视觉皮肤,而是一套按用户意图组织内容的系统。
E013 的研究问题因此变成:
Content / Setting / Management intent
-> choose List Pattern
-> choose cell anatomy
-> choose section / group / card container
-> choose accessory / swipe action
-> bind Theme Engine tokens
先选模式,再写行。这是整篇最重要的一句话。
二、文档地图:List 能力横跨 HDS 与 ArkUI
和 Menu、Sheet、Feedback 一样,鸿蒙 List 也没有「一篇文档讲完」。E013 整理的阅读地图:
HarmonyOS List Experience
├── HDS ListItem
│ ├── prefix / text / suffix anatomy
│ ├── HdsSwipeActionOptions / FullDelete
│ └── pattern wrapper(不支持通用属性/事件)
├── HDS ListItemCard
│ ├── HdsListItemCardOptions
│ ├── card background / radius / height
│ └── 多设备系统列表样式
├── ArkUI List
│ ├── List / ListItem / ListItemGroup
│ ├── divider / space / edgeEffect
│ └── ListScroller.closeAllSwipeActions
└── 页面级能力
├── Refresh 下拉刷新
├── onReachEnd 加载更多
└── Theme / Navigation shell
|
区域 |
它拥有什么 |
ArkUILab 读法 |
|---|---|---|
|
HdsListItem |
横向侧滑、承载 Card、菜单预览、选中态 |
模式包装器,不是自由样式 Row |
|
HdsListItemCard |
prefix/text/suffix + 卡片外形 |
内部布局走 Options,少用通用 padding/margin |
|
ListItem.swipeAction |
start/end、actionAreaDistance、onAction |
管理类行级动作的 ArkUI 原生能力 |
|
ListScroller |
|
滚动/切 tab/操作完成时收起侧滑的基础设施 |
官网页面多为动态内容,E013 用官方入口建地图,再用本机 DevEco SDK 的 .d.ts 交叉验证可编译 API——这是鸿蒙文档驱动实验的固定方法。
三、能力地图:现代 App 列表至少要覆盖这些
HarmonyOS List Capability
├── Plain List 行距、分割线、轻量导航
├── Setting List section、组表面、arrow/switch/checkbox/value
├── Card List 卡片表面、间距、圆角、阴影、对象组
├── Swipe Action 多按钮、full swipe、状态回调、closeAll
├── Refresh / Pagination 下拉刷新、触底加载、loading/no more footer
├── Section / Group header、footer 帮助文案、语义边界
├── Accessory Arrow / Switch / CheckBox / Value / Custom
└── Future Grid / WaterFlow / Timeline / 多选 / 拖拽排序 / LazyForEach
E013 v1 只沉淀前三个模式(Setting / Card / Swipe)以及刷新加载。Content Browser(搜索筛选 + 空态错误态 + Grid 切换)要和 E010 Search Session、后续 Grid/WaterFlow 一起做,不要硬塞进 List Framework v1。
四、Pattern 决策:四种意图,四条路径
4.1 决策表
|
用户意图 |
Pattern |
默认结构 |
典型页面 |
|---|---|---|---|
|
配置应用/系统状态 |
Setting |
Section → Group surface → Cell → Accessory |
设置、账户、隐私、通知 |
|
管理重复对象 |
Management |
List + SwipeActionRow + ListScroller |
文件、消息、待办、下载 |
|
比较/展示对象集合 |
Card |
CardSection → 相关 rows |
推荐、项目摘要、记录组 |
|
浏览大体量内容 |
Content Browser |
Plain/Card + 搜索筛选 + 刷新加载 |
内容库、榜单(v1 不完整覆盖) |
口诀:
Configure system or app state -> Setting Pattern
Manage repeated objects -> Management Pattern
Compare grouped content -> Card Pattern
Browse large content -> Content Browser Pattern
4.2 官方设计原则(对齐系统应用)
E013 对系统设置 / 文件 / 图库 / 应用市场做了 Benchmark 后的结论:
-
用 row anatomy 保持可预期:prefix 识别对象,title 主语义,subtitle 解释,suffix 状态或动作。
-
用 section/group 控制密度:相近配置同组,组与组用空间区隔,而不是密线分割全页。
-
用 card 承载对象集合:需要更强边界时才上 Card List,不要为了「好看」把设置页全卡片化。
-
用 swipe 承载重复管理动作:删除、收藏、分享、更多——对同类对象反复发生的操作。
-
Divider 是组内关系,不是页面装饰:卡片之间靠 spacing,不靠密集分割线。
|
Benchmark |
组织方式 |
对 ArkUILab 的启发 |
|---|---|---|
|
鸿蒙设置 |
Section + 分组行 + arrow/switch/value |
Setting 是配置入口默认模式 |
|
鸿蒙文件 |
名称 + 元信息 + 操作入口 |
管理列表需要 Swipe/Menu/Selection,不能只靠点击 |
|
鸿蒙图库 |
网格与时间/地点分组优先 |
v1 不覆盖图库主视图,后续 Grid/WaterFlow |
|
应用市场 |
卡片、榜单、横向流混合 |
Card 适合推荐集合,不适合设置页全量替换 |
五、ListCell 解剖:一行到底该长什么样
5.1 统一解剖结构
[ prefix ] [ title ] [ suffix ]
[ subtitle / secondary ]
|
区域 |
承载什么 |
不该承载什么 |
|---|---|---|
|
prefix |
图标、对象识别色块 |
超大 launcher 位图 |
|
title |
主语义,单行优先 |
整段说明文案 |
|
subtitle |
解释、元信息,最多两行 |
操作按钮 |
|
suffix |
一种主要语义:进入 / 值 / 开关 / 选择 |
同时塞开关 + 箭头 + 长文案 |
5.2 ArkUIListCell 实战
export enum ArkUIListCellAccessory {
None, Arrow, Switch, Checkbox, Value
}
@Component
export struct ArkUIListCell {
title: string = '';
subtitle: string = '';
iconLabel: string = '';
valueText: string = '';
accessory: ArkUIListCellAccessory = ArkUIListCellAccessory.Arrow;
checked: boolean = false;
showDivider: boolean = true;
onCellClick: () => void = (): void => {};
onCheckedChange: (value: boolean) => void = (_: boolean): void => {};
// 使用 ThemeBrandAccent 时必须配合 @StorageProp 品牌键(E012 规则)
@StorageProp(BRAND_STORAGE_KEY) private activeBrand: BrandTheme = BrandTheme.Default;
}
关键实现细节(来自真代码,不是伪代码口号):
// 1. 长文本必须截断
Text(this.title)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
// 2. suffix value 必须限宽,否则挤爆标题
Text(this.valueText)
.constraintSize({ maxWidth: 116 })
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
// 3. 分割线缩进:有 icon 时从文字列起,最后一项不画
Divider()
.margin({ left: this.iconLabel.length > 0 ? 62 : 16 })
// 4. Switch/Checkbox 用品牌选中色,结构色不污染
Toggle({ type: ToggleType.Switch, isOn: this.checked })
.selectedColor(ThemeBrandAccent())
后缀一次只表达一种主要语义:
|
意图 |
accessory |
|---|---|
|
进入下一级 |
Arrow(可附带短 value) |
|
当前状态只读展示 |
Value |
|
直接改布尔 |
Switch(点行不导航) |
|
持久多选选项 |
Checkbox |
|
纯展示/侧滑宿主 |
None |
六、Setting Pattern:分组表面,不是一堆卡片
6.1 结构
Page
-> Section header(小标题)
-> Group surface(统一圆角底板)
-> ListCell × N(组内 divider)
-> Optional footer(帮助说明)
@Component
export struct ArkUISettingSection {
title: string = '';
footer: string = '';
@BuilderParam content: () => void = this.EmptyContent;
build() {
Column({ space: 8 }) {
if (this.title.length > 0) {
Text(this.title)
.fontSize(13)
.fontColor(ThemeTextSecondary())
.padding({ left: 16, right: 16 })
}
Column() {
this.content()
}
.backgroundColor(ThemeSurfaceBackground())
.borderRadius(16)
.clip(true)
if (this.footer.length > 0) {
Text(this.footer)
.fontSize(12)
.fontColor(ThemeTextTertiary())
.padding({ left: 16, right: 16 })
}
}
}
}
业务侧组装:
ArkUISettingSection({
title: '通知',
footer: '关闭后仍可在系统设置中管理通知权限。'
}) {
ArkUIListCell({
title: '允许通知',
subtitle: '接收重要状态与提醒',
iconLabel: 'N',
accessory: ArkUIListCellAccessory.Switch,
checked: this.notifyOn,
showDivider: true,
onCheckedChange: (v: boolean) => { this.notifyOn = v; }
})
ArkUIListCell({
title: '通知样式',
valueText: '横幅',
accessory: ArkUIListCellAccessory.Arrow,
showDivider: false, // 组内最后一项
onCellClick: () => { /* push 子页 */ }
})
}
6.2 Setting 铁律
-
相关配置进同一 group surface,不要每行独立卡片。
-
组与组之间用 spacing,不是满屏 divider。
-
Switch / Checkbox 改状态,不导航;Arrow 才进入。
-
Footer 放解释性帮助,不把长说明塞进 subtitle。
七、Card Pattern:边界有意义才上卡片
7.1 什么时候用 Card
适合:推荐集合、项目摘要、订单/记录组、需要「这一坨是一个对象」的比较视图。
不适合:把整个设置页改成大卡片堆——那是视觉通胀,不是系统质感。
CardSection
-> title / subtitle(对象组标题)
-> related ListCell rows
-> inner dividers
-> light shadow / elevation
@Component
export struct ArkUICardSection {
title: string = '';
subtitle: string = '';
@BuilderParam content: () => void = this.EmptyContent;
build() {
Column({ space: 10 }) {
// 标题区用 structural text tokens
Column() {
this.content()
}
.padding(16)
.backgroundColor(ThemeSurfaceBackground())
.borderRadius(18)
.shadow({
radius: 12,
color: ThemeCardShadowColor(),
offsetY: 4
})
}
}
}
7.2 Divider vs Spacing
|
关系 |
用什么 |
|---|---|
|
同一卡片/组内的两行 |
Divider(内部关系) |
|
两个独立卡片对象 |
Spacing(对象边界) |
|
两个设置分组 |
Section spacing |
多个独立圆角 HDS 卡连续堆叠时,E013 结论是:先加 8vp spacer,不要假装成「只有首尾圆角的 grouped list」——除非你已经验证了 per-corner radius 或自定义行方案。HdsListItemCardOptions 当前主要暴露统一 cardBorderRadius。
八、Management Pattern:侧滑是行级动作,不是编辑器
8.1 结构
List(scroller: ListScroller)
-> SwipeActionRow
-> end actions: 更多 / 分享 / 收藏 / 删除
-> full swipe -> onAction 删除
-> 滚动/切 tab/完成操作/页面消失 -> closeAllSwipeActions
@Component
export struct ArkUISwipeActionRow {
title: string = '';
subtitle: string = '';
onDelete: () => void = (): void => {};
onFavorite: () => void = (): void => {};
// ...
build() {
ListItem() {
ArkUIListCell({
title: this.title,
subtitle: this.subtitle,
accessory: ArkUIListCellAccessory.None,
showDivider: false
})
.backgroundColor(ThemeSurfaceBackground())
}
.height(72)
.swipeAction({
end: {
builder: (): void => { this.EndActions(); },
actionAreaDistance: 256,
onAction: (): void => { this.onDelete(); }, // full swipe
onStateChange: (state: SwipeActionState): void => {
// 可观测展开/收起
}
}
})
}
}
8.2 closeAllSwipeActions 是基础设施
private swipeScroller: ListScroller = new ListScroller();
private closeAllSwipe(): void {
// 必须在 scroller 已绑定 List 之后调用,否则运行时可能抛错
this.swipeScroller.closeAllSwipeActions({
onFinish: () => { /* optional */ }
});
}
// 调用时机(默认全做):
// 1. 纵向滚动开始
// 2. Tab / 模式切换
// 3. 某个 action 完成
// 4. 下拉刷新 / 加载更多开始前
// 5. 页面 onHidden / aboutToDisappear
边界:
-
Swipe 适合重复对象管理,不替代 Selection 多选框架;
-
不适合一次性复杂危险流程(权限撤销、清空数据)——应走 Dialog / Feedback;
-
删除必须真正改 backing state,不能只改一行状态文案;
-
危险动作不要只靠暧昧图标,文案或颜色要可读。
8.3 侧滑与纵向滚动冲突
用户展开侧滑后再竖滑列表,若不收起,会出现手势抢夺和「幽灵按钮」。E013 默认:滚动开始即 closeAll。这是管理列表「像官方应用」的关键细节。
九、Refresh + Load More:列表默认能力,不是插件
现代列表很少只有静态展示。E013 把下拉刷新和触底加载写进 Framework 默认能力:
Page
-> Refresh({ refreshing })
-> List / Scroll
-> onReachEnd
-> loading footer / no-more footer
@State isRefreshing: boolean = false;
@State loadingMore: boolean = false;
@State noMore: boolean = false;
Refresh({
refreshing: this.isRefreshing,
// builder: 自定义刷新头(可选)
}) {
List({ scroller: this.listScroller }) {
// items ...
ListItem() {
// footer:加载中 / 没有更多了
}
}
.onReachEnd(() => {
this.LoadMore();
})
}
.onRefreshing(() => {
this.RefreshList();
})
.pullToRefresh(true)
规则:
|
规则 |
说明 |
|---|---|
|
页面级能力 |
Refresh 不属于某一行 |
|
显式状态 |
|
|
手势关系 |
刷新或加载开始前先 |
|
职责拆分 |
Framework 管手势位与 footer 展示;业务管请求与分页策略 |
|
Hero 不放运行态 |
刷新中、已加载 N 条不要塞进顶部身份区 |
Hero/Header 可以用 ArkUILab tonal gradient 做页面身份,但文字必须用 structural text tokens,保证 Light / Dark / Brand 可读。动态指标放列表体、footer 或 Feedback 层。
十、官方 HDS 原生路径:能编译 ≠ 能上真机硬套
E013 额外做了 HdsListNativeLabDestination,直接验证 UIDesignKit 导出的官方组件。
10.1 两层定位
|
组件 |
定位 |
关键限制 |
|---|---|---|
|
HdsListItem |
pattern wrapper:侧滑 + menu + selection + 承载 Card |
不支持通用属性/通用事件 |
|
HdsListItemCard |
多设备系统列表卡片 |
内部布局用 |
// 保守且已验证的路径
List({ scroller: this.listScroller }) {
ListItem() {
// 或直接:
HdsListItem({
hdsListItemCard: this.PlainCardOptions('标题', '副标题', '打开'),
swipeActionOptions: this.SwipeOptions('行 id', (): void => {
this.DeleteRow('行 id');
})
})
}
}
删除要走真实回调并改状态:
// HdsSwipeActionOptions 侧
deleteIconOptions.onAction // 点删除图标
fullDeleteOptions.onFullDeleteAction // 满滑删除
// 两个入口都要从数据源移除 row,而不是只 toast 一句「已删除」
10.2 HDS 踩坑清单(设备已踩)
|
现象 |
原因 |
做法 |
|---|---|---|
|
想给 |
不支持通用 modifier |
改 Options / 外层结构 |
|
|
与 card 组合未经验证 |
暂勿当默认路径 |
|
|
组件方法当 builder 传入不安全 |
先走 |
|
行高爆炸 |
用了 |
先用 HDS 小控件/小图标 |
|
|
ArkTS 类型不接受 |
用默认宽或简单 Dimension |
|
多张圆角卡粘连 |
统一 radius 无 per-corner |
卡间加 spacing |
|
多个 HDS card 塞进一个 ListItem |
测量不稳 |
一个 ListItem 一张独立 card |
|
当完整页面框架 |
HDS 只管行 |
仍要自备 header/section/refresh/footer/theme |
10.3 双轨策略(v1 建议)
业务实验 / 快速迭代
-> ArkUIListCell + SettingSection + CardSection + SwipeActionRow
-> 设计系统 API 稳定、主题可控
官方视觉 / 原生行为对齐
-> HdsListItem + HdsListItemCard + HdsSwipeActionOptions
-> 设备 Review 通过后再考虑替换 adapter
不要一上来把所有 HDS 参数裸露给业务。先沉淀稳定 anatomy API,再评估 ArkUIListCell 是否变成 HdsListItemCardOptions 的薄封装。
十一、Theme Engine:结构色与品牌色正交
列表页最容易把「好看」做成「换肤就炸」。E013 规则:
|
用途 |
Token |
|---|---|
|
页面背景 |
|
|
分组/卡片表面 |
|
|
主/次/三级文字 |
|
|
分割线 |
|
|
卡片阴影 |
|
|
图标底/开关选中/强调 |
|
原则:
品牌色只用于识别与选中态,不污染 page / surface / text / divider 的结构层。
任何调用 ThemeBrandAccent() 的组件,必须遵守 E012:声明 @StorageProp(BRAND_STORAGE_KEY),让品牌切换触发重建。否则你在 Theme Playground 换了粉/绿,列表图标还停在旧色。
十二、页面壳:HdsNavDestination 与白屏排查
E013 类页面若由 HdsNavigation.navDestination 推出,必须用 HdsNavDestination 包内容,而不是裸 Scroll/Column。
常见白屏 + 底部 Floating TabBar 仍在:
检查顺序
1. PageMap 是否 return HdsNavDestination() { ... }
2. TabBar 显隐是否绑在 .onShown() / .onHidden()
(不要只依赖 aboutToAppear/Disappear,可能与路由可见性错位)
3. 沉浸标题是否 .bindToScrollable([scroller])
4. closeAllSwipeActions 是否在 ListScroller 绑定前被调用
十三、接入清单(可直接当 PR Checklist)
13.1 模式与结构
-
明确页面意图:Setting / Management / Card / Browser
-
Setting:
ArkUISettingSection+ArkUIListCell,分组 surface,最后一项无 divider -
Card:组边界有语义才上卡;组内 divider,卡间 spacing
-
Management:
List+ListScroller+ swipe;滚动/完成/刷新前 closeAll -
远程数据:页面级
Refresh+onReachEnd+ loading/no-more footer
13.2 行与主题
-
suffix 一次一种主语义
-
长文本
maxLines + textOverflow;value 设 maxWidth -
结构 token 与品牌 token 分层;
ThemeBrandAccent配@StorageProp -
Hero 用 structural 文字,不放刷新/条数等运行态
13.3 HDS 原生(若使用)
-
HdsListItem放在List({ scroller })内 -
保守路径:
hdsListItemCard + swipeActionOptions -
删除双回调都改数据源
-
一卡一 ListItem;独立圆角卡加 spacing
-
不用大 launcher 图、不用 calc 宽度、不用通用 modifier 改内部 anatomy
13.4 壳与构建
-
HdsNavDestination+ onShown/onHidden 管 TabBar -
HAP:
CompileArkTS+PackageHap通过
DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk \
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw assembleHap --no-daemon
十四、设备 Review 最小集
-
切 Light / Dark / Brand,返回列表页,颜色跟 token。
-
顶栏标题在三套主题下可读。
-
下拉刷新:状态正确、数据复位。
-
触底:loading → 追加 → no more。
-
Setting:分组、行高、divider、开关/勾选、箭头对齐。
-
Card:间距、圆角、阴影、组内节奏。
-
Swipe:多按钮、full swipe 删除、动作后收起、竖滑收起、Close 全收。
-
窄屏长文案不溢出。
-
HDS Native Lab:删除图标与满滑都真实移除行。
十五、已知边界与下一步
v1 已有:
List Framework v1 Candidate
├── Pattern 决策模型
├── Setting / Card / Swipe
├── Pull Refresh / Load More
├── ListCell anatomy
├── Theme Engine 合规
├── HDS Native probe
└── Runbook / Skill
尚未 stable 的点:
-
ArkUIListCell是实验 adapter,不是官方HdsListItem替代品;视觉平替需真机 Review 后再定。 -
卡片圆角/阴影是 ArkUILab 近似值,需对照系统应用继续校准。
-
Grid / WaterFlow / Timeline / 多选编辑 / 拖拽排序 / LazyForEach 大数据不在 E013 范围。
-
customItemBuilder安全 handoff 未完全证明前,不要当默认业务路径。
十六、写在最后
鸿蒙列表体验的分水岭,不在「会不会写 List」,而在有没有把意图落成模式:
先选 Pattern,再写 Row。
Setting 用分组表面,Card 用对象边界,Swipe 用行级重复动作。
Divider 管组内,Spacing 管对象,Hero 不管运行态。
closeAllSwipeActions 是基础设施,不是锦上添花。
Refresh / Load More 是列表默认能力,不是业务临时补丁。
HDS 是 pattern 资产,ArkUILab adapter 是实验与统一 API 的桥梁——能编译还要能过真机。
记住官方心智:
prefix 识别对象,title 承载主语义,subtitle 承载解释,suffix 承载一种状态或动作。
本文基于 ArkUILab 项目 E013 实验。它已是 List Framework v1 candidate:可构建、可主题切换、可侧滑与刷新加载,并带有 HDS 原生探针。若你正在做设置页、内容组或行级管理列表,可以直接按 Pattern 选型,复用 ArkUIListFramework.ets 与 Runbook 清单起步,再按产品需要演进到 HDS 原生或 Grid/WaterFlow。
更多推荐




所有评论(0)