【共创季稿事节】鸿蒙原生 ArkTS 布局实战:Scroll + Row 实现水平滚动导航菜单
2026/6/27 2:41:02 网站建设 项目流程

鸿蒙原生 ArkTS 布局实战:Scroll + Row 实现水平滚动导航菜单




一、引言

在移动端应用中,导航菜单是最常见的交互组件之一。新闻客户端的分类栏、电商应用的商品品类切换器、社交应用的顶部 Tab —— "超出屏幕宽度时可左右滑动浏览"已经成为用户的默认预期体验。

在 HarmonyOS NEXT 的 ArkTS 生态中,实现这一需求的官方推荐方案是Scroll+Row组合布局。本文通过一个完整的可运行示例,深入剖析这一布局模式的核心原理、实现步骤与最佳实践。

1.1 为什么是 Scroll + Row?

在 ArkTS 的布局体系中:

  • Column:垂直排列子组件,溢出时通过外部 Scroll 实现垂直滚动。
  • Row:水平排列子组件,默认不会滚动 —— 子项超出父容器宽度时按布局规则压缩或截断。
  • Scroll:通用滚动容器,包裹单个子组件并赋予滚动能力。

Scroll+Row的方案逻辑清晰:Row做水平排列,Scroll赋予水平滚动能力,各司其职。

对比List+ListItem方案,Scroll+Row更轻量灵活,适合菜单项数量适中(几十个以内)的场景。


二、项目准备

2.1 开发环境

项目说明
IDEDevEco Studio(hvigor 6.23.5)
目标 API24(HarmonyOS NEXT / SDK 6.1.0)
语言ArkTS(基于 TypeScript)
构建工具hvigorw

2.2 新建工程

在 DevEco Studio 中:File → New → Create Project → Empty Ability,选择兼容 SDK6.1.0(23)及以上。工程创建后主要关注entry/src/main/ets/pages/Index.ets文件。


三、需求分析与页面结构设计

3.1 需求

  1. 顶部导航菜单栏—— 一行水平排列的菜单项,总宽超出屏幕时可左右滑动浏览。
  2. 下方内容展示区—— 显示当前选中的菜单项信息。

3.2 交互要求

  • 手指左右滑动时菜单栏平滑滚动
  • 滚动到边缘有弹簧回弹效果
  • 点击菜单项 → 橙色高亮 + Toast 提示
  • 鼠标悬停有 Hover 效果

3.3 页面布局树

Column(全屏) ├── Scroll(水平滚动,高度 56vp) ← 核心容器 │ └── Row(width: auto,子项撑开宽度) ← 唯一子节点 │ ├── MenuItem(首页, 64vp) │ ├── MenuItem(推荐, 64vp) │ ├── …… 共 13 项 × 64vp = 832vp …… │ └── MenuItem(游戏, 64vp) │ (总宽度 >> 屏幕宽度 ~360vp → 可滚动) └── Column(内容区,layoutWeight=1 填充) └── 选中项图标 + 文字 + 提示

四、核心代码实现

4.1 数据模型

