HarmonyOS 6.1.1:Tabs 支持嵌套滚动后,复杂页面终于更顺手了
两层页签放在一起,问题不只是布局

很多业务页面都会出现双层 Tabs。外层负责切换“工作台、报表”等业务模块,内层负责切换“概览、明细、日志”等当前模块中的分类。静态页面看起来只是两行标签,用户真正左右拖动时,却会出现手势归属问题。
手指落在内层区域,内层 Tabs 还没有到边界时,通常应该优先处理;内层已经滑到最后一项,用户继续向同一方向拖动时,产品又可能希望外层接手。另一类页面更重视区域稳定,即使内层到达边界,也不希望一次手势改变外层模块。
如果开发者不能明确控制这条交接规则,用户会遇到两种相反体验:内层还没操作完,外层已经误切;内层到边界后继续拖动,页面却像卡住一样没有反馈。问题发生在手势链路,但用户只会觉得页面“不跟手”。
HarmonyOS 6.1.1 为 Tabs 补充 nestedScroll 能力,并提供 TabsNestedScrollMode。本文从前端界面工程师角度讨论如何组织双层 Tabs、记录父子索引、验证边界交接,并特别区分“按钮设置出来的说明状态”和“真实拖动产生的运行证据”。
当前 Demo 已完成页面代码、首页入口和路由注册。现有环境记录尚不能证明 02 已独立完成构建和真实手势验证,因此本文对 SELF_FIRST 与 SELF_ONLY 的行为使用 API 语义和待验证条件表述,不把辅助按钮生成的状态写成运行结果。发布前必须补齐 API 24 环境下的真实连续拖动证据。
先把父层和子层的职责说清楚

外层 Tabs 表示大的业务上下文。用户从“工作台”切到“报表”,页面含义可能完全变化。内层 Tabs 表示当前上下文中的局部分类,从“概览”切到“日志”通常不会离开外层业务。
这两层虽然都响应横向手势,操作代价不同。外层误切会让用户失去当前任务位置,内层到边界后无法接续则会增加重复操作。因此嵌套滚动不是动画细节,而是导航层级与手势消费策略之间的映射。
设计页面前应先回答三个问题:
- 用户在内层区域拖动时,哪一层优先响应。
- 内层到达首尾边界后,剩余手势是否交给外层。
- 页面如何让用户和测试人员知道此次变化发生在哪一层。
前两个问题由嵌套滚动模式决定,第三个问题需要页面状态和反馈设计。只配置 API 而不记录父子索引,真实测试出现误切时很难定位。
SELF_FIRST 与 SELF_ONLY 的选择逻辑

根据当前 API 语义,SELF_FIRST 表示内层 Tabs 优先处理滚动。当内层仍能切换时,手势由内层消费;到达边界后,继续拖动可以进入父层接续判断。它适合希望形成连续导航体验的页面。
SELF_ONLY 表示内层 Tabs 只处理自身滚动,不主动把同一次手势交给父层。它适合外层模块切换代价较高、需要防止误触的页面。
选择模式不能只看哪一种“更顺”。消息中心可能希望内层分类到边界后自然切换外层收件箱;表单编辑页却可能要求外层绝不因局部拖动改变,避免未保存内容离开。模式是产品规则的技术表达。
本文 Demo 使用外层“工作台/报表”和内层“概览/明细/日志”形成最小层级。测试时同一起点、同一方向和同样拖动距离很重要,否则两种模式的结果没有可比性。
页面状态必须分别记录两层索引

Demo 不使用一个通用 currentIndex,而是为父子 Tabs 分别保存索引、边界状态和最近一次操作。
@State nestedMode: TabsNestedScrollMode =
TabsNestedScrollMode.SELF_FIRST;
@State outerIndex: number = 0;
@State innerIndex: number = 0;
@State edgeState: string = '未到达边界';
@State lastGesture: string = '等待操作';
@State interactionCount: number = 0;
父子索引分开后,一次真实拖动至少可以产生两个明确观察值。内层从 1 变成 2、外层保持 0,表示当前变化发生在子层;内层已经是 2,继续拖动后外层从 0 变成 1,才可能支持边界后接续的判断。
edgeState 和 lastGesture 用于把瞬时手势转成可读信息。它们应由真实回调或验证逻辑更新,并附带操作计数。页面只显示最终标签,不足以说明用户经过了哪条路径。
nestedScroll 应绑定页面当前模式

