HarmonyOS应用开发实战:猫猫大作战-Scroll 容器、Scroller 控制器、滚动监听与性能

前言

前几篇我们用 Column/Row/Blank/layoutWeight 搭好了 HUD、底部栏、棋盘------所有内容都一屏装得下。但实战中经常遇到内容超出屏幕 的场景:游戏规则说明太长、战绩历史几十条、设置页十几项。这时候需要滚动容器让用户上下/左右滑动查看。

HarmonyOS 提供了 Scroll 滚动容器 + Scroller 滚动控制器------前者负责可滚动区域,后者负责编程式滚动(scrollTo、scrollEdge)。本篇以「猫猫大作战」规则说明面板扩展为锚点,把 Scroll 容器、Scroller 控制器、滚动监听与性能三大要点讲透。

提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1--24 篇。本篇是布局进阶的第五篇。

一、场景拆解:规则面板需要滚动

回顾「猫猫大作战」主菜单的规则面板(第 4 篇):

ts 复制代码
// 来源:entry/src/main/ets/pages/Index.ets  MainMenuView() 规则面板
Column() {
  Text('游戏规则').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#2C3E50')
  Text('• 点击列投放猫咪').fontSize(13).fontColor('#7F8C8D')
  Text('• 相邻同级猫咪自动合并升级').fontSize(13).fontColor('#7F8C8D')
  Text('• 连续合并触发连击加分').fontSize(13).fontColor('#7F8C8D')
  Text('• 猫咪堆到顶部则游戏结束').fontSize(13).fontColor('#7F8C8D')
}
.width('80%')
.padding(16)
.backgroundColor('rgba(255,255,255,0.7)')
.borderRadius(12)
.alignItems(HorizontalAlign.Start)

现在规则扩展到 10 条,Column 高度撑爆屏幕 ,底部按钮被推出可视区。需要给规则面板套一层 Scroll 让它可上下滚动。

Scroll 的解法

ts 复制代码
Scroll() {
  Column() {
    /* 10 条规则 */
  }
}
.scrollable(ScrollDirection.Vertical)    // 纵向滚动
.scrollBar(BarState.Auto)                // 自动显示滚动条
.width('80%')
.height(200)                             // 固定高度,超出滚动
}

关键经验Scroll 必须设固定 height------不设高度,Scroll 会撑开到内容全高,失去滚动意义。

二、Scroll 滚动容器

2.1 基本结构

ts 复制代码
Scroll() {
  Column() {                  // 或 Row,只能有一个直接子组件
    /* 内容 */
  }
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Auto)
.width('100%')
.height(200)

核心约束

约束 说明
只能有一个直接子组件 多个需用 Column/Row 包裹
必须设 height(纵向) 否则撑开到内容全高,不滚动
必须设 width(横向) 否则撑开到内容全宽
内容超出容器尺寸才会滚动 内容短不滚动

2.2 scrollable 滚动方向

ts 复制代码
.scrollable(ScrollDirection.Vertical)    // 纵向(默认)
.scrollable(ScrollDirection.Horizontal)  // 横向
方向 适用 height 要求
Vertical 列表、文章 必须设
Horizontal 横向轮播、Tab 条 必须设 width

2.3 scrollBar 滚动条

ts 复制代码
.scrollBar(BarState.Auto)    // 自动:滚动时显示,停止后淡出
.scrollBar(BarState.On)      // 常显:一直显示
.scrollBar(BarState.Off)     // 隐藏:不显示滚动条

实战经验游戏内规则面板用 BarState.Auto------滚动时给视觉反馈,不滚动时不打扰。

三、Scroller 滚动控制器

3.1 创建 Scroller

ts 复制代码
private scroller: Scroller = new Scroller();

Scroll(this.scroller) {              // 把 Scroller 传给 Scroll
  Column() { /* ... */ }
}
.height(200)

Scroller 是一个控制器对象,传给 Scroll 后,就能用它的 API 编程式控制滚动。

3.2 scrollTo 滚动到指定位置

