【共创季稿事节】鸿蒙原生 ArkTS 布局实战:Scroll + Row 实现水平滚动导航菜单
鸿蒙原生 ArkTS 布局实战Scroll Row 实现水平滚动导航菜单一、引言在移动端应用中导航菜单是最常见的交互组件之一。新闻客户端的分类栏、电商应用的商品品类切换器、社交应用的顶部 Tab —— 超出屏幕宽度时可左右滑动浏览已经成为用户的默认预期体验。在 HarmonyOS NEXT 的 ArkTS 生态中实现这一需求的官方推荐方案是ScrollRow组合布局。本文通过一个完整的可运行示例深入剖析这一布局模式的核心原理、实现步骤与最佳实践。1.1 为什么是 Scroll Row在 ArkTS 的布局体系中Column垂直排列子组件溢出时通过外部 Scroll 实现垂直滚动。Row水平排列子组件默认不会滚动 —— 子项超出父容器宽度时按布局规则压缩或截断。Scroll通用滚动容器包裹单个子组件并赋予滚动能力。ScrollRow的方案逻辑清晰Row做水平排列Scroll赋予水平滚动能力各司其职。对比ListListItem方案ScrollRow更轻量灵活适合菜单项数量适中几十个以内的场景。二、项目准备2.1 开发环境项目说明IDEDevEco Studiohvigor 6.23.5目标 API24HarmonyOS NEXT / SDK 6.1.0语言ArkTS基于 TypeScript构建工具hvigorw2.2 新建工程在 DevEco Studio 中File → New → Create Project → Empty Ability选择兼容 SDK6.1.0(23)及以上。工程创建后主要关注entry/src/main/ets/pages/Index.ets文件。三、需求分析与页面结构设计3.1 需求顶部导航菜单栏—— 一行水平排列的菜单项总宽超出屏幕时可左右滑动浏览。下方内容展示区—— 显示当前选中的菜单项信息。3.2 交互要求手指左右滑动时菜单栏平滑滚动滚动到边缘有弹簧回弹效果点击菜单项 → 橙色高亮 Toast 提示鼠标悬停有 Hover 效果3.3 页面布局树Column全屏 ├── Scroll水平滚动高度 56vp ← 核心容器 │ └── Rowwidth: auto子项撑开宽度 ← 唯一子节点 │ ├── MenuItem(首页, 64vp) │ ├── MenuItem(推荐, 64vp) │ ├── …… 共 13 项 × 64vp 832vp …… │ └── MenuItem(游戏, 64vp) │ 总宽度 屏幕宽度 ~360vp → 可滚动 └── Column内容区layoutWeight1 填充 └── 选中项图标 文字 提示四、核心代码实现4.1 数据模型interfaceMenuDataItem{id:number;// 唯一标识label:string;// 显示的文本icon?:ResourceStr;// 可选图标}使用interface而非class更轻量且编译期无开销。4.2 页面组件与状态管理EntryComponentstruct Index{StateprivatemenuItems:MenuDataItem[][{id:1,label:首页,icon:},{id:2,label:推荐,icon:},// ... 共 13 项{id:13,label:游戏,icon:},];StateprivateselectedId:number1;}状态提升原则选中状态放在父组件Index中通过数据驱动统一控制所有菜单项的选中态。子组件通过条件判断决定自身样式橙色文字 浅橙背景 ← 当 selectedId item.id 灰色文字 透明背景 ← 其他情况这是 ArkTS / React / Vue 等声明式 UI 框架中状态提升的典型应用。4.3 主布局Scroll Rowbuild(){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 自定义菜单项BuilderBuilderprivateMenuDataItemView(item:MenuDataItem){Column(){Text(item.icon).fontSize(18).lineHeight(22)Text(item.label).fontSize(14).fontColor(this.selectedIditem.id?#FF6B00:#666666).fontWeight(this.selectedIditem.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.selectedIditem.id?#FFF0E0:Color.Transparent).onClick((){this.selectedIditem.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)实现全局换肤。宽度选择为何 64vp64vp × 13 项 padding 848vp超出手机屏幕 ~360vp 约 2.3 倍用户明显感知需要滑动 → 滚动效果得到验证若需弹性伸缩可改为.constraintSize({ minWidth: 64 })五、编译验证在项目根目录执行hvigorw assembleApp输出解读Finished ::PreBuildApp... # 预构建通过 Finished :entry:defaultCompileArkTS... # ArkTS 编译成功 WARN: onScrollEnd deprecated # 仅弃用警告不影响运行 WARN: showToast deprecated # 同上 Finished :entry:defaultPackageHap... # HAP 打包成功 BUILD SUCCESSFUL in 2 s 254 ms # ✅ 构建成功弃用 API 在 API 24 中仍完全可用仅提示迁移到新接口。运行效果验证操作预期行为页面加载菜单栏白色背景前约 5 项可见其余隐藏左滑菜单栏隐藏项依次出现美食→旅行→健康→教育→游戏滑到最左/右弹簧回弹动画Spring点击「科技」橙色高亮 Toast「切换到「科技」」下方同步更新六、进阶扩展6.1 动态下划线指示器StateprivateindicatorOffset:number0;// 点击时计算偏移.onClick((){this.selectedIditem.id;this.indicatorOffset(item.id-1)*64;})// Row 底部叠加下划线.overlay({builder:(){Row().width(32).height(3).backgroundColor(#FF6B00).borderRadius(2).position({x:this.indicatorOffset16,y:48}).animation({duration:300,curve:Curve.FastOutSlowIn})}}).animation()让下划线平滑过渡大幅提升视觉质感。6.2 大数据量LazyForEach菜单项达上百个时使用LazyForEach按需创建/销毁节点import{LazyForEach}fromkit.ArkUI;Scroll(){Row(){LazyForEach(this.dataSource,(item:MenuDataItem){this.MenuDataItemView(item)},(item:MenuDataItem)item.id)}.width(auto)}内存占用从 O(总项数) 降为 O(可见项数)。6.3 大屏幕响应式适配折叠屏展开态下菜单全部可见时可禁用滚动StateprivateisCompact:booleantrue;aboutToAppear(){this.isCompactDisplayUtil.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}fromkit.ArkUI;interfaceMenuDataItem{id:number;label:string;icon?:ResourceStr;}EntryComponentstruct 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:number1;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(ii.idthis.selectedId)?.icon??).fontSize(48).margin({bottom:16})Text(当前选中「${this.menuItems.find(ii.idthis.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.selectedIditem.id?#FF6B00:#666666).fontWeight(this.selectedIditem.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.selectedIditem.id?#FFF0E0:Color.Transparent).onClick((){this.selectedIditem.id;promptAction.showToast({message:切换到「${item.label}」,duration:1000});}).responseRegion({x:0,y:0,width:64,height:48}).hoverEffect(HoverEffect.Auto)}}九、总结本文围绕 HarmonyOS NEXTAPI 24的Scroll Row 水平滚动菜单布局从零到一构建了完整应用。核心知识点总结领域内容布局组件Scroll 的 scrollable/edgeEffect/scrollBar行布局Row 的 width(‘auto’) 子项撑开状态管理State 状态提升控制选中态代码复用Builder 定义菜单项模板交互反馈onClick showToast responseRegion滚动体验EdgeEffect.Spring 边缘回弹布局技巧layoutWeight 填充剩余空间构建验证hvigorw assembleAppScroll Row 组合是 ArkTS 最实用也最基础的布局模式之一。掌握它相当于拿到了构建导航菜单栏、分类筛选器、标签面板等常见 UI 的通用钥匙。建议读者将示例代码在 DevEco Studio 中运行体验然后尝试调整样式、添加下划线指示器或改为 LazyForEach 处理大数据量 —— 每一次改动都是对 ArkTS 布局能力的深化理解。参考资料HarmonyOS 开发者文档 — Scroll 组件HarmonyOS 开发者文档 — Row 组件HarmonyOS NEXT ArkTS 开发指南版权声明本文为 HarmonyOS 技术分享用途文中代码可用于任何开源或商业项目。