内层 Tabs 使用同一套结构,模式作为状态直接绑定:
Tabs({
index: this.innerIndex,
controller: this.innerTabsController
}) {
// 概览、明细、日志
}
.nestedScroll(this.nestedMode)
.barMode(BarMode.Scrollable)
.onChange((index: number) => {
this.innerIndex = index;
})
这种写法避免为两种模式复制两套页面。切换 nestedMode 后,内层 Tabs 使用新的策略,内容、索引显示和测试入口仍保持一致。
模式切换时建议重置父子索引和边界状态。否则用户在 SELF_FIRST 下已经把外层切到“报表”,再改成 SELF_ONLY,随后截图或日志的起点就不同。对照实验必须从外层 0、内层 0 等明确基线开始。
TabsController 的作用与边界
Demo 为父子组件分别创建 TabsController,用于重置和辅助定位页签。
private readonly outerController: TabsController = new TabsController();
private readonly innerController: TabsController = new TabsController();
private resetScenario(): void {
this.outerIndex = 0;
this.innerIndex = 0;
this.outerController.changeIndex(0);
this.innerController.changeIndex(0);
this.edgeState = '未到达边界';
this.lastGesture = '等待真实拖动';
}
控制器适合建立可重复起点,也可以帮助用户快速回到内层边界。但 changeIndex() 是程序主动修改页签,不是手势分发结果。通过按钮直接把外层改成 1,只能说明页面能显示那个状态,不能证明 nestedScroll 把手势从内层交给外层。
现有 Demo 中“到达内层边界”“继续拖动”按钮原本用于固定说明画面,其中 continueDragAtEdge() 根据模式直接调用外层控制器。这个设计可以作为原理演示,却不能进入真实运行证据链。文章将它重新定义为“状态复现辅助区”,发布结论只接受真实拖动和真实索引变化。
为什么辅助状态容易被误认为证据

