一、前言:列表不是样式,是内容组织系统

先看一个真实翻车现场。

产品经理说:「做个设置页,参考系统设置。」开发者写了:

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 })
    }
  })
}

看起来「很卡片、很现代」,但上真机后问题立刻暴露:

  1. 信息密度失控:每一行都是独立圆角卡片,页面又高又碎,不像系统设置的分组感。

  2. 后缀语义混乱:有的行该是开关,有的该是当前值,有的该是进入下一级——全用箭头糊弄。

  3. 管理列表缺动作:文件/消息列表只有「点进详情」,删除、收藏要绕一层。

  4. 侧滑与滚动打架:好不容易接了 swipeAction,纵向一滑,侧滑条还开着。

  5. 没有刷新位:远程数据变了,用户只能退出再进。

根因不是「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

closeAllSwipeActions()

滚动/切 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 后的结论:

  1. 用 row anatomy 保持可预期:prefix 识别对象,title 主语义,subtitle 解释,suffix 状态或动作。

  2. 用 section/group 控制密度:相近配置同组,组与组用空间区隔,而不是密线分割全页。

  3. 用 card 承载对象集合:需要更强边界时才上 Card List,不要为了「好看」把设置页全卡片化。

  4. 用 swipe 承载重复管理动作:删除、收藏、分享、更多——对同类对象反复发生的操作。

  5. 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 不属于某一行

显式状态

refreshing / loadingMore / noMore 必须可渲染

手势关系

刷新或加载开始前先 closeAllSwipeActions

职责拆分

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

多设备系统列表卡片

内部布局用 HdsListItemCardOptions

// 保守且已验证的路径
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 踩坑清单(设备已踩)

现象

原因

做法

想给 HdsListItem 链式 .padding()

不支持通用 modifier

改 Options / 外层结构

listItemModifier 能编译但布局不稳

与 card 组合未经验证

暂勿当默认路径

customItemBuilder 运行时 bind of undefined

组件方法当 builder 传入不安全

先走 hdsListItemCard + swipeActionOptions

行高爆炸

用了 app.media.startIcon 等 launcher 大图

先用 HDS 小控件/小图标

cardWidth: calc(...) 编译失败

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

页面背景

ThemePageBackground()

分组/卡片表面

ThemeSurfaceBackground()

主/次/三级文字

ThemeTextPrimary/Secondary/Tertiary()

分割线

ThemeDividerColor()

卡片阴影

ThemeCardShadowColor()

图标底/开关选中/强调

ThemeBrandAccent()

原则:

品牌色只用于识别与选中态,不污染 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 最小集

  1. 切 Light / Dark / Brand,返回列表页,颜色跟 token。

  2. 顶栏标题在三套主题下可读。

  3. 下拉刷新:状态正确、数据复位。

  4. 触底:loading → 追加 → no more。

  5. Setting:分组、行高、divider、开关/勾选、箭头对齐。

  6. Card:间距、圆角、阴影、组内节奏。

  7. Swipe:多按钮、full swipe 删除、动作后收起、竖滑收起、Close 全收。

  8. 窄屏长文案不溢出。

  9. 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。

Logo

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

更多推荐