深入鸿蒙 NestedScroll 嵌套滚动协同:从零实战 ArkUI 滚动架构
深入鸿蒙 NestedScroll 嵌套滚动协同:从零实战 ArkUI 滚动架构
写在前面
2025 年的移动端开发格局已然发生了深刻变化。HarmonyOS 作为国产操作系统的标杆,历经多个版本的演进,已经从初试啼声走向了生态繁荣。在鸿蒙原生应用开发中,ArkUI(方舟 UI 框架) 凭借其声明式 UI、跨设备自适应能力和原生性能优势,正吸引着越来越多的开发者投入其中。
而在 ArkUI 的众多能力中,滚动交互 是最基础也最复杂的话题之一。从电商首页到资讯 App,从社交个人主页到短视频信息流,「外层 Header 折叠 + 内层列表滚动」几乎成了现代移动应用的标配交互范式。本文将以一个完整的 NestedScroll 嵌套滚动协同项目为蓝本,逐行拆解 HarmonyOS API 24(SDK 6.1.0)下 ArkUI 的滚动架构,深入探讨从布局设计到性能优化的方方面面。
本文涉及的完整项目已托管在作者的 AtomGit 仓库中(app6106),读者可对照源码阅读本文。文章中所有代码均基于 API Version 24(对应 HarmonyOS 6.1.0,SDK 版本 23)编写,使用 Stage 模型 和 ArkTS 语言。
第一章:理解 NestedScroll —— 什么是嵌套滚动协同?
1.1 场景痛点
先设想一个常见的移动端页面:顶部是一个大幅的 Hero Banner(品牌宣传图),中间是分类 Tab 栏,下方是内容列表。用户的预期行为是:
- 上滑时:Banner 先折叠缩小,腾出空间 → Tab 吸顶 → 列表正常滚动
- 下滑时:列表先滚回顶部 → Banner 再展开恢复原状
在传统 Web 开发中,这可以通过 position: sticky + 滚动监听来模拟。在原生 Android 中,有 CoordinatorLayout + AppBarLayout + NestedScrollView 的经典组合。而在 HarmonyOS 的 ArkUI 中,对应的解决方案就是 NestedScroll 嵌套滚动协同机制。
1.2 NestedScroll 的核心思想
NestedScroll(嵌套滚动)本质上是一种 父子组件之间的滚动事件分配协议。当用户的手指在屏幕上滑动时,滚动事件不会简单地由某一个组件全权处理,而是按照预定义的规则在父组件(外层 Scroll)和子组件(内层 List)之间传递和协同。
ArkUI 提供了两个方向的协同策略枚举值:
| 枚举值 | 常量名 | 含义 |
|---|---|---|
0 |
NestedScrollMode.PARENT_FIRST |
父组件优先消费滚动距离,到达边界后再传递给子组件 |
1 |
NestedScrollMode.SELF_FIRST |
子组件优先消费滚动距离,到达边界后再传递给父组件 |
通过组合这两个方向(scrollForward 和 scrollBackward),开发者可以实现四种不同的协同策略。本文项目采用的策略是:
scrollForward: PARENT_FIRST (0) // 上滑:外层先消费
scrollBackward: SELF_FIRST (1) // 下滑:内层先消费
这样正好实现了「上滑时 Banner 先折叠 → 下滑时 List 先滚回顶部」的用户预期。
1.3 与平台传统方案对比
| 维度 | Android CoordinatorLayout | ArkUI NestedScroll |
|---|---|---|
| 声明方式 | XML + Java/Kotlin 代码绑定 | 纯声明式 API(nestedScroll 属性) |
| 协同规则 | Behavior 回调,扩展性强但复杂度高 | 四个方向组合枚举,简洁直观 |
| 滚动监听 | 需要额外注册 OnOffsetChangedListener | 内置 onScroll 回调 |
| 动画驱动 | 需自行实现 ValueAnimator | 直接绑定 State 变量,自动驱动 UI 更新 |
ArkUI 的设计哲学是 用最少的代码表达最清晰的意图。从 NestedScroll API 的设计就能看出,鸿蒙团队大幅简化了嵌套滚动的配置门槛,让开发者能把更多精力放在业务逻辑和视觉体验上。
第二章:项目结构全景 —— 解剖一个鸿蒙应用的骨架
在深入代码之前,我们先宏观了解一下这个项目的文件结构和各模块的职责。
2.1 顶层目录
app6106/
├── AppScope/ # 应用级资源配置
│ ├── app.json5 # 应用元信息(包名、版本、图标等)
│ └── resources/ # 应用级资源(图标、字符串等)
├── entry/ # 应用入口模块
│ ├── src/main/
│ │ ├── ets/ # ArkTS 源码
│ │ │ ├── entryability/ # Ability 生命周期
│ │ │ ├── entrybackupability/ # 备份扩展
│ │ │ └── pages/ # 页面(核心代码在这里)
│ │ ├── module.json5 # 模块配置
│ │ └── resources/ # 模块级资源(颜色、字符串、媒体等)
│ └── build-profile.json5 # 模块构建配置
├── hvigor/ # 构建工具配置
│ └── hvigor-config.json5
├── build-profile.json5 # 项目级构建配置
├── oh-package.json5 # 包管理配置
└── hvigorfile.ts # 构建脚本入口
这个目录结构遵循了 HarmonyOS Stage 模型 的标准约定。Stage 模型是 HarmonyOS 自 3.1 版本开始主推的应用开发模型,它将应用的运行单元抽象为 Ability(能力单元),通过 module.json5 文件集中声明模块的能力组件。
2.2 模块配置解读
在 entry/src/main/module.json5 中,我们声明了两类能力:
{
"module": {
"name": "entry",
"type": "entry", // entry 类型:应用的主入口模块
"deviceTypes": ["phone"], // 目标设备:手机
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["ohos.want.action.home"]
}
]
}
],
"extensionAbilities": [
{
"name": "EntryBackupAbility",
"srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
"type": "backup", // 备份扩展能力
"exported": false
}
]
}
}
这里值得关注的是 extensionAbilities 中的备份恢复能力。EntryBackupAbility 继承自 BackupExtensionAbility,这是一个非常实用的系统级能力 —— 它允许应用在用户数据备份/恢复时自动处理自身数据的持久化。虽然我们的演示应用没有真正的用户数据需要备份,但这个骨架为生产级应用提供了完善的基础。
2.3 构建配置的 API 版本链路
项目级的 build-profile.json5 中指定了 SDK 版本:
{
"products": [{
"name": "default",
"targetSdkVersion": "6.1.0(23)",
"compatibleSdkVersion": "6.1.0(23)",
"runtimeOS": "HarmonyOS"
}]
}
而 oh-package.json5 的 modelVersion 是 6.1.0。这里有一个重要的概念澄清:
API Version 24 对应的是 ArkTS 语言的 API 等级,而
targetSdkVersion中的(23)指的是 HarmonyOS SDK 版本号。两者共同决定了开发者可用的 ArkUI 组件能力和系统 API 范围。
在实际开发中,升级 API 版本意味着可以享受最新的组件特性和性能优化,但也需要关注 API 变更带来的兼容性调整。本项目中使用的 nestedScroll API 是从 API 12+ 开始稳定的,到 API 24 已经非常成熟。
第三章:核心代码深度解析 —— 逐行拆解 NestedScroll 页面
本章是整个博客的核心。我们将从 Index.ets 的第一行开始,逐段解析每一处设计决策和实现细节。
3.1 文档头与常量定义
/**
* NestedScroll 嵌套滚动协同布局 示例
* ====================================
* 场景:Tab内列表与外层滚动协同
* 核心技术:NestedScroll(Scroll + List 嵌套滚动协同)
*
* 布局层次(从上到下):
* 1. 可折叠的 Hero Banner(渐变背景 + 标题 + 描述)
* 2. 粘性 Tab 栏(「精选」「榜单」「推荐」)
* 3. 内层 List(每个 Tab 对应不同的列表数据)
*
* 滚动协同策略:
* - 手指上滑时 → 外层的 Banner 先折叠收起 → 内层 List 再滚动内容
* - 手指下滑时 → 内层 List 先滚回顶部 → 外层 Banner 再展开
* - onScroll 实时追踪偏移量,驱动 Banner 透明度 / 缩放等视觉变化
*/
const BANNER_FULL_HEIGHT: number = 220; // 单位:vp
const TAB_BAR_HEIGHT: number = 48; // Tab 栏高度
const LIST_ITEM_HEIGHT: number = 72; // 列表项高度
const ITEM_COUNT_PER_TAB: number = 30; // 每个 Tab 的数据条目数
这组常量定义了页面的 视觉度量系统。在 ArkUI 中,布局单位使用 vp(virtual pixel,虚拟像素),它是鸿蒙的响应式像素单位,在不同屏幕密度下自动缩放,确保 UI 在不同设备上保持一致的物理尺寸感。
选择将这些数值定义为模块级常量(而不是硬编码在 Builder 中),遵循了 单点定义、多处复用 的工程原则。当设计师调整 Banner 高度时,只需要修改一个数字,所有依赖(如滚动偏移量的阈值计算)自动适配。
3.2 页面组件与响应式状态
@Entry
@Component
struct NestedScrollPage {
@State currentTabIndex: number = 0;
@State bannerOpacity: number = 1.0;
@State bannerScale: number = 1.0;
@State showFab: boolean = false;
@State scrollOffset: number = 0;
private readonly tabTitles: string[] = ['精选', '榜单', '推荐'];
private readonly listDataSource: string[][] = [
this.generateListData('精选'),
this.generateListData('榜单'),
this.generateListData('推荐'),
];
}
@State 装饰器 是 ArkUI 响应式系统的基石。被 @State 修饰的变量一旦发生变化,ArkUI 框架会自动触发与之绑定的 UI 组件重绘。这里定义了五个状态变量:
currentTabIndex:当前选中的 Tab 索引,切换时会触发条件渲染,重建内层 ListbannerOpacity:Banner 的透明度,从 1.0(完全不透明)到 0.2(接近透明)bannerScale:Banner 的缩放比例,从 1.0(原始大小)到 0.85(稍微缩小)showFab:是否显示「回到顶部」悬浮按钮scrollOffset:当前滚动偏移量,用于各处的数值计算
这些状态变量之间形成了 单向数据流:scrollOffset 由 onScroll 回调写入 → 其他状态(bannerOpacity、bannerScale、showFab)由它推导得出 → 这些状态绑定到 UI 属性(.opacity()、.scale()、if 条件渲染)。
3.3 数据生成与工具方法
private generateListData(prefix: string): string[] {
const data: string[] = [];
for (let i = 1; i <= ITEM_COUNT_PER_TAB; i++) {
data.push(`${prefix}内容项 #${i}`);
}
return data;
}
private getItemIcon(index: number): string {
const icons: string[] = ['🎨', '🚀', '💡', '📱', '🔧', '🌟', '🎯', '📊', '⚡', '🔥'];
return icons[index % icons.length];
}
private getItemDesc(index: number): string {
const descs: string[] = [
'探索前沿技术趋势', '提升开发效率', '最佳实践分享',
'开源项目推荐', '性能优化指南', '架构设计思考',
'UI 交互创新', '数据可视化', '跨平台方案', '社区精选',
];
return descs[index % descs.length];
}
这些方法虽然简单,但体现了一个很好的设计模式:将数据逻辑与 UI 逻辑分离。在实际项目中,generateListData 会被替换为网络请求 + 数据模型,getItemIcon 和 getItemDesc 会映射到真实的数据字段。这种抽象层让后续替换数据源变得非常干净。
特别注意到图标使用的是 Emoji 字符(🎨🚀💡📱🔧🌟🎯📊⚡🔥),这是一种轻量级的视觉点缀方案 —— 无需引入额外的图标字体文件或 SVG 资源,纯文本渲染即可呈现丰富的视觉层次。
3.4 主构建方法 —— 嵌套滚动的核心
build() {
Stack({ alignContent: Alignment.BottomEnd }) {
Scroll() {
Column() {
this.buildBanner()
this.buildTabBar()
this.buildContentList()
}
.width('100%')
}
.width('100%')
.height('100%')
.nestedScroll({
scrollForward: 0, // PARENT_FIRST:上滑时外层先消费
scrollBackward: 1, // SELF_FIRST:下滑时内层先消费
})
.onScroll((xOffset: number, yOffset: number) => {
this.scrollOffset = yOffset;
this.bannerOpacity = Math.max(0.2, 1.0 - yOffset / 180);
this.bannerScale = Math.max(0.85, 1.0 - yOffset / 600);
this.showFab = yOffset > 100;
})
.edgeEffect(EdgeEffect.None)
.scrollBar(BarState.Off)
if (this.showFab) {
this.buildFab()
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
这一百多行代码是整个页面的骨架。我们来逐一分析每一处的设计用意:
3.4.1 最外层 Stack:层级堆叠
Stack({ alignContent: Alignment.BottomEnd })
Stack 是一个层叠布局容器,子组件按声明顺序从下到上堆叠。这里使用 Stack 的唯一目的就是让 FAB(悬浮按钮) 可以浮动在页面的右下角,不受 Scroll 滚动影响。Alignment.BottomEnd 对齐方式将 FAB 定位到右下角。
3.4.2 中间层 Scroll:外层滚动容器
Scroll() {
Column() { ... }
}
Scroll 是 ArkUI 的基础滚动组件。值得注意的是,这里 没有传入 Scroller 控制器对象。这是有意为之 —— 原文注释也说明了原因:
★★★ 不传入 Scroller 控制器,避免 Preview 编译器对 Scroller 类型的依赖 ★★★
这是一个非常有实战价值的 经验总结。在 DevEco Studio 的 Preview 预览器(尤其是在早期版本)中,如果 Scroll 使用了非泛型的 Scroller 对象,预览器有时会因类型推断问题而无法渲染。省略 Scroller 控制器后,onScroll 回调仍然可以正常工作,Preview 也能顺畅渲染。
3.4.3 NestedScroll 配置:父子协同的灵魂
.nestedScroll({
scrollForward: 0, // PARENT_FIRST
scrollBackward: 1, // SELF_FIRST
})
这是整个页面最关键的三个属性调用之一。让我们用一个 滚动事件流程图 来理解这两行配置到底做了什么:
上滑操作 (scrollForward = PARENT_FIRST):
手指上滑
↓
外层 Scroll 消费滚动距离 → Banner 折叠(高度缩小 + 透明度降低)
↓
Banner 完全折叠后(滚动距离 > BANNER_FULL_HEIGHT)?
├── 否 → 继续由外层消费
└── 是 → 滚动距离传递给 → 内层 List 开始滚动内容
下滑操作 (scrollBackward = SELF_FIRST):
手指下滑
↓
内层 List 消费滚动距离 → 列表内容向上滚动
↓
列表已经滚回到顶部?
├── 否 → 继续由内层消费
└── 是 → 滚动距离传递给 → 外层 Scroll 展开 Banner
这个策略组合完美模拟了电商 App 首页的经典交互:「头图可折叠 + 列表可无限滚动」的用户心智模型。
3.4.4 onScroll 回调:驱动的视觉变化
.onScroll((xOffset: number, yOffset: number) => {
this.scrollOffset = yOffset;
this.bannerOpacity = Math.max(0.2, 1.0 - yOffset / 180);
this.bannerScale = Math.max(0.85, 1.0 - yOffset / 600);
this.showFab = yOffset > 100;
})
onScroll 是 Scroll 组件提供的滚动事件回调,每次滚动位置变化时触发。这里做了三件事:
- 更新滚动偏移量:存储
yOffset供后续使用 - 计算 Banner 透明度:
1.0 - yOffset / 180,在偏移 0~180vp 范围内从 1.0 线性下降到 0.2(Math.max(0.2, ...)截断下限) - 计算 Banner 缩放:
1.0 - yOffset / 600,下降速率更慢,产生微妙的视差层次感 - 控制 FAB 显隐:滚动超过 100vp 时显示「回到顶部」按钮
性能考量:onScroll 回调在用户频繁滚动时可能高频触发。本项目中只做了简单的数学计算和状态赋值,没有复杂运算或数组遍历,因此对帧率的影响可以忽略。如果在生产环境中需要在滚动时进行重计算(比如列表项的动态加载),建议使用防抖或节流策略。
3.4.5 滚动条与边缘效果
.edgeEffect(EdgeEffect.None)
.scrollBar(BarState.Off)
edgeEffect(EdgeEffect.None):关闭滚动到边缘时的回弹效果。这是为了避免外层 Scroll 和内层 List 都在边缘产生回弹,造成视觉上的「双重回弹」冲突。scrollBar(BarState.Off):关闭外层 Scroll 的滚动条。因为页面的滚动由内层 List 的滚动条来指示即可,展示两个滚动条会让用户感到困惑。
3.5 子组件:Banner 头部
@Builder
buildBanner() {
Column() {
Text('NestedScroll 演示')
.fontSize(26)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.shadow({
radius: 4,
color: 'rgba(0,0,0,0.3)',
offsetX: 0,
offsetY: 2,
})
Text('Tab内列表与外层滚动协同')
.fontSize(14)
.fontColor('rgba(255,255,255,0.85)')
.margin({ top: 8 })
Row() {
Text('🔍 搜索推荐内容...')
.fontSize(14)
.fontColor('rgba(255,255,255,0.65)')
}
.width('90%')
.height(40)
.backgroundColor('rgba(255,255,255,0.2)')
.borderRadius(20)
.padding({ left: 16 })
.margin({ top: 16 })
}
.width('100%')
.height(BANNER_FULL_HEIGHT)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.opacity(this.bannerOpacity)
.scale({ x: this.bannerScale, y: this.bannerScale })
.linearGradient({
direction: GradientDirection.Bottom,
colors: [
['#667EEA', 0.0], // 顶部:浅紫蓝
['#764BA2', 1.0], // 底部:深紫色
],
})
.padding({ top: 32 })
}
Banner 的设计有几个亮点:
3.5.1 视觉层次
Banner 包含三层文字内容:主标题(26fp 粗体)→ 副标题(14fp 半透明白)→ 搜索栏(圆角半透明输入框模拟)。这三层在垂直方向上形成清晰的 信息层级,用户在滚动折叠 Banner 时,视觉焦点会自然地从品牌信息过渡到列表内容。
3.5.2 渐变背景
.linearGradient({
direction: GradientDirection.Bottom,
colors: [
['#667EEA', 0.0], // #667EEA:靛蓝紫色
['#764BA2', 1.0], // #764BA2:紫罗兰色
],
})
linearGradient 是 ArkUI 的线性渐变 API。这里采用了从浅紫蓝到深紫色的垂直渐变,营造出年轻、科技感的品牌氛围。上下两种颜色的过渡在视觉上产生「深邃感」,配合随滚动变化的透明度和缩放,形成类似 3D 视差的效果。
3.5.3 阴影增强可读性
.shadow({
radius: 4,
color: 'rgba(0,0,0,0.3)',
offsetX: 0,
offsetY: 2,
})
白色文字在渐变背景上可能因背景亮度过高而难以辨认。通过添加文字投影(shadow),文字与背景之间产生了一个微小的「分离」效果,显著提升了可读性 —— 这是一个常被忽视但极其有效的无障碍设计细节。
3.6 子组件:粘性 Tab 栏
@Builder
buildTabBar() {
Row() {
ForEach(this.tabTitles, (title: string, index: number) => {
Column() {
Text(title)
.fontSize(16)
.fontWeight(this.currentTabIndex === index ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.currentTabIndex === index ? '#667EEA' : '#666666')
if (this.currentTabIndex === index) {
Divider()
.width(20)
.height(3)
.borderRadius(1.5)
.color('#667EEA')
.margin({ top: 6 })
}
}
.height(TAB_BAR_HEIGHT)
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.onClick(() => { this.onTabClick(index) })
})
}
.width('100%')
.height(TAB_BAR_HEIGHT)
.backgroundColor(Color.White)
.shadow({
radius: 4,
color: 'rgba(0,0,0,0.08)',
offsetX: 0,
offsetY: 2,
})
}
Tab 栏的设计简洁而完整:
ForEach循环渲染:基于tabTitles数组动态生成 Tab 项,新增 Tab 时只需添加数组元素- 条件高亮:通过
currentTabIndex === index控制文字粗细、颜色和指示器下划线的显隐 layoutWeight(1)等分:在Row容器中,每个 Tab 项通过layoutWeight(1)均分可用宽度- 白色背景 + 底部阴影:
backgroundColor(Color.White)让 Tab 栏在滚动时覆盖下方内容,shadow参数制造出「悬浮」的视觉效果
Tab 栏虽然没有显式设置 sticky 或 position,但由于它位于 Scroll 内部的 Column 中、Banner 的下方,当 Banner 完全折叠后,Tab 栏就自然吸附在屏幕顶部 —— 这其实是 NestedScroll 协同的副产品,而非通过粘性定位实现的。
3.7 子组件:内层 List 与条件渲染
@Builder
buildContentList() {
if (this.currentTabIndex === 0) {
this.buildListForTab(0)
} else if (this.currentTabIndex === 1) {
this.buildListForTab(1)
} else {
this.buildListForTab(2)
}
}
@Builder
buildListForTab(tabIndex: number) {
Column() {
List({ space: 0 }) {
ForEach(this.listDataSource[tabIndex], (item: string, index: number) => {
ListItem() {
this.buildListItem(item, index as number)
}
}, (item: string) => item)
}
.width('100%')
.height(this.listDataSource[tabIndex].length * LIST_ITEM_HEIGHT)
.edgeEffect(EdgeEffect.Spring)
.scrollBar(BarState.Auto)
.sticky(StickyStyle.Header)
.nestedScroll({
scrollForward: 1, // SELF_FIRST
scrollBackward: 1, // SELF_FIRST
})
}
.width('100%')
.padding({ top: 4 })
}
这是嵌套滚动体系中的 内层滚动组件。有几个关键设计点:
3.7.1 条件渲染重置滚动状态
buildContentList 中使用了 if/else if/else 条件渲染,而不是给 List 设置 .key() 属性。这是一个 刻意为之的设计决策:
★★★ 使用条件渲染替代 .key():当 currentTabIndex 变化时,ArkUI 自动销毁旧 List 并创建新 List,确保滚动状态彻底重置 ★★★
在之前的开发中,如果直接使用 .key(tabIndex) 来复用 List 组件,有时会出现滚动位置残留的问题 —— 即切换到 Tab B 时,列表的滚动位置还停留在 Tab A 的状态。条件渲染则通过 完全销毁旧组件 → 创建新组件 的方式,从根本上避免了状态残留。
当然,这种方案的代价是每次切换 Tab 都会执行组件的完整生命周期(创建 + 渲染),在列表条目数量极大时可能存在性能开销。对于每页 30 条数据的规模,这个开销可以忽略不计。
3.7.2 固定高度是故意为之
.height(this.listDataSource[tabIndex].length * LIST_ITEM_HEIGHT)
内层 List 的高度被固定为「条目数 × 每项高度」,而不是 '100%' 或自适应。这是 嵌套滚动能够正确工作的前提:
- 如果 List 高度为
'100%',它会填满父容器(Column),此时 List 本身没有溢出内容,就不会产生滚动 - 只有 List 内容区域大于其显示区域时,List 才会产生独立滚动
- 固定高度确保了 List 内容超出可视区域,从而让
NestedScroll协同机制生效
3.7.3 内层 NestedScroll 配置
.nestedScroll({
scrollForward: 1, // SELF_FIRST:上滑时内层先消费
scrollBackward: 1, // SELF_FIRST:下滑时内层先消费
})
内层 List 在两个方向上都设置为 SELF_FIRST(1),这与外层 Scroll 的配置形成 互补:
- 上滑:外层
PARENT_FIRST→ 内层SELF_FIRST,先到内层 → 内层到底 → 到外层(与外层配置衔接) - 下滑:外层
SELF_FIRST→ 内层SELF_FIRST,先到内层 → 内层到顶 → 到外层
这两个配置协同工作的逻辑链条完整且自洽。
3.7.4 sticky 头部
.sticky(StickyStyle.Header)
虽然本示例中的 ListItem 没有真正意义上的粘性头部,但在更复杂的场景中(比如带有分类标题的列表),.sticky(StickyStyle.Header) 可以让分类标题在滚动时自动吸顶。这里预先开启了该功能,为后续扩展打下了基础。
3.8 子组件:列表单项卡片
@Builder
buildListItem(title: string, index: number) {
Row() {
Text(this.getItemIcon(index))
.fontSize(28)
.width(48)
.height(48)
.textAlign(TextAlign.Center)
Column() {
Text(title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.width('100%')
Text(this.getItemDesc(index))
.fontSize(12)
.fontColor('#999999')
.margin({ top: 4 })
.width('100%')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('›')
.fontSize(22)
.fontColor('#CCCCCC')
}
.width('100%')
.height(LIST_ITEM_HEIGHT)
.padding({ left: 16, right: 16 })
.backgroundColor(Color.White)
.borderRadius(12)
.margin({ left: 12, right: 12, bottom: 8 })
.shadow({
radius: 2,
color: 'rgba(0,0,0,0.04)',
offsetX: 0,
offsetY: 1,
})
}
每个列表项是一个典型的 左图标 + 中标题 + 右箭头 三列布局:
- 左边:48×48 的 Emoji 图标,居中显示
- 中间:
layoutWeight(1)填充剩余空间的文字列,包含标题(16fp #333)和描述(12fp #999) - 右边:右箭头符号
›,暗示可点击跳转
列表项使用了 borderRadius(12) 圆角 + 轻微阴影,整体呈现出 卡片化 的视觉风格。.margin({ left: 12, right: 12, bottom: 8 }) 让每张卡片之间有 8vp 的间距,形成呼吸感。
一个值得注意的细节是 index as number 的转型。由于 ForEach 的第二个参数类型为 number | string,在某些 ArkTS 严格类型检查模式下,ForEach 泛型中的 index 类型需要显式转换才能传递给期望 number 参数的方法。这个小技巧来自 ArkTS 的类型系统约束。
3.9 子组件:「回到顶部」FAB
@Builder
buildFab() {
Button() {
Text('↑ 回到顶部')
.fontSize(16)
.fontColor(Color.White)
}
.height(44)
.borderRadius(22)
.backgroundColor('#667EEA')
.shadow({
radius: 8,
color: 'rgba(102,126,234,0.5)',
offsetX: 0,
offsetY: 4,
})
.padding({ left: 20, right: 20 })
.margin({ right: 20, bottom: 32 })
.onClick(() => {
this.showFab = false;
this.bannerOpacity = 1.0;
this.bannerScale = 1.0;
this.scrollOffset = 0;
})
}
FAB(Floating Action Button)是一个圆角按钮,当 showFab 为 true 时出现于页面右下角。
点击 FAB 时,由于我们 没有使用 Scroller 的 scrollTo 方法,而是直接重置了状态变量,Scroll 的位置会通过另一种机制归零。在 ArkUI 中,最可靠的做法是持有 Scroller 对象并调用 scrollTo({ yOffset: 0 })—— 本项目中省略了 Scroller 对象(为了 Preview 兼容性),但重置状态变量的方式同样可以让 Banner 视觉回退到初始状态。
关于 onClick 中的状态重置:
this.showFab = false; // 隐藏 FAB
this.bannerOpacity = 1.0; // 恢复 Banner 透明度
this.bannerScale = 1.0; // 恢复 Banner 缩放比例
this.scrollOffset = 0; // 重置滚动偏移
虽然这些操作会立即更新 UI,但 外层 Scroll 的实际滚动位置可能并没有归零(因为没调用 scrollTo)。这意味着 Banner 的视觉属性会瞬间恢复,但页面的物理滚动位置可能需要在用户下次滑动时重新同步。这是一个已知的简化 —— 在实际项目中,建议使用 Scroller 的 scrollToEdge(Edge.Top) 方法来确保物理滚动位置和 UI 状态双重归零。
3.10 Tab 切换事件处理
private onTabClick(index: number): void {
if (this.currentTabIndex === index) {
return;
}
this.currentTabIndex = index;
}
Tab 切换逻辑非常简洁:
- 如果点击的是当前已选中的 Tab,不做任何处理(
return) - 否则更新
currentTabIndex→ 触发条件渲染重建 List → 新 List 的滚动位置自然归零
为什么不需要显式调用 scrollTo? 因为条件渲染会销毁旧的 List 组件并创建新的 List 组件,新组件初始化时滚动位置就是 0。这比调用 scrollTo 更干净 —— 无需处理动画等待、无需考虑竞态条件。
第四章:Ability 生命周期 —— 应用的后台管理
4.1 EntryAbility:主能力单元
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
try {
this.context.getApplicationContext()
.setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
} catch (err) {
hilog.error(DOMAIN, 'testTag',
'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
}
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag',
'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
// ... onDestroy, onWindowStageDestroy, onForeground, onBackground
}
这是一个标准的 Stage 模型 Ability 生命周期实现:
onCreate:在 Ability 创建时调用,这里设置颜色模式为COLOR_MODE_NOT_SET(跟随系统设置)。注意整个操作被包在try/catch中,因为颜色模式设置可能在某些设备上抛出异常,这是一个良好的防御性编程实践。onWindowStageCreate:窗口创建后加载首页内容。loadContent的第二个参数是异步回调,返回的err对象包含code和message字段。
生命周期日志统一使用了 hilog(鸿蒙日志系统)的 info 级别输出,便于在 DevEco Studio 的 Log 面板中跟踪 Ability 的状态流转。
4.2 EntryBackupAbility:备份恢复能力
export default class EntryBackupAbility extends BackupExtensionAbility {
async onBackup() {
hilog.info(DOMAIN, 'testTag', 'onBackup ok');
await Promise.resolve();
}
async onRestore(bundleVersion: BundleVersion) {
hilog.info(DOMAIN, 'testTag',
'onRestore ok %{public}s', JSON.stringify(bundleVersion));
await Promise.resolve();
}
}
BackupExtensionAbility 是 HarmonyOS 提供的应用备份恢复扩展能力。用户在使用「手机克隆」、「华为云备份」等功能时,系统会调用 onBackup 和 onRestore 生命周期方法,让应用有机会保存和恢复自身数据。
在本示例中,onBackup 和 onRestore 只是记录了日志并 await Promise.resolve() —— 这是一个骨架实现。在生产环境中,这里应该写入应用的持久化数据(如 Preferences 存储的内容)。
4.3 能力组件注册
在 module.json5 中,EntryBackupAbility 被注册为 "type": "backup",并关联了 backup_config.json:
{
"name": "ohos.extension.backup",
"resource": "$profile:backup_config"
}
而 backup_config.json 的内容是:
{
"allowToBackupRestore": true
}
这个简单的配置告诉系统:「本应用允许参与系统级备份和恢复」。很多开发者容易忽略这个步骤 —— 如果不配置 backup_config 或者没有声明 backup 类型的扩展能力,应用数据将不会出现在用户的备份中。
第五章:构建配置与依赖管理
5.1 项目级构建配置(build-profile.json5)
{
"app": {
"products": [{
"name": "default",
"signingConfig": "default",
"targetSdkVersion": "6.1.0(23)",
"compatibleSdkVersion": "6.1.0(23)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true
}
}
}],
"buildModeSet": [
{ "name": "debug" },
{ "name": "release" }
]
},
"modules": [
{ "name": "entry", "srcPath": "./entry", "targets": [
{ "name": "default", "applyToProducts": ["default"] }
]}
]
}
这里有几个值得注意的配置项:
caseSensitiveCheck: true:开启大小写敏感检查。在 ArkTS 中,文件名和导入路径的大小写必须严格匹配。这是 6.1.0 版本引入的更严格的构建检查。useNormalizedOHMUrl: true:使用标准化的 OHM(OpenHarmony Module)URL 格式,确保模块间依赖解析的正确性。buildModeSet定义了 debug/release 两种构建模式,在 DevEco Studio 中通过工具栏切换。
5.2 模块级构建配置(entry/build-profile.json5)
{
"apiType": "stageMode",
"buildOption": {
"resOptions": {
"copyCodeResource": { "enable": false }
}
},
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": false,
"files": ["./obfuscation-rules.txt"]
}
}
}
}
],
"targets": [{ "name": "default" }, { "name": "ohosTest" }]
}
"apiType": "stageMode" 确认了这是 Stage 模型的应用(而非 FA 模型)。混淆配置被设置为 enable: false,在 debug 阶段不做混淆以方便调试;发布 release 版本时,可以开启混淆来减小包体积并增强代码安全性。
5.3 包依赖管理
根目录的 oh-package.json5:
{
"modelVersion": "6.1.0",
"devDependencies": {
"@ohos/hypium": "1.0.25",
"@ohos/hamock": "1.0.0"
}
}
@ohos/hypium:鸿蒙官方单元测试框架,提供describe/it/expect等 BDD 风格的测试 API@ohos/hamock:鸿蒙 Mock 框架,用于在测试中模拟依赖和服务
两个都是 devDependencies,意味着它们只在开发和测试阶段引入,不会打包到正式发布的应用中。dependencies 字段为空 —— 这个应用没有外部运行时依赖,所有功能均使用 ArkUI 的内置组件实现。
5.4 Hvigor 构建配置
{
"modelVersion": "6.1.0",
"execution": {
// "parallel": true,
// "incremental": true,
// "typeCheck": false
},
"logging": { "level": "info" }
}
Hvigor 是 HarmonyOS 的构建工具,类比于 Android 的 Gradle 或前端领域的 Webpack。大部分执行选项(并行编译、增量编译、类型检查)都使用了默认值。注释掉的配置行展示了可调的优化选项,供开发者按需开启。
第六章:主题与资源管理 —— 暗色模式适配
6.1 浅色与深色主题的颜色配置
entry/src/main/resources/base/element/color.json(浅色模式):
{
"color": [
{ "name": "start_window_background", "value": "#FFFFFF" }
]
}
entry/src/main/resources/dark/element/color.json(深色模式):
{
"color": [
{ "name": "start_window_background", "value": "#000000" }
]
}
HarmonyOS 的资源系统支持 基于限定词的资源目录。base 目录存放默认(浅色模式)资源,dark 目录存放深色模式资源。应用在运行时根据系统主题自动切换资源文件,无需手动编写判断逻辑。
这里配置了启动窗口背景色:浅色模式为纯白(#FFFFFF),深色模式为纯黑(#000000)。当用户从最近任务列表切换应用时,startWindowBackground 会作为启动窗口的背景色显示,直到应用的主窗口渲染完毕。这个简单的配置可以避免启动时的白屏闪烁或黑屏闪烁。
6.2 字符串与字体大小资源
string.json 定义了模块描述和 Ability 标签的多语言字符串。float.json 定义了一个 page_text_font_size: 50fp —— 这通常用于启动引导页或其他特殊页面的超大字重,在本项目中未被使用,保留了扩展空间。
6.3 媒体资源
layered_image.json 定义了分层图标:
{
"layered-image": {
"background": "$media:background",
"foreground": "$media:foreground"
}
}
分层图标是 HarmonyOS 自适应图标的标准格式,由背景层和前景层组成。系统会在不同设备上对两层图标采用不同的处理(如圆形遮罩、缩放等),确保图标在所有设备上以统一的视觉风格呈现。background.png 和 foreground.png 两张位图就存放在 base/media/ 目录中。
第七章:测试体系 —— 为质量护航
7.1 本地单元测试
LocalUnit.test.ets 展示了一个基础的测试用例:
export default function localUnitTest() {
describe('localUnitTest', () => {
beforeAll(() => { /* 所有测试开始前的准备工作 */ });
beforeEach(() => { /* 每个测试用例前的准备工作 */ });
afterEach(() => { /* 每个测试用例后的清理工作 */ });
afterAll(() => { /* 所有测试结束后的清理工作 */ });
it('assertContain', 0, () => {
let a = 'abc';
let b = 'b';
expect(a).assertContain(b);
expect(a).assertEqual(a);
});
});
}
测试框架采用了 hypium(鸿蒙单元测试框架)的 BDD(行为驱动开发)风格 API。这个简单的测试验证了字符串包含关系(assertContain)和等价关系(assertEqual)。
7.2 测试模块注册
List.test.ets 作为测试列表入口,导入了所有测试套件:
import localUnitTest from './LocalUnit.test';
export default function testsuite() {
localUnitTest();
}
当应用在 DevEco Studio 中以 ohosTest 目标运行测试时,系统会自动发现并执行 ets/test/ 目录下的测试文件。测试结果会显示在运行面板中,支持覆盖率统计。
7.3 测试模块配置
{
"module": {
"name": "ohosTest",
"type": "test",
"srcPath": "./ets/test/",
// ...
}
}
测试代码作为独立的模块存在,与主应用代码完全隔离。这遵循了 关注点分离 的原则:测试代码仅在测试构建变体中编译和执行,不会影响应用的发布包大小。
第八章:开发踩坑实录 —— 那些我们在 NestedScroll 上踩过的坑
在开发和调试 NestedScroll 的过程中,我积累了一些经验教训,在这里分享给读者,希望能帮大家少走弯路。
8.1 坑一:Scroller 对象引起 Preview 渲染失败
现象:在 DevEco Studio 的 Preview 面板中,页面渲染为空白,控制台提示类型错误。
原因:当 Scroll 组件显式传入 Scroller 对象时,Preview 编译器在处理继承自 ScrollController 的 Scroller 类型时存在类型推断问题。
解决方案:省略 Scroll 的 scroller 参数,使用无参构造函数,通过 onScroll 回调间接获取滚动状态。
// ❌ 不推荐(Preview 可能失败)
private scroller: Scroller = new Scroller();
Scroll(this.scroller) { ... }
.onScrollFrame(() => { ... })
// ✅ 推荐(Preview 兼容)
Scroll() { ... }
.onScroll((xOffset, yOffset) => { ... })
8.2 坑二:内层 List 高度未固定导致嵌套滚动失效
现象:外层 Banner 滚动正常,但内层 List 无法独立滚动。
原因:List 的高度设置为 '100%' 或没有显式设置,导致 List 内容没有溢出容器,无法产生滚动事件。
解决方案:手动计算 List 的固定高度。
// ❌ 无法触发嵌套滚动
List() { ... }
.height('100%')
// ✅ 正确:高度 = 条目数 × 每项高度
List() { ... }
.height(itemCount * LIST_ITEM_HEIGHT)
8.3 坑三:Tab 切换后 List 滚动位置残留
现象:切换到 Tab B 后,列表的滚动位置还停留在 Tab A 的状态。
原因:在 if 条件切换时,如果使用 buildListForTab(currentTabIndex) 这种形式,ArkUI 可能会复用旧的 List 实例,保留其内部滚动状态。
解决方案:
// ❌ 可能残留旧滚动位置
this.buildListForTab(this.currentTabIndex)
// ✅ 条件渲染强制重建
if (this.currentTabIndex === 0) { this.buildListForTab(0) }
else if (this.currentTabIndex === 1) { this.buildListForTab(1) }
else { this.buildListForTab(2) }
另一种方案是给 List 设置 .key(tabIndex.toString()),但在早期 SDK 上效果不稳定,条件渲染是更可靠的方案。
8.4 坑四:内外双层回弹效果冲突
现象:滚动到边缘时,外层 Scroll 和内层 List 同时产生回弹动画,视觉上非常突兀。
原因:默认情况下,Scroll 和 List 都有 edgeEffect 效果,在嵌套滚动中两者边缘叠加。
解决方案:
// 外层 Scroll:禁用边缘效果
Scroll() { ... }
.edgeEffect(EdgeEffect.None)
// 内层 List:保留边缘效果(Spring 回弹更流畅)
List() { ... }
.edgeEffect(EdgeEffect.Spring)
8.5 坑五:FAB 点击后视觉归位但物理滚动位置未归零
现象:点击「回到顶部」按钮后,Banner 的透明度和缩放立刻恢复了,但再次上滑时 Banner 不会立刻折叠——需要滑动一段距离后才同步。
原因:只重置了 UI 状态变量(bannerOpacity、bannerScale),但没有调用 Scroller.scrollTo(0) 让外层 Scroll 的物理滚动条归零。
解决方案:持有 Scroller 对象并在 FAB 点击时调用:
this.scroller.scrollTo({ yOffset: 0, animation: { duration: 300 } });
同时结合状态重置,确保 UI 状态和物理滚动位置双重归零。
第九章:性能优化与最佳实践
9.1 滚动性能
onScroll 回调在用户快速滑动时可能以每秒 60 帧的频率触发。本项目的回调中只做了简单的数学运算和状态赋值,性能开销微乎其微。但如果需要在滚动时做更多工作(如网络懒加载、复杂计算),建议:
- 使用
requestAnimationFrame节流:将非必要的计算延迟到下一个绘制帧 - 避免在 onScroll 中触发状态变更循环:ArkUI 的
@State变更会触发组件重绘,在 onScroll 中频繁修改状态可能导致额外的布局计算 - 考虑
LazyForEach:当列表数据量超过 100 项时,使用LazyForEach替代ForEach,实现按需渲染
9.2 列表性能
本示例使用了 ForEach,因为它简单直观且适用于演示。对于生产环境中的长列表场景,ArkUI 提供了更优的选择:
| 特性 | ForEach |
LazyForEach |
|---|---|---|
| 渲染方式 | 一次性全部渲染 | 按需渲染(仅渲染可视区域 + 缓冲区) |
| 数据量 | ≤100 项 | 不限(理论上支持万级) |
| 内存占用 | 随数据量线性增长 | 相对固定(仅缓存可视区域附近的组件) |
| 使用复杂度 | 简单 | 需要实现 IDataSource 接口 |
9.3 视觉优化
本项目已经实施了几项有效的视觉优化:
- 视差效果:Banner 的透明度和缩放使用不同的变化速率(
/180vs/600),产生微妙的层次感 - 圆角卡片:列表项使用
borderRadius(12)+ 轻阴影,符合 Material Design 3 的卡片设计语言 - 渐变背景:Banner 的线性渐变代替纯色背景,在折叠过程中依然保持视觉丰富度
- 文字投影:Banner 标题的文字阴影确保可读性不因渐变背景而降低
9.4 无障碍设计
几点容易被忽略的无障碍建议:
- 颜色对比度:列表标题
#333333在白色背景上对比度约为 11.4:1,远高于 WCAG AA 要求的 4.5:1 - 描述文字:
#999999在白色背景上对比度为 3.2:1,仅满足大文字 AA 标准——如果需要更好的可读性,建议加深到#666666 - FAB 的可访问标签:虽然 Text 按钮上的「↑ 回到顶部」对可视用户足够清晰,但仍建议为按钮添加
accessibilityText属性,供屏幕阅读器使用
第十章:从演示到生产 —— 如何扩展这个示例
10.1 替换 Mock 数据为真实 API
当前的数据源是本地生成的模拟数据。接入真实 API 时,可以:
- 定义数据模型接口(
ContentItem) - 注入 HTTP 请求客户端(使用
@ohos.net.http或第三方库) - 将
listDataSource改为@State变量并异步更新
interface ContentItem {
id: string;
title: string;
description: string;
icon: string;
category: string;
}
@State realData: ContentItem[] = [];
aboutToAppear() {
this.fetchData();
}
async fetchData() {
// 使用 @ohos.net.http 发起网络请求
// 解析响应 → 更新 this.realData
}
10.2 添加下拉刷新
ArkUI 的 Refresh 组件可以为 List 添加下拉刷新能力,配合 NestedScroll 协同策略确保不与 Banner 折叠冲突。
10.3 支持动态 Tab
将 tabTitles 和 listDataSource 改为 @State 动态管理,Tab 栏改用 Swiper + List 组合实现左右滑动切换。
10.4 跨设备适配
利用 ArkUI 响应式布局和 BreakpointSystem,让页面在大屏上展示为多列网格。
第十一章:HarmonyOS 6.1.0 与 API 24 的新特性
API 24(HarmonyOS 6.1.0)相比早期版本带来了多项进步:
11.1 声明式 UI 成熟
ArkUI 声明式语法对 TypeScript 支持更完整,条件渲染、列表复用等能力有了标准写法。
11.2 构建工具链改进
Hvigor 提升了增量编译准确性和速度,混淆和资源压缩控制更精细。
11.3 安全性增强
应用权限管理、隐私保护要求更严格,copyCodeResource 选项加强对代码资源的保护。
11.4 备份恢复完善
BackupExtensionAbility 支持更细粒度的数据备份策略指定。
第十二章:心得体会与进阶建议
12.1 从 Web 开发到 ArkUI 的思维转变
如果你和我一样,有 Web 前端或 React Native 的开发背景,接触 ArkUI 时可能会经历一个适应期。最大的转变在于:
- 布局理念:从 CSS 的盒模型 + 流式布局 → ArkUI 的 Flex 容器 + 声明式属性链
- 状态管理:从 React 的 Hooks/Redux → ArkUI 的
@State/@Prop/@Link装饰器体系 - 滚动机制:从
overflow: scroll的隐式滚动 → ArkUI 的显式Scroll/List组件 +NestedScroll协议
这些差异一开始可能看起来是学习成本,但实际写下来会发现,ArkUI 的声明式 API 设计得相当简洁——组件属性链式调用、状态自动追踪 UI 更新、布局容器直观命名,都让开发体验非常顺畅。
12.2 学习资源推荐
深入学习路径:华为开发者联盟 ArkUI 指南 → AtomGit Sample 项目 → DevEco Studio Preview 实战 → 社区博客经验分享。
12.3 下一步方向
完成这个 NestedScroll 示例后,你可以尝试以下进阶方向:
- 添加共享元素过渡动画:点击列表项跳转详情页时,使用
sharedTransition共享元素过渡 - 实现短视频 Feed 流:基于 NestedScroll 实现类似 TikTok 的上下滑动短视频流
- 接入端云协同:使用
@kit.CloudService将应用与华为云服务对接 - 多端部署:将页面适配到折叠屏和平板,利用 ArkUI 的响应式布局能力
总结
本文对一个基于 HarmonyOS API 24 的 NestedScroll 嵌套滚动协同应用进行了全面的技术剖析,涵盖项目结构、构建配置、核心代码、Ability 生命周期、资源管理、测试体系、性能优化和生产化扩展等关键环节。
通过这个项目,我们可以看到 ArkUI 在滚动交互领域的成熟度 —— NestedScroll API 用寥寥几行配置就实现了原本需要大量代码才能完成的嵌套滚动协同,而 @State + 链式属性绑定的响应式体系让 UI 状态管理变得异常简洁。
鸿蒙生态正在快速发展,ArkUI 作为其原生的声明式 UI 框架已从追赶期进入体验打磨期。希望本文能帮助你理解 NestedScroll 的工作原理,掌握鸿蒙开发的核心实践。
附录
A. 项目元信息
| 项目 | 值 |
|---|---|
| 包名 | com.example.app6106 |
| 版本名 | 1.0.0 |
| 版本号 | 1000000 |
| API 版本 | 24(SDK 6.1.0 / SDK 23) |
| 应用模型 | Stage 模式 |
| 语言 | ArkTS(TypeScript 方言) |
| 构建工具 | Hvigor 6.1.0 |
B. 项目源码文件索引
| 文件路径 | 功能 |
|---|---|
| entry/src/main/ets/pages/Index.ets | 主页面:NestedScroll 嵌套滚动协同(核心代码) |
| entry/src/main/ets/entryability/EntryAbility.ets | 主 Ability:应用生命周期管理 |
| entry/src/main/ets/entrybackupability/EntryBackupAbility.ets | 备份恢复扩展能力 |
| entry/src/main/module.json5 | 模块配置:声明 Ability 和扩展能力 |
| AppScope/app.json5 | 应用级配置:包名、版本、图标 |
| build-profile.json5 | 项目级构建配置 |
| entry/build-profile.json5 | 模块级构建配置 |
| hvigor/hvigor-config.json5 | Hvigor 构建工具配置 |
| entry/src/test/LocalUnit.test.ets | 本地单元测试 |
| entry/src/test/List.test.ets | 测试列表入口 |
C. 常用链接
- 华为开发者联盟:https://developer.huawei.com/consumer/cn/
- ArkUI 开发指南:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkui-overview
本文由 AtomCode(deepseek-v4-flash)辅助生成,基于 D:\hongmeng\app6106 项目源码撰写。项目使用 HarmonyOS API 24,运行于 HarmonyOS 6.1.0 环境。


更多推荐




所有评论(0)