截图只呈现某个时刻。读者看到外层“报表”、内层“日志”和 SELF_FIRST,很容易认为画面来自一次真实边界拖动。如果实际是按钮直接设置索引,图片只能证明预期结果页面长什么样。
辅助状态仍有用途:它能解释公平对照的起点,帮助测试人员理解应观察哪些字段,也能在没有设备时评审页面信息是否可读。但图注必须标记“操作说明”或“预期状态”,不能写成“手势接续成功”。
真实证据至少要包含操作前状态、到达边界状态、继续真实拖动后的状态,并有索引或事件记录证明变化顺序。静态文章可以使用连续三帧;如果平台支持视频或动图,可以保留完整手势,同时从中抽取关键帧用于审稿。
设计一条公平的真实验证路径
建立统一基线
选择 SELF_FIRST,重置外层为“工作台”、内层为“概览”,清空事件记录。记录模式、父子索引和设备环境。
让内层走到右边界
手指或鼠标在内层内容区域向左拖动,依次让内层从“概览”进入“明细”和“日志”。每次 onChange 更新内层索引,外层应保持在“工作台”。
在边界继续真实拖动
保持手势起点仍在内层区域,继续向相同方向拖动。记录外层索引、内层索引和事件顺序。只有此处的真实变化才能验证边界接续。
切换模式后重复
选择 SELF_ONLY,完全重置页面,使用相同起点、方向和操作步骤。两组结果从同一父子索引开始,才能比较模式差异。
如果拖动距离、速度或起点不同,应在记录中注明。手势识别受命中区域和阈值影响,不宜只做一次就形成结论。
事件记录应该包含什么
页面当前只有父子 onChange 可以记录最终索引,还需要将模式、序号和时间组合成可读日志。
private recordChange(layer: 'outer' | 'inner', index: number): void {
this.interactionCount += 1;
this.lastGesture =
`#${this.interactionCount} ${layer} -> ${index} (${this.modeName()})`;
}
内层和外层回调分别调用这个方法。连续操作后,如果日志顺序是 inner -> 1、inner -> 2、outer -> 1,它比最终画面更能说明交接过程。
更严格的 Demo 可以保留最近若干条事件,而不是只显示最后一条。测试人员能够确认外层是否在内层到边界之前提前响应,也能发现一次拖动触发多次变化。
日志只能证明回调和索引变化,仍要与实际手势录像或连续帧结合。程序主动调用 changeIndex() 也会改变索引,因此记录里还应标记事件来源是 gesture、reset 还是 helper。
触摸命中区域会影响判断
用户从哪里开始拖动非常重要。如果手势起点落在外层标签栏、内层标签栏、内容区或某个可横向滚动的子组件,参与竞争的手势识别器可能不同。
验证说明要明确要求从内层内容区域开始,并保证该区域没有另一个横向滑动组件。否则外层变化可能来自直接操作外层,而不是嵌套接续。
页面中的纵向 Scroll 也需要考虑。如果用户斜向拖动,系统可能先判断主要方向。测试动作应尽量保持水平,并在真机上重复多次,避免鼠标模拟与触摸手势差异。
复杂业务里还可能出现 Swiper、列表滑动删除或横向图表。接入 nestedScroll 前应梳理同一区域内所有横向手势,确定优先级,而不是只看两层 Tabs。
常见异常怎样排查
内层还没到边界,外层已经变化
先确认模式是否为预期值,再检查手势起点是否落在内层。查看事件日志中父子回调顺序。如果外层首先变化,问题可能是命中区域或组件结构,而不是边界接续。
到达边界后外层没有反应
确认内层索引确实位于首项或末项,拖动方向是否指向边界外,并检查当前模式。一次很短的鼠标拖动可能没有达到切换阈值,应在相同条件下重复。
状态文字变化,页签选中态没变化
这通常是页面手动更新了说明字段,却没有收到 Tabs 回调,或索引与控制器不同步。证据应以真实选中态和回调为准,不能只看说明文字。
两种模式看起来完全一样
先确认测试是否真正发生在内层边界。内层仍有可切换项时,两种模式都可能表现为内层响应;差异通常要在边界后继续拖动才出现。若条件正确仍无差异,再检查 API 版本和运行环境。
Pad 安装失败
如果 HAP 与设备的 API 或 releaseType 不匹配,失败发生在页面启动前。此时不能分析嵌套滚动行为,应使用匹配 API 24 的镜像完成安装,再进入手势验证。
这项能力如何进入产品决策
SELF_FIRST 更适合用户希望连续浏览的结构。例如内层是内容分类,外层是相邻业务区,边界后接续符合空间方向。
SELF_ONLY 更适合外层切换代价高的结构。例如内层包含编辑状态、外层切换会丢失上下文,产品更重视防误触。
还可以不依赖手势接续,保留明确按钮或外层标签点击。嵌套滚动提供选择,不代表所有页面都应该让两层形成手势接力。产品设计应考虑用户能否理解层级、错误切换的恢复成本和无障碍操作。
前端实现完成后,最好让产品、测试和开发共同确认边界行为。用“内层优先、边界后是否交接”描述规则,比只在需求中写 SELF_FIRST 更便于团队理解。
三类页面应该怎样选择
第一类是内容浏览页。外层表示频道,内层表示频道中的分类,用户主要目标是连续浏览。内层到最后一项后接续外层,可能减少返回和重新定位操作。这类页面可以优先评估 SELF_FIRST,但仍要确认跨层切换不会让用户迷失。
第二类是任务处理页。外层表示待办、已办或不同业务单据,内层表示当前任务的详情区。误切外层可能导致正在填写的内容离开,恢复成本高。这类页面更适合 SELF_ONLY,再提供明确外层点击入口。
第三类是数据分析页。外层表示报表类型,内层表示时间范围或指标分类,页面里还可能有横向图表。此时不只两层 Tabs 竞争手势,图表拖动也会参与。产品应先确定图表操作优先级,再决定是否允许 Tabs 边界接续。单独配置 SELF_FIRST 可能无法解决全部冲突。
还有一种选择是避免同方向嵌套。内层改用分段按钮、下拉选择或纵向列表,能从结构上减少手势竞争。新 API 提供控制能力,但信息架构仍然是第一层解决方案。
无障碍和非手势操作不能缺席
嵌套滚动主要讨论触摸和鼠标拖动,但页面不能只依赖手势。外层、内层标签仍应支持点击、键盘焦点和系统辅助能力,让不能稳定执行横向拖动的用户完成切换。
状态反馈也不能只依赖颜色。模式、当前父层、当前子层和边界信息应有文本表达。发生跨层切换时,标题和内容变化要足够明确,避免用户只看到颜色变化却不知道导航层级已改变。
在大屏、折叠屏或键鼠环境中,用户可能更常直接点击标签。此时嵌套滚动不是主操作方式,但它仍影响触控场景。测试矩阵应覆盖触摸、鼠标拖动和标签点击,确认不同输入不会让索引状态失步。
如果外层切换会丢失编辑内容,还要在导航层提供保存或确认机制。手势策略不能代替业务防护,SELF_ONLY 也不是数据安全保证。
建立可以重复执行的测试用例