ts 复制代码
this.scroller.scrollTo({
  xOffset: 0,                        // 横向偏移(vp)
  yOffset: 100,                      // 纵向偏移(vp):向下滚 100vp
  animation: { duration: 300, curve: Curve.EaseOut }   // 带动画
})

场景:点击「跳到第 5 条规则」按钮,平滑滚到对应位置。

3.3 scrollEdge 滚动到边缘

ts 复制代码
this.scroller.scrollEdge(Edge.Top)      // 滚到顶
this.scroller.scrollEdge(Edge.Bottom)   // 滚到底

场景:聊天页收到新消息自动滚到底部。

3.4 scrollToIndex 滚到指定项(需配合 List)

ts 复制代码
// Scroller 主要配合 List 使用
List({ scroller: this.scroller }) { /* ... */ }
this.scroller.scrollToIndex(10)        // 滚到第 10 项

提示:scrollToIndex 主要用于 List 组件 ,普通 Scroll 用 scrollTo。本系列第 68 篇会专讲 LazyForEach 大列表。

3.5 currentOffset 获取当前偏移

ts 复制代码
const offset = this.scroller.currentOffset();
console.info(`x: ${offset.xOffset}, y: ${offset.yOffset}`);

场景:根据滚动位置显示「回到顶部」按钮------yOffset > 200 时显示。

四、用 Scroll 改造规则面板

4.1 改造为可滚动规则面板

ts 复制代码
@Builder
MainMenuView() {
  Column() {
    Spacer().height('15%')

    // 游戏标题
    Text('🐱').fontSize(72).margin({ bottom: 8 })
    Text('猫猫大作战').fontSize(36).fontWeight(FontWeight.Bold).fontColor('#2C3E50').margin({ bottom: 8 })
    Text('合并进化 · 策略消除').fontSize(16).fontColor('#95A5A6').margin({ bottom: 48 })

    // 最高分
    if (this.highScore > 0) {
      Row() {
        Text('🏆 最高分: ').fontSize(16).fontColor('#F1C40F')
        Text(this.highScore.toString()).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#F1C40F')
      }.margin({ bottom: 32 })
    }

    // 开始游戏按钮
    Button('开始游戏')
      .width('70%').height(56)
      .fontSize(20).fontWeight(FontWeight.Bold)
      .fontColor('#FFFFFF').backgroundColor('#2ECC71')
      .borderRadius(28)
      .shadow({ radius: 8, color: 'rgba(46, 204, 113, 0.4)', offsetY: 4 })
      .onClick(() => { this.startGame(); })

    Spacer().height(24)

    // 游戏规则面板(本篇改造:套 Scroll 让规则可滚动)
    Scroll() {
      Column() {
        Text('游戏规则')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#2C3E50')
          .margin({ bottom: 8 })

        Text('• 点击列投放猫咪').fontSize(13).fontColor('#7F8C8D').margin({ bottom: 4 })
        Text('• 相邻同级猫咪自动合并升级').fontSize(13).fontColor('#7F8C8D').margin({ bottom: 4 })
        Text('• 连续合并触发连击加分').fontSize(13).fontColor('#7F8C8D').margin({ bottom: 4 })
        Text('• 猫咪堆到顶部则游戏结束').fontSize(13).fontColor('#7F8C8D').margin({ bottom: 4 })
        Text('• 同级猫咪相邻 2 个自动合并').fontSize(13).fontColor('#7F8C8D').margin({ bottom: 4 })
        Text('• 合并后等级 +1,得分按 3^n 增长').fontSize(13).fontColor('#7F8C8D').margin({ bottom: 4 })
        Text('• 连击窗口 1.5s,连续合并倍率叠加').fontSize(13).fontColor('#7F8C8D').margin({ bottom: 4 })
        Text('• 最高等级传奇猫,得分 2430').fontSize(13).fontColor('#7F8C8D').margin({ bottom: 4 })
        Text('• 自动生成间隔 2s,最多 40 只').fontSize(13).fontColor('#7F8C8D').margin({ bottom: 4 })
        Text('• 棋盘 5 列 8 行,第 2 行堆积则结束').fontSize(13).fontColor('#7F8C8D')
      }
      .alignItems(HorizontalAlign.Start)
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Auto)
    .width('80%')
    .height(180)                   // 固定 180vp 高,超出滚动
    .padding(16)
    .backgroundColor('rgba(255,255,255,0.7)')
    .borderRadius(12)

    Spacer()
  }
  .width('100%').height('100%')
  .linearGradient({
    direction: GradientDirection.Bottom,
    colors: [['#E8F4F8', 0.0], ['#D6EEF5', 0.5], ['#C9E8F2', 1.0]]
  })
  .alignItems(HorizontalAlign.Center)
}

