恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

HarmonyOS 6.1 实战:Scroll + Stack 联动实现商品页吸顶 Header

  • 首页
  • 资讯中心
  • /
  • HarmonyOS 6.1 实战:Scroll + Stack 联动实现商品页吸顶 Header

相关资讯

AI副业口碑传播的“黄金72小时”法则:数据证实——首周传播效率占全周期68.3%,错过即断链 2026/10/1 13:38:46
最后一个月!9月PMP末班车冲刺:每天2小时,照着这份“保命”攻略走 2026/8/2 19:11:50
中天帝景传说 · 智慧物业管理系统(功能全解读 / 实景演示) 2026/8/2 19:11:50

最新资讯

宿舍夜谈里的理想与现实:从碰撞到行动的成长路径
Arch/Manjaro 上运行企业微信:AUR、Docker 与虚拟机实战指南
MobileNetV2微生物图像分类实战:轻量模型+显微图像专用预处理
宿舍夜聊:理想与现实碰撞下的深度对话指南
字符串算法刷题指南:底层逻辑、双指针与多语言避坑
FastAPI+LangChain构建AI Agent:异步流式LLM服务实战

今日推荐

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

本周热门

从像素到笔画:srt-whiteboard-animation骨架笔迹追踪实现(Zhang-Suen细化+8邻接追踪)
网站建设的英语怎么说?别只背单词,看完这套安全完整流程才敢上线
新手入门看这篇:建设网站加盟避坑指南与SEO实操

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

HarmonyOS 6.1 实战:Scroll + Stack 联动实现商品页吸顶 Header