测试不应只写“切换两种模式看看效果”,而要定义初始状态、动作和预期观察项。
用例一验证内层正常切换。外层 0、内层 0,从内层区域拖动到内层 1。预期只出现内层回调,外层保持 0。两种模式都执行,确认普通子层操作没有意外改变父层。
用例二验证右边界。外层 0、内层 2,从内层区域继续向左拖动。分别记录两种模式的父子索引和事件顺序,不预先用辅助按钮设置结果。
用例三验证左边界。外层 1、内层 0,向右拖动。左右方向都覆盖,避免实现只在一个边界符合预期。
用例四验证模式切换。页面处于非默认索引时切换模式,确认是否按设计重置。若产品决定保留当前位置,也要在文章和测试中明确,不能让两组对照起点随机变化。
用例五验证快速连续手势。连续拖动多次,观察是否跨过多个页签、回调顺序是否稳定,以及交互计数是否与实际动作大致一致。
用例六验证斜向手势。在内层区域做轻微斜向拖动,观察纵向 Scroll 和横向 Tabs 的方向判定,确保页面不会因为滚动方向竞争出现抖动。
用例七验证点击与拖动混合。先点击内层标签,再从内容区拖动,确认控制器索引、@State 和实际选中态一致。
每个用例至少记录模式、起始父子索引、手势区域、方向、结束索引和事件日志。这样同一用例可以在模拟器、真机和后续系统版本重复执行。
代码层还可以怎样提高可测试性
页面现在把模式、索引和提示文字放在一个组件中,便于演示。进入生产项目后,可以把交互状态整理成独立模型,让 UI 只负责展示和接收回调。
模型提供 reset()、onOuterChanged()、onInnerChanged() 和 recordGestureSource() 等明确入口。单元测试可以检查索引与日志状态,组件测试负责验证 Tabs 回调是否正确接入。
辅助按钮也应显式标记事件来源。调用控制器时写入 helper,真实 onChange 根据当前输入标记 gesture 或 controller。虽然框架回调未必直接告诉来源,但 Demo 可以在触发前设置短生命周期标志,避免日志把程序定位误写成用户拖动。
测试模型不能替代真实手势测试。它保证页面自己的状态逻辑,nestedScroll 的父子消费仍由框架运行行为决定。两层测试结合,才能区分页面状态错误和组件行为差异。
小结
Tabs 嵌套滚动解决的是父子组件之间的手势归属,不是给页面增加一项视觉效果。前端工程师要同时处理组件模式、父子状态、命中区域、边界条件和反馈记录。
SELF_FIRST 与 SELF_ONLY 的差异必须在同一起点、真实拖动和明确索引记录下验证。按钮直接设置页签可以帮助解释预期状态,却不能替代运行证据。把这条边界说清楚,文章才能既有足够实现细节,又不把设计演示写成已经验证的事实。
附录:HarmonyOS 6.1.1 新特性开发环境与真机验证准入
1. 版本硬基线
本批新特性统一以 HarmonyOS 6.1.1 API 24 为目标版本。项目 sourceproject/build-profile.json5 必须保持:
{
"compatibleSdkVersion": "6.1.1(24)",
"targetSdkVersion": "6.1.1(24)",
"runtimeOS": "HarmonyOS"
}
开发者不得为了绕过构建错误,把项目静默改为 API 26 或其他版本。版本变化会同时改变 API 声明、兼容设备、文章结论和文章事实范围。
2. 编译环境准入
在 DevEco Studio 的 SDK Manager 中,必须选择与项目一致的 HarmonyOS 6.1.1(API 24) SDK。仅有 system-image 只能启动模拟器,不能证明 ArkTS 项目可以编译。至少应核对以下编译组件:

| 组件 | 作用 | 准入要求 |
|---|---|---|
hms/ets |
ArkTS/ETS API 声明与编译 | 目录存在,元数据与 Hvigor 兼容 |
hms/native |
Native 编译支持 | 目录存在,元数据与 Hvigor 兼容 |
hms/toolchains |
编译、签名和设备工具链 | 目录存在,hdc 可执行 |
hms/previewer |
预览与设计期支持 | 目录存在,版本与 SDK 对齐 |
openharmony/toolchains |
设备安装、启动与调试 | hdc.exe 可调用 |
硬性判定不是“SDK Manager 显示了 API 24”,而是构建已经越过 SDK 扫描并进入 CompileArkTS。本项目曾遇到组件 metaVersion: 3.1.0 与项目自带 Hvigor 扫描器不兼容,最终报 00303168 SDK component missing;此时不能进入特性 API 编码和文章结论阶段。
3. 推荐构建链路
当前已验证可用的是 DevEco Studio 内置 Hvigor 与 DevEco JBR,而不是项目自带的旧/不兼容 Hvigor 组合:
$env:DEVECO_SDK_HOME='D:\Program Files\Huawei\DevEco Studio\sdk'
$env:JAVA_HOME='D:\Program Files\Huawei\DevEco Studio\jbr'
$env:Path="$env:JAVA_HOME\bin;$env:Path"
& 'D:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat' `
--no-daemon --mode module -p module=entry@default -p product=default assembleHap --stacktrace
准入日志必须至少出现:
Finished :entry:default@CompileArkTS
Finished :entry:default@PackageHap
BUILD SUCCESSFUL
如果失败停在 SDK 扫描、依赖解析或 ArkTS 编译之前,结论只能写“环境未解锁”。不要根据 IDE 能打开项目、预览器能显示页面或旧 HAP 仍能安装,推导新特性 API 可用。
4. HAP 安装与启动环境
安装验证至少记录设备、包名、HAP 来源和结果。当前项目基线如下:
| 项目 | 要求/已验证值 |
|---|---|
| 包名 | com.csdn.harmonyos.featuredemos |
| 项目 API | compatibleSdkVersion=6.1.1(24)、targetSdkVersion=6.1.1(24) |
| 设备 API | 与项目兼容范围匹配,当前 API 24 |
releaseType |
项目、SDK、设备保持一致,当前为 Release |
| 设备形态 | 本批 Demo 以横向 Pad 为主要截图形态;手机需单独复核 |
| HAP 来源 | 当前 SDK 重新构建的产物,不沿用旧 HAP |
$hdc='D:\Program Files\Huawei\DevEco Studio\sdk\default\openharmony\toolchains\hdc.exe'
& $hdc install -r 'sourceproject\entry\build\default\outputs\default\entry-default-unsigned.hap'
& $hdc shell aa start -a EntryAbility -b com.csdn.harmonyos.featuredemos
install bundle successfully 只证明 HAP 与设备的安装条件匹配;start ability successfully 只证明应用可以启动。两者均不证明 Map、Camera、Notification 听觉、AI 字幕或通行证识别已经成功。
参考资料
- HarmonyOS 6.1.1 ArkUI API 差异说明:
https://developer.huawei.com/consumer/cn/doc/harmonyos-releases/js-apidiff-arkui-6111 - ArkUI Tabs 组件参考:
https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-tabs
更多推荐
所有评论(0)