改造对比

维度 原 Column 版 改造 Scroll 版
规则条数 4 条 10 条
高度 撑开到内容全高 固定 180vp
超出处理 顶出底部按钮 内部滚动
滚动条 Auto 自动显示

4.2 关键:height(180) 固定高度

ts 复制代码
Scroll() { /* 10 条规则 */ }
  .height(180)                   // 固定高度

规则:内容总高 > 180vp 时滚动;内容总高 < 180vp 时不滚动(Scroll 撑开到内容高)。

踩坑:如果不设 height,Scroll 会撑开到 10 条规则的全高(约 400vp),把底部按钮顶出屏幕------这时 Scroll 等于普通 Column,失去滚动能力。

五、Scroll 与 Scroller 配合:跳到顶部按钮

5.1 场景

规则面板滚到底部后,用户想快速回到顶部。我们在面板右上角放一个「↑」按钮,点击调用 scroller.scrollEdge(Edge.Top) 平滑滚到顶。

5.2 实现

ts 复制代码
@Entry
@Component
struct Index {
  private ruleScroller: Scroller = new Scroller();
  @State showBackToTop: boolean = false;       // 是否显示「回顶」按钮

  @Builder
  MainMenuView() {
    Column() {
      /* 标题、按钮等 */

      // 规则面板(带 Scroller)
      Stack() {
        Scroll(this.ruleScroller) {
          Column() { /* 10 条规则 */ }
            .alignItems(HorizontalAlign.Start)
        }
        .scrollable(ScrollDirection.Vertical)
        .scrollBar(BarState.Auto)
        .onScroll((xOffset: number, yOffset: number) => {
          // 滚动超过 100vp 显示回顶按钮
          this.showBackToTop = yOffset > 100;
        })
        .width('80%')
        .height(180)
        .padding(16)
        .backgroundColor('rgba(255,255,255,0.7)')
        .borderRadius(12)

        // 右上角回顶按钮
        if (this.showBackToTop) {
          Button('↑')
            .width(32).height(32)
            .fontSize(18).fontColor('#FFFFFF')
            .backgroundColor('rgba(44, 62, 80, 0.6)')
            .borderRadius(16)
            .position({ x: '85%', y: 8 })       // 右上角
            .onClick(() => {
              this.ruleScroller.scrollEdge(Edge.Top);
            })
        }
      }
      .width('80%').height(180)
    }
  }
}

执行流程

  1. 用户向下滚规则面板。
  2. onScroll 回调触发,yOffset > 100showBackToTop = true
  3. if (this.showBackToTop) 渲染「↑」按钮。
  4. 用户点击「↑」,scrollEdge(Edge.Top) 平滑滚到顶。

5.3 onScroll 回调

ts 复制代码
.onScroll((xOffset: number, yOffset: number) => {
  console.info(`滚动偏移: x=${xOffset}, y=${yOffset}`);
})
参数 含义
xOffset 横向滚动偏移(vp)
yOffset 纵向滚动偏移(vp)

注意onScroll 滚动时高频触发(每帧一次),回调内不要做重计算。

六、Scroll 性能优化

6.1 Scroll vs List 的取舍

维度 Scroll List
子组件 一个(Column/Row 包内容) 多个 ListItem
复用机制 ❌ 无,全部渲染 ✅ LazyForEach 按需渲染
适合 内容条数固定且少(规则面板、说明页) 内容条数多或动态(聊天、战绩列表)
性能 条数多时卡顿 千条流畅