发布时间:2026/10/1 13:39:14
HarmonyOS 6.1 实战:Scroll + Stack 联动实现商品页吸顶 Header 前言电商应用中最常见的交互之一页面顶部有大图 Banner其下紧跟分类标签栏当用户向下滚动超过 Banner 高度后标签栏“吸顶“固定显示继续滚动商品列表时标签栏始终可见。本文用Stack Scroll onScroll实现这一联动效果无需原生 StickyList API。运行效果初始状态Banner 分类标签滚动后标签栏吸顶显示商品列表核心实现原理Stack (alignContent: Alignment.Top) ├── Scroll主内容Banner 内联标签栏 商品列表 │ └── 监听 onScroll → 更新 scrollOffsetY └── if (scrollOffsetY HEADER_HEIGHT) Column (固定吸顶标签栏position y0, zIndex10)当scrollOffsetY HEADER_HEIGHTBanner 高度时在 Stack 顶部渲染固定的标签栏覆盖层产生“吸顶“效果。完整示例代码interface ProductItem { name: string price: string category: string icon: string } Entry Component struct Index { State scrollOffsetY: number 0 State activeTab: number 0 private scroller: Scroller new Scroller() private HEADER_HEIGHT: number 220 private TAB_HEIGHT: number 44 private tabs: string[] [全部, 电子, 服装, 食品, 家居] private allProducts: ProductItem[] [ { name: 无线蓝牙耳机 Pro, price: ¥299, category: 电子, icon: }, { name: 智能手表 GT3, price: ¥799, category: 电子, icon: ⌚ }, { name: 机械键盘 87 键, price: ¥459, category: 电子, icon: ⌨️ }, { name: 运动 T 恤, price: ¥89, category: 服装, icon: }, { name: 休闲牛仔裤, price: ¥199, category: 服装, icon: }, { name: 运动鞋 Air, price: ¥349, category: 服装, icon: }, { name: 有机燕麦片 1kg, price: ¥39, category: 食品, icon: }, { name: 进口坚果礼盒, price: ¥128, category: 食品, icon: }, { name: 绿茶 250g, price: ¥58, category: 食品, icon: }, { name: 北欧风台灯, price: ¥168, category: 家居, icon: }, { name: 简约书架, price: ¥289, category: 家居, icon: }, { name: 香薰蜡烛套装, price: ¥79, category: 家居, icon: ️ }, { name: 4K 显示器, price: ¥1899, category: 电子, icon: ️ }, { name: 羽绒外套, price: ¥599, category: 服装, icon: }, { name: 手冲咖啡豆, price: ¥88, category: 食品, icon: ☕ }, ] private getFilteredProducts(): ProductItem[] { let activeTabName this.tabs[this.activeTab] if (activeTabName 全部) { return this.allProducts } let result: ProductItem[] [] for (let i 0; i this.allProducts.length; i) { if (this.allProducts[i].category activeTabName) { result.push(this.allProducts[i]) } } return result } Builder tabBar(isFixed: boolean) { Row({ space: 0 }) { ForEach(this.tabs, (tab: string, idx: number) { Column({ space: 0 }) { Text(tab) .fontSize(14) .fontWeight(this.activeTab idx ? FontWeight.Bold : FontWeight.Normal) .fontColor(this.activeTab idx ? #0066ff : #555) .height(this.TAB_HEIGHT - 2) .textAlign(TextAlign.Center) // 下划线指示器 Text() .width(this.activeTab idx ? 24 : 0) .height(2) .backgroundColor(#0066ff) .borderRadius(1) } .layoutWeight(1) .height(this.TAB_HEIGHT) .justifyContent(FlexAlign.Center) .onClick(() { this.activeTab idx }) }) } .width(100%) .height(this.TAB_HEIGHT) .backgroundColor(#ffffff) .border({ width: { bottom: 1 }, color: #f0f0f0 }) // 吸顶时显示投影 .shadow(isFixed ? { radius: 6, color: #00000015, offsetX: 0, offsetY: 3 } : { radius: 0, color: transparent, offsetX: 0, offsetY: 0 }) } Builder productCard(item: ProductItem) { Row() { Column() { Text(item.icon).fontSize(28) } .width(56).height(56) .backgroundColor(#f5f8ff).borderRadius(12) .justifyContent(FlexAlign.Center) .margin({ right: 12 }) Column({ space: 4 }) { Text(item.name) .fontSize(15).fontColor(#222).fontWeight(FontWeight.Medium) .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) Row({ space: 8 }) { Text(item.category) .fontSize(11).fontColor(#0066ff) .backgroundColor(#f0f5ff) .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4) Text(满 200 减 30).fontSize(11).fontColor(#ff4444) } } .layoutWeight(1).alignItems(HorizontalAlign.Start) Column({ space: 6 }) { Text(item.price) .fontSize(17).fontWeight(FontWeight.Bold).fontColor(#ff4444) Text(加入购物车) .fontSize(11).fontColor(#ffffff) .backgroundColor(#0066ff) .padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(12) } .alignItems(HorizontalAlign.End) } .width(100%) .padding({ left: 16, right: 16, top: 14, bottom: 14 }) .backgroundColor(#ffffff) .border({ width: { bottom: 1 }, color: #f8f8f8 }) } build() { Stack({ alignContent: Alignment.Top }) { // ── 主滚动体 ── Scroll(this.scroller) { Column({ space: 0 }) { // Hero Banner可滚走 Column({ space: 12 }) { Row() { Column({ space: 4 }) { Text(发现好物).fontSize(24).fontWeight(FontWeight.Bold).fontColor(#ffffff) Text(精选好货每日更新).fontSize(13).fontColor(#ffffffcc) } .alignItems(HorizontalAlign.Start).layoutWeight(1) Column({ space: 4 }) { Text().fontSize(24) Text(搜索).fontSize(11).fontColor(#ffffffbb) } .alignItems(HorizontalAlign.Center) } .width(100%).padding({ left: 20, right: 20, top: 28, bottom: 8 }) Row({ space: 10 }) { Column({ space: 4 }) { Text(⚡ 限时特惠).fontSize(13).fontWeight(FontWeight.Bold).fontColor(#ff8800) Text(今日好价 低至 5 折).fontSize(11).fontColor(#664400) } .layoutWeight(1).height(60).backgroundColor(#fff8e8).borderRadius(10) .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center) .border({ width: 1, color: #ffcc66 }) Column({ space: 4 }) { Text( 新人礼包).fontSize(13).fontWeight(FontWeight.Bold).fontColor(#6600cc) Text(首单立减 50 元).fontSize(11).fontColor(#440088) } .layoutWeight(1).height(60).backgroundColor(#f8eeff).borderRadius(10) .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center) .border({ width: 1, color: #cc88ff }) } .padding({ left: 20, right: 20, bottom: 16 }) } .width(100%).height(this.HEADER_HEIGHT) .backgroundColor(#0066ff) .linearGradient({ angle: 135, colors: [[#0044cc, 0], [#0088ff, 1]] }) // 内联标签栏随页面滚动Banner 消失后被吸顶层覆盖 this.tabBar(false) // 商品列表 Column({ space: 0 }) { Row() { Text(this.tabs[this.activeTab] 全部 ? 全部商品 : this.tabs[this.activeTab] 分类) .fontSize(14).fontWeight(FontWeight.Bold).fontColor(#333) Text( · this.getFilteredProducts().length.toString() 件) .fontSize(13).fontColor(#999) } .width(100%).height(40) .padding({ left: 16, right: 16 }) .backgroundColor(#f8f8f8) .border({ width: { bottom: 1 }, color: #eeeeee }) ForEach(this.getFilteredProducts(), (item: ProductItem) { this.productCard(item) }) Column().width(100%).height(32).backgroundColor(#f5f5f5) } .width(100%).backgroundColor(#ffffff) } .width(100%) } .onScroll((_xOffset: number, _yOffset: number) { this.scrollOffsetY this.scroller.currentOffset().yOffset }) .scrollBar(BarState.Off) .width(100%).height(100%).backgroundColor(#f5f5f5) // ── 吸顶标签栏当滚过 Banner 后才显示── if (this.scrollOffsetY this.HEADER_HEIGHT) { Column() { this.tabBar(true) } .width(100%) .position({ x: 0, y: 0 }) .zIndex(10) } // 滚动状态角标 Row() { Text(↕ Math.round(this.scrollOffsetY).toString() px) .fontSize(11).fontColor(#ffffff) .padding({ left: 8, right: 8, top: 3, bottom: 3 }) .backgroundColor(this.scrollOffsetY this.HEADER_HEIGHT ? #00aa55 : #0066ff) .borderRadius(10) Text(this.scrollOffsetY this.HEADER_HEIGHT ? 吸顶中 : 滚动中) .fontSize(11).fontColor(#ffffff) .padding({ right: 8, top: 3, bottom: 3 }) .backgroundColor(this.scrollOffsetY this.HEADER_HEIGHT ? #00aa55 : #0066ff) .borderRadius(10) } .position({ x: 16, y: 8 }) .zIndex(20) .borderRadius(12) .shadow({ radius: 4, color: #00000020, offsetX: 0, offsetY: 2 }) } .width(100%).height(100%) } }关键知识点1. Stack 条件渲染实现吸顶核心思路在Stack顶部用if条件渲染一个固定在 y0 的覆盖层Stack({ alignContent: Alignment.Top }) { Scroll() { ... } .onScroll(() { this.scrollOffsetY this.scroller.currentOffset().yOffset }) // 吸顶覆盖层 if (this.scrollOffsetY HEADER_HEIGHT) { Column() { this.tabBar(true) } .position({ x: 0, y: 0 }) // 固定在顶部 .zIndex(10) // 覆盖在滚动内容之上 } }2. onScroll 获取绝对偏移.onScroll(dx, dy)的参数是增量用scroller.currentOffset().yOffset获取绝对位移.onScroll((_x: number, _y: number) { this.scrollOffsetY this.scroller.currentOffset().yOffset })3. 吸顶时显示阴影Builder tabBar(isFixed: boolean)接收参数当isFixedtrue时给标签栏加投影增强层次感Builder tabBar(isFixed: boolean) { Row() { ... } .shadow(isFixed ? { radius: 6, color: #00000015, offsetX: 0, offsetY: 3 } : { radius: 0, color: transparent, offsetX: 0, offsetY: 0 }) }4. RowAttribute 不支持 .overflow()Row组件没有.overflow()属性.overflow()仅存在于Text等文本组件如果需要裁剪超出内容改用.clip(true)// 错误Row 没有 .overflow() Row() { ... }.overflow(Overflow.Hidden) // 正确用 .clip() Row() { ... }.clip(true)5. Builder 参数传递Builder方法支持传参可以根据参数渲染不同状态的 UIBuilder tabBar(isFixed: boolean) { // 根据 isFixed 调整 shadow、backgroundColor 等 } // 调用 this.tabBar(false) // 内联标签栏 this.tabBar(true) // 吸顶覆盖层小结吸顶效果的关键Stack包裹 onScroll监听偏移量 if条件渲染固定覆盖层获取绝对滚动位置用scroller.currentOffset().yOffset不要用onScroll的差量参数累加吸顶覆盖层通过.position({ x:0, y:0 }).zIndex(n)固定在最顶部Row没有.overflow()属性裁剪用.clip(true)同一个Builder方法传不同参数可渲染“滚动版“和“吸顶版“两种状态避免代码重复

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号