云计算百科
云计算领域专业知识百科平台

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

文章配图:Scroll 容器、Scroller 控制器、滚动监听与性能

页面预览

前言

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

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

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

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

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

// 来源: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 的解法:

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

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

二、Scroll 滚动容器

2.1 基本结构

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

核心约束:

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

2.2 scrollable 滚动方向

.scrollable(ScrollDirection.Vertical) // 纵向(默认)
.scrollable(ScrollDirection.Horizontal) // 横向

方向适用height 要求
Vertical 列表、文章 必须设
Horizontal 横向轮播、Tab 条 必须设 width

2.3 scrollBar 滚动条

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

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

三、Scroller 滚动控制器

3.1 创建 Scroller

private scroller: Scroller = new Scroller();

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

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

3.2 scrollTo 滚动到指定位置

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

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

3.3 scrollEdge 滚动到边缘

this.scroller.scrollEdge(Edge.Top) // 滚到顶
this.scroller.scrollEdge(Edge.Bottom) // 滚到底

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

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

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

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

3.5 currentOffset 获取当前偏移

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

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

四、用 Scroll 改造规则面板

4.1 改造为可滚动规则面板

@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) 固定高度

Scroll() { /* 10 条规则 */ }
.height(180) // 固定高度

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

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

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

5.1 场景

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

5.2 实现

@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)
}
}
}

执行流程:

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

    .onScroll((xOffset: number, yOffset: number) => {
    console.info(`滚动偏移: x=${xOffset}, y=${yOffset}`);
    })

    参数含义
    xOffset 横向滚动偏移(vp)
    yOffset 纵向滚动偏移(vp)

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

    六、Scroll 性能优化

    6.1 Scroll vs List 的取舍

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

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

    6.2 避免嵌套 Scroll

    // ❌ 错误:嵌套 Scroll,手势冲突
    Scroll() {
    Scroll() { /* … */ }
    }

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

    6.3 contentScrollEnabled 动态禁滚

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

    Scroll() { /* … */ }
    .enabled(this.isContentReady) // 内容未就绪时禁滚

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

    // 改造版:规则面板套 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 不滚动

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

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

    8.2 Scroll 内多个直接子组件

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

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

    8.3 onScroll 回调内改 state 卡顿

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

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

    九、调试技巧

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

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

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

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

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


    相关资源:

    • 「猫猫大作战」项目源码:本仓库 entry/src/main/ets/pages/Index.ets
    • Scroll 滚动容器官方指南
    • Scroller 滚动控制器官方指南
    • List 列表组件官方指南
    • ArkUI 滚动性能最佳实践
    • 开源鸿蒙跨平台社区
    • HarmonyOS 开发者官方文档首页
    • 系列索引:本仓库 articles/INDEX.md
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » HarmonyOS应用开发实战:猫猫大作战-Scroll 容器、Scroller 控制器、滚动监听与性能
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!