实战经验条数 < 20 用 Scroll,条数 ≥ 20 用 List + LazyForEach。本系列第 68 篇会专讲 LazyForEach 大列表。

6.2 避免嵌套 Scroll

ts 复制代码
// ❌ 错误:嵌套 Scroll,手势冲突
Scroll() {
  Scroll() { /* ... */ }
}

// ✅ 正确:用 List 嵌套,或用 Scroll + Column 分段
Scroll() {
  Column() {
    HeaderSection()
    ContentSection()
  }
}

6.3 contentScrollEnabled 动态禁滚

某些场景需要临时禁用滚动(如内容正在加载):

ts 复制代码
Scroll() { /* ... */ }
  .enabled(this.isContentReady)   // 内容未就绪时禁滚

七、完整代码:可滚动规则面板

ts 复制代码
// 改造版:规则面板套 Scroll,10 条规则可滚动
@Builder
MainMenuView() {
  Column() {
    Spacer().height('15%')

    Text('🐱').fontSize(72).margin({ bottom: 8 })
    Text('猫猫大作战').fontSize(36).fontWeight(FontWeight.Bold).fontColor('#2C3E50').margin({ bottom: 8 })
    Text('合并进化 · 策略消除').fontSize(16).fontColor('#95A5A6').margin({ bottom: 48 })

    if (this.highScore > 0) {
      Row() {
        Text('🏆 最高分: ').fontSize(16).fontColor('#F1C40F')
        Text(this.highScore.toString()).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#F1C40F')
      }.margin({ bottom: 32 })
    }

    Button('开始游戏')
      .width('70%').height(56)
      .fontSize(20).fontWeight(FontWeight.Bold)
      .fontColor('#FFFFFF').backgroundColor('#2ECC71')
      .borderRadius(28)
      .shadow({ radius: 8, color: 'rgba(46, 204, 113, 0.4)', offsetY: 4 })
      .onClick(() => { this.startGame(); })

    Spacer().height(24)

    // 规则面板(Scroll 改造)
    Scroll() {
      Column() {
        Text('游戏规则')
          .fontSize(14).fontWeight(FontWeight.Bold).fontColor('#2C3E50')
          .margin({ bottom: 8 })
        Text('• 点击列投放猫咪').fontSize(13).fontColor('#7F8C8D').margin({ bottom: 4 })
        Text('• 相邻同级猫咪自动合并升级').fontSize(13).fontColor('#7F8C8D').margin({ bottom: 4 })
        Text('• 连续合并触发连击加分').fontSize(13).fontColor('#7F8C8D').margin({ bottom: 4 })
        Text('• 猫咪堆到顶部则游戏结束').fontSize(13).fontColor('#7F8C8D').margin({ bottom: 4 })
        Text('• 同级猫咪相邻 2 个自动合并').fontSize(13).fontColor('#7F8C8D').margin({ bottom: 4 })
        Text('• 合并后等级 +1,得分按 3^n 增长').fontSize(13).fontColor('#7F8C8D').margin({ bottom: 4 })
        Text('• 连击窗口 1.5s,连续合并倍率叠加').fontSize(13).fontColor('#7F8C8D').margin({ bottom: 4 })
        Text('• 最高等级传奇猫,得分 2430').fontSize(13).fontColor('#7F8C8D').margin({ bottom: 4 })
        Text('• 自动生成间隔 2s,最多 40 只').fontSize(13).fontColor('#7F8C8D').margin({ bottom: 4 })
        Text('• 棋盘 5 列 8 行,第 2 行堆积则结束').fontSize(13).fontColor('#7F8C8D')
      }
      .alignItems(HorizontalAlign.Start)
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Auto)
    .width('80%')
    .height(180)
    .padding(16)
    .backgroundColor('rgba(255,255,255,0.7)')
    .borderRadius(12)

    Spacer()
  }
  .width('100%').height('100%')
  .linearGradient({
    direction: GradientDirection.Bottom,
    colors: [['#E8F4F8', 0.0], ['#D6EEF5', 0.5], ['#C9E8F2', 1.0]]
  })
  .alignItems(HorizontalAlign.Center)
}