interfaceMenuDataItem{id:number;// 唯一标识label:string;// 显示的文本icon?:ResourceStr;// 可选图标}

使用interface而非class,更轻量且编译期无开销。

4.2 页面组件与状态管理

@Entry@Componentstruct Index{@StateprivatemenuItems:MenuDataItem[]=[{id:1,label:'首页',icon:'🏠'},{id:2,label:'推荐',icon:'🔥'},// ... 共 13 项{id:13,label:'游戏',icon:'🎮'},];@StateprivateselectedId:number=1;}
状态提升原则

选中状态放在父组件Index中,通过数据驱动统一控制所有菜单项的选中态。子组件通过条件判断决定自身样式:

橙色文字 + 浅橙背景 ← 当 selectedId === item.id 灰色文字 + 透明背景 ← 其他情况

这是 ArkTS / React / Vue 等声明式 UI 框架中"状态提升"的典型应用。

4.3 主布局:Scroll + Row

build(){Column(){// ========= 核心:水平滚动菜单栏 =========Scroll(){Row(){ForEach(this.menuItems,(item:MenuDataItem)=>{this.MenuDataItemView(item)},(item:MenuDataItem)=>item.id.toString())}.width('auto')// ★ 关键:宽度由子项撑开.height('100%').alignItems(VerticalAlign.Center).padding({left:8,right:8})}.scrollable(ScrollDirection.Horizontal)// ★ 关键:开启水平滚动.scrollBar(BarState.Auto)// 滚动条自动显隐.edgeEffect(EdgeEffect.Spring)// 边缘回弹.width('100%').height(56).backgroundColor('#FFFFFF').shadow({radius:4,color:'#1A000000',offsetX:0,offsetY:2})// ========= 下方内容展示区 =========Column(){/* 展示选中项信息 */}.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center).backgroundColor('#F5F5F5')}.width('100%').height('100%')}
布局要点拆解

① 必须指定滚动方向

.scrollable(ScrollDirection.Horizontal)

Scroll默认不滚动,遗漏.scrollable()是初学者最容易踩的坑。

② Scroll 内只能有一个根子组件
ArkTS 硬性约束:Scroll > Row > (子项)。不能写两个平级的 Row。

③ Row 必须设 width(‘auto’)
这是可滚动的关键。若设width('100%'),Row 宽度被锁定在 Scroll 宽度内,不会溢出,从而无法滚动。

④ 边缘回弹提升手感

.edgeEffect(EdgeEffect.Spring)

Spring 模式类似 iOS 的 rubber-band 效果,比 Fade 或硬边界更符合移动端用户预期。

4.4 自定义菜单项:@Builder

@BuilderprivateMenuDataItemView(item:MenuDataItem){Column(){Text(item.icon).fontSize(18).lineHeight(22)Text(item.label).fontSize(14).fontColor(this.selectedId===item.id?'#FF6B00':'#666666').fontWeight(this.selectedId===item.id?FontWeight.Bold:FontWeight.Regular).margin({top:2})}.width(64).height(48).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center).padding({top:4,bottom:4}).borderRadius(8).backgroundColor(this.selectedId===item.id?'#FFF0E0':Color.Transparent).onClick(()=>{this.selectedId=item.id;promptAction.showToast({message:`切换到「${item.label}`,duration:1000});}).responseRegion({x:0,y:0,width:64,height:48}).hoverEffect(HoverEffect.Auto)}
为什么用 @Builder?
  • 代码复用:一处定义,ForEach 每次循环复用
  • 自动绑定 this:天然访问this.selectedId
  • 性能优化:ArkUI 框架会缓存优化 Builder 生成的节点
选中态样式
未选中: #666666 灰色 + 透明背景 选中态: #FF6B00 橙色 + #FFF0E0 浅橙背景

橙色是 HarmonyOS Design 推荐主色之一,生产项目可替换为$r('app.color.xxx')实现全局换肤。

宽度选择:为何 64vp?
  • 64vp × 13 项 + padding = 848vp,超出手机屏幕 ~360vp 约 2.3 倍
  • 用户明显感知"需要滑动" → 滚动效果得到验证
  • 若需弹性伸缩,可改为.constraintSize({ minWidth: 64 })

五、编译验证

在项目根目录执行:

hvigorw assembleApp

输出解读:

Finished ::PreBuildApp... # 预构建通过 Finished :entry:default@CompileArkTS... # ArkTS 编译成功 WARN: 'onScrollEnd' deprecated # 仅弃用警告,不影响运行 WARN: 'showToast' deprecated # 同上 Finished :entry:default@PackageHap... # HAP 打包成功 BUILD SUCCESSFUL in 2 s 254 ms # ✅ 构建成功

弃用 API 在 API 24 中仍完全可用,仅提示迁移到新接口。

运行效果验证

操作预期行为
页面加载菜单栏白色背景,前约 5 项可见,其余隐藏
左滑菜单栏隐藏项依次出现:美食→旅行→健康→教育→游戏
滑到最左/右弹簧回弹动画(Spring)
点击「科技」橙色高亮 + Toast「切换到「科技」」,下方同步更新

六、进阶扩展

6.1 动态下划线指示器

@StateprivateindicatorOffset:number=0;// 点击时计算偏移.onClick(()=>{this.selectedId=item.id;this.indicatorOffset=(item.id-1)*64;})// Row 底部叠加下划线.overlay({builder:()=>{Row().width(32).height(3).backgroundColor('#FF6B00').borderRadius(2).position({x:this.indicatorOffset+16,y:48}).animation({duration:300,curve:Curve.FastOutSlowIn})}})

.animation()让下划线平滑过渡,大幅提升视觉质感。

6.2 大数据量:LazyForEach

菜单项达上百个时,使用LazyForEach按需创建/销毁节点:

import{LazyForEach}from'@kit.ArkUI';Scroll(){Row(){LazyForEach(this.dataSource,(item:MenuDataItem)=>{this.MenuDataItemView(item)},(item:MenuDataItem)=>item.id)}.width('auto')}

内存占用从 O(总项数) 降为 O(可见项数)。

6.3 大屏幕响应式适配

折叠屏展开态下菜单全部可见时,可禁用滚动:

@StateprivateisCompact:boolean=true;aboutToAppear(){this.isCompact=DisplayUtil.isCompact();// 伪代码,需实现检测逻辑}build(){if(this.isCompact){Scroll(){Row(){/* 菜单项 */}}}else{Row(){/* 菜单项,无需Scroll */}}}

七、常见问题

Q1:设置了 Scroll 但无法滚动?

排查清单:

  • 调用了.scrollable(ScrollDirection.Horizontal)
  • Row宽度为'auto'而非'100%'
  • 子项总宽是否确实超出Scroll宽度?
  • Scroll设置了固定width: '100%'

Q2:滚动卡顿?

  • 确保ForEach的 key 生成器返回唯一稳定值(如item.id.toString()
  • 避免在滚动回调中做数组find()等高开销操作
  • 图片资源做尺寸适配和缓存

Q3:点击区域不灵敏?

使用responseRegion扩大热区。示例中已设为{ x:0, y:0, width:64, height:48 },覆盖整个菜单项。


八、完整源码

/** * Scroll + Row 实现水平滚动菜单 * 场景:可水平滚动的导航菜单栏 * 核心技术:Scroll + Row + @Builder */import{promptAction}from'@kit.ArkUI';interfaceMenuDataItem{id:number;label:string;icon?:ResourceStr;}@Entry@Componentstruct Index{@StateprivatemenuItems:MenuDataItem[]=[{id:1,label:'首页',icon:'🏠'},{id:2,label:'推荐',icon:'🔥'},{id:3,label:'关注',icon:'⭐'},{id:4,label:'热点',icon:'📈'},{id:5,label:'科技',icon:'💻'},{id:6,label:'体育',icon:'⚽'},{id:7,label:'娱乐',icon:'🎬'},{id:8,label:'财经',icon:'💰'},{id:9,label:'美食',icon:'🍜'},{id:10,label:'旅行',icon:'✈️'},{id:11,label:'健康',icon:'💪'},{id:12,label:'教育',icon:'📚'},{id:13,label:'游戏',icon:'🎮'},];@StateprivateselectedId:number=1;build(){Column(){// ========= 水平滚动菜单栏 =========Scroll(){Row(){ForEach(this.menuItems,(item:MenuDataItem)=>{this.MenuDataItemView(item)},(item:MenuDataItem)=>item.id.toString())}.width('auto').height('100%').alignItems(VerticalAlign.Center).padding({left:8,right:8})}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Auto).edgeEffect(EdgeEffect.Spring).width('100%').height(56).backgroundColor('#FFFFFF').shadow({radius:4,color:'#1A000000',offsetX:0,offsetY:2})// ========= 内容展示区 =========Column(){Text(this.menuItems.find(i=>i.id===this.selectedId)?.icon??'').fontSize(48).margin({bottom:16})Text(`当前选中:「${this.menuItems.find(i=>i.id===this.selectedId)?.label??''}`).fontSize(20).fontColor('#333333').fontWeight(FontWeight.Medium)Text('← 左右滑动上方菜单查看更多分类 →').fontSize(14).fontColor('#999999').margin({top:24})Divider().width('80%').margin({top:24,bottom:16})Text(`${this.menuItems.length}个菜单项,超出屏幕宽度的部分可水平滑动查看`).fontSize(13).fontColor('#BBBBBB').textAlign(TextAlign.Center)}.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center).backgroundColor('#F5F5F5')}.width('100%').height('100%')}@BuilderprivateMenuDataItemView(item:MenuDataItem){Column(){Text(item.icon).fontSize(18).lineHeight(22)Text(item.label).fontSize(14).fontColor(this.selectedId===item.id?'#FF6B00':'#666666').fontWeight(this.selectedId===item.id?FontWeight.Bold:FontWeight.Regular).margin({top:2})}.width(64).height(48).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center).padding({top:4,bottom:4}).borderRadius(8).backgroundColor(this.selectedId===item.id?'#FFF0E0':Color.Transparent).onClick(()=>{this.selectedId=item.id;promptAction.showToast({message:`切换到「${item.label}`,duration:1000});}).responseRegion({x:0,y:0,width:64,height:48}).hoverEffect(HoverEffect.Auto)}}

九、总结

本文围绕 HarmonyOS NEXT(API 24)的Scroll + Row 水平滚动菜单布局,从零到一构建了完整应用。核心知识点总结:

领域内容
布局组件Scroll 的 scrollable/edgeEffect/scrollBar
行布局Row 的 width(‘auto’) 子项撑开
状态管理@State + 状态提升控制选中态
代码复用@Builder 定义菜单项模板
交互反馈onClick + showToast + responseRegion
滚动体验EdgeEffect.Spring 边缘回弹
布局技巧layoutWeight 填充剩余空间
构建验证hvigorw assembleApp

Scroll + Row 组合是 ArkTS 最实用也最基础的布局模式之一。掌握它,相当于拿到了构建导航菜单栏、分类筛选器、标签面板等常见 UI 的通用钥匙。建议读者将示例代码在 DevEco Studio 中运行体验,然后尝试调整样式、添加下划线指示器或改为 LazyForEach 处理大数据量 —— 每一次改动都是对 ArkTS 布局能力的深化理解。


参考资料

  1. HarmonyOS 开发者文档 — Scroll 组件
  2. HarmonyOS 开发者文档 — Row 组件
  3. HarmonyOS NEXT ArkTS 开发指南

版权声明:本文为 HarmonyOS 技术分享用途,文中代码可用于任何开源或商业项目。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询