八、踩坑提示

8.1 Scroll 不设 height 不滚动

ts 复制代码
// ❌ 错误:没设 height,Scroll 撑开到内容全高
Scroll() { Column() { /* 10 条规则 */ } }
// 内容全显示,不滚动

// ✅ 正确:设固定 height
Scroll() { /* ... */ }.height(180)

8.2 Scroll 内多个直接子组件

ts 复制代码
// ❌ 错误:多个直接子组件,只渲染第一个
Scroll() {
  Text('A')
  Text('B')
}

// ✅ 正确:用 Column 包裹
Scroll() {
  Column() {
    Text('A')
    Text('B')
  }
}

8.3 onScroll 回调内改 state 卡顿

ts 复制代码
// ❌ 错误:每帧改 state,触发重渲染,卡顿
.onScroll((_, yOffset) => {
  this.currentY = yOffset;     // 高频改 state
})

// ✅ 正确:节流,或只在阈值跨越时改
.onScroll((_, yOffset) => {
  const shouldShow = yOffset > 100;
  if (shouldShow !== this.showBackToTop) {
    this.showBackToTop = shouldShow;   // 只在状态变化时改
  }
})

九、调试技巧

  1. DevEco 预览器:鼠标在 Scroll 区域内滚轮即可测试滚动。
  2. console.info 打偏移 :onScroll 回调里 log yOffset,追滚动位置。
  3. 不滚动排查:检查 height 是否设置;检查内容是否真超出 height。
  4. 真机手感差 :检查 scrollBar(BarState.Auto) 是否误设为 Off 隐藏了反馈。

十、性能与最佳实践

  1. 条数少(<20)用 Scroll,条数多用 List + LazyForEach
  2. Scroll 必须设固定 height------否则撑开失去滚动能力。
  3. Scroll 只能有一个直接子组件------多个用 Column/Row 包。
  4. onScroll 高频回调内别改 state------节流或阈值跨越才改。
  5. 避免嵌套 Scroll------手势冲突,用 List 或分段 Column。

总结

本篇我们从 Scroll 滚动容器切入,掌握了Scroll 基本结构(必须设 height)Scroller 控制器(scrollTo/scrollEdge/currentOffset)onScroll 滚动监听与回顶按钮Scroll vs List 取舍 四大要点,并给出了可滚动规则面板完整代码。核心要点:Scroll 设固定 height 才滚动;Scroller 编程式控制;onScroll 高频回调别改 state;条数多用 List

下一篇我们将拆解 Badge------消息角标的实现。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

相关推荐
GitCode官方3 小时前
开源鸿蒙跨平台直播| 快手KRN鸿蒙适配与性能优化
华为·开源·harmonyos·atomgit
爱写代码的森3 小时前
鸿蒙三方库 | harmony-utils之TypeUtil类型检查工具详解
华为·harmonyos·鸿蒙·huawei
萌新源3 小时前
【鸿蒙开发实战】HarmonyOS 跨设备分享教程:隔空传送与碰一碰分享
华为·harmonyos
用户0934077735143 小时前
HarmonyOS WPS Open SDK 二开周回顾:从注册到关窗回传怎么串
typescript·harmonyos
程序员黑豆4 小时前
鸿蒙应用开发实战:轻松实现列表上拉加载更多
前端·华为·harmonyos
fiona20265 小时前
HarmonyOS应用《玄象》开发实战:LunarCalendar.ets 农历计算核心:朔望月 + 节气 + 闰月推算
harmonyos·鸿蒙
yaoyaoxingzhe5 小时前
HarmonyOS应用开发实战:猫猫大作战-`$r` 与 `$rawfile` 的区别、资源目录结构、多分辨率适配
harmonyos·鸿蒙
youtootech17 小时前
HarmonyOS 实战教程(八):个人中心与华为云服务集成 —— 以「柚兔自测量表」为例
华为·华为云·harmonyos
红烧大青虫18 小时前
HarmonyOS应用《玄象》开发实战:掷钱动画:animateTo + 缓动曲线的物理感模拟
harmonyos·鸿蒙