欢迎光临
我们一直在努力

HarmonyOS7 Scroll 嵌套滚动、章节锚点与返回顶部实战

文章目录

      • 前言
      • 代码讲解
        • 页面入口怎么安排
        • 数据模型先稳住
        • 状态变量控制页面反馈
        • 布局代码不要从样式开始读
        • Builder 和方法承担复用
        • 使用方式
      • 完整代码
      • 关键代码解析
        • `interface` 不是摆设
        • `@State` 是交互的开关
        • 列表和卡片要靠数据驱动
        • 事件回调要短一点
        • 样式参数别急着抽常量
      • 总结

前言

长文章页的体验不在内容多少,而在章节定位、滚动反馈和返回顶部是否顺手。

我建议把这篇当成一个可以直接改造的 ArkUI 页面模板来看。先跑起来,确认页面展示和交互都正常;再把里面的模拟数据替换成接口数据;最后再调整颜色、间距和业务字段。这样改起来比较稳,不容易一边改样式一边把状态逻辑弄乱。

A hand-drawn sketch-note diagram illustrating the

这篇文章会按「页面意图、数据结构、状态流转、布局组织、交互细节」这条线来读代码。你不用从第一行样式硬看到最后,先抓住主线,后面的细节会轻松很多。

代码讲解

页面入口怎么安排

代码里的入口组件是 ScrollableArticleGuidePage。如果你把它当成单独页面预览,保留 @Entry 就可以;如果要塞进现有工程的路由体系,通常会去掉 @Entry,再由外层页面或路由模块统一管理。

这个习惯很重要。一个项目里入口页面太多,预览时容易混,后期拆组件也会麻烦。我的做法是:案例阶段保留入口,接业务时只保留真正需要被路由访问的页面。

数据模型先稳住

本案例涉及的数据模型主要有:SectionInfo、ArticleBlock。

这些 interface 的作用不是为了“看起来规范”,而是给 UI 层划边界。比如列表项需要标题、描述、颜色、选中态,就应该在模型里说清楚。后续从接口拿数据时,可以在请求层做字段映射,别让后端字段名直接污染页面代码。

如果你准备接真实业务,建议把模拟数组替换成接口返回后的 ViewModel。页面只关心自己要渲染什么,不关心接口原始字段长什么样。

状态变量控制页面反馈

本案例里的关键状态包括:scrollTop、showBackTop、activeSection。

@State 的职责是让 UI 对用户操作有反馈。比如选中某个卡片、切换分类、展开内容、修改输入值、控制加载中状态,这些都应该由状态驱动。状态一变,相关 UI 自动刷新,这也是 ArkUI 声明式开发最核心的体验。

这里有个容易踩的坑:不要把所有数据都塞进 @State。静态配置用普通数组就够了,只有用户会改、接口会更新、页面需要重新渲染的数据,才适合放进状态里。

布局代码不要从样式开始读

这个页面里比较关键的组件有:

  • Row:横向排列、左右分栏、按钮组或卡片行都依赖它来控制水平方向的节奏。
  • Column:纵向组织标题、内容、操作区,适合搭页面主骨架。
  • Stack:负责图层叠放,常见于角标、遮罩、封面文字压图。
  • Divider:负责信息分组,不只是画一条线,而是帮用户理解层级。
  • List:适合长列表和分组列表,重点是数据过滤、滚动和状态同步。
  • Grid:适合固定宫格、桌面图标、内容卡片等二维排布。

读布局时,我更建议先找大容器,再看内部如何拆块。比如一个页面通常会先用 Column 纵向放标题区、内容区、操作区;内容区里再用 Row、Grid、List 或其他容器细分。这样看代码,层级会比逐行看 .padding()、.fontSize() 清楚得多。

如果页面显示错位,优先检查容器关系,而不是马上改像素。很多问题不是某个 .margin() 不对,而是外层容器的宽高、滚动方向、权重分配或对齐方式没想清楚。

An infographic sketch-note showing the recommended

Builder 和方法承担复用

这篇没有额外拆出 Builder,页面结构主要集中在 build() 中。

辅助方法不多,逻辑主要写在事件回调和渲染表达式里。

@Builder 更适合复用 UI 片段,比如卡片、列表行、头部区域、按钮组。普通方法更适合放计算逻辑,比如筛选列表、统计数量、切换状态、生成颜色。两者分清楚后,页面会干净很多。

我不建议把所有东西都塞进 build()。刚开始代码少还行,需求一多,build() 会变成几百行,后面改一个按钮都要找半天。

使用方式

把下面的完整代码放到 ArkTS 页面文件里即可运行。文章中的组件名已经是语义化命名,不包含编号式案例名称,接路由时直接使用 ScrollableArticleGuidePage 就行。

如果你要改成业务页面,优先改三处:数据模型、状态变量、事件方法。样式可以后调,先保证数据流和交互是通的。

完整代码

// ScrollableArticleGuidePage: 滚动容器综合 – Scroll 嵌套与滚动控制
// 知识点: Scroll、scrollable方向、scrollBar、onScroll、scrollTo、嵌套滚动

interface SectionInfo {
sectionId: number
sectionTitle: string
sectionEmoji: string
anchorY: number
}

interface ArticleBlock {
blockId: number
blockType: string
blockContent: string
blockBg: string
}

@Entry
@Component
struct ScrollableArticleGuidePage {
@State scrollTop: number = 0
@State showBackTop: boolean = false
@State activeSection: number = 0
private scrollController: Scroller = new Scroller()

private sections: SectionInfo[] = [
{ sectionId: 0, sectionTitle: '简介', sectionEmoji: '📖', anchorY: 0 },
{ sectionId: 1, sectionTitle: '核心特性', sectionEmoji: '⚡', anchorY: 400 },
{ sectionId: 2, sectionTitle: '使用方法', sectionEmoji: '🔧', anchorY: 800 },
{ sectionId: 3, sectionTitle: '注意事项', sectionEmoji: '⚠️', anchorY: 1200 },
]

private articleBlocks: ArticleBlock[] = [
{ blockId: 1, blockType: 'heading', blockContent: '📖 Scroll 组件简介', blockBg: '#FFFFFF' },
{ blockId: 2, blockType: 'paragraph', blockContent: 'Scroll 组件是 HarmonyOS ArkUI 中提供滚动能力的基础容器。当子组件的布局尺寸超过 Scroll 父组件的视图尺寸时,内容可以滚动显示。', blockBg: '#FFFFFF' },
{ blockId: 3, blockType: 'paragraph', blockContent: '默认情况下,Scroll 支持垂直方向滚动。可以通过 scrollable 属性修改为水平方向(Horizontal)或双向滚动(Free)。', blockBg: '#FFFFFF' },
{ blockId: 4, blockType: 'code', blockContent: 'Scroll(controller) {\\n Column() { … }\\n}\\n.scrollable(ScrollDirection.Vertical)\\n.scrollBar(BarState.Auto)', blockBg: '#1E1E2E' },
{ blockId: 5, blockType: 'divider', blockContent: '', blockBg: '#FFFFFF' },
{ blockId: 6, blockType: 'heading', blockContent: '⚡ 核心特性', blockBg: '#FFFFFF' },
{ blockId: 7, blockType: 'tip', blockContent: '✅ 支持设置滚动方向:Vertical / Horizontal / Free', blockBg: '#F0FFF4' },
{ blockId: 8, blockType: 'tip', blockContent: '✅ 滚动条样式:Auto(自动显示)/ On / Off', blockBg: '#F0FFF4' },
{ blockId: 9, blockType: 'tip', blockContent: '✅ 边缘效果:Spring(弹簧)/ Fade(渐隐)/ None', blockBg: '#F0FFF4' },
{ blockId: 10, blockType: 'tip', blockContent: '✅ onScroll 回调:实时获取滚动偏移量', blockBg: '#F0FFF4' },
{ blockId: 11, blockType: 'tip', blockContent: '✅ 编程式控制:scrollTo、scrollEdge、scrollPage', blockBg: '#F0FFF4' },
{ blockId: 12, blockType: 'divider', blockContent: '', blockBg: '#FFFFFF' },
{ blockId: 13, blockType: 'heading', blockContent: '🔧 使用方法', blockBg: '#FFFFFF' },
{ blockId: 14, blockType: 'paragraph', blockContent: '通过 Scroller 控制器对象,可以主动控制 Scroll 组件的滚动行为,例如滚动到指定位置、滚动到顶部/底部等。', blockBg: '#FFFFFF' },
{ blockId: 15, blockType: 'code', blockContent: 'private ctrl: Scroller = new Scroller()\\n\\n// 滚动到顶部\\nthis.ctrl.scrollEdge(Edge.Top)\\n\\n// 滚动到指定坐标\\nthis.ctrl.scrollTo({ xOffset: 0, yOffset: 500 })', blockBg: '#1E1E2E' },
{ blockId: 16, blockType: 'paragraph', blockContent: 'onScroll 回调可获取当前滚动偏移量,适合实现标题栏背景渐变、返回顶部按钮显示等交互效果。', blockBg: '#FFFFFF' },
{ blockId: 17, blockType: 'divider', blockContent: '', blockBg: '#FFFFFF' },
{ blockId: 18, blockType: 'heading', blockContent: '⚠️ 注意事项', blockBg: '#FFFFFF' },
{ blockId: 19, blockType: 'warning', blockContent: '⚠️ Scroll 内部只能有一个直接子组件,通常配合 Column / Row 使用', blockBg: '#FFFBF0' },
{ blockId: 20, blockType: 'warning', blockContent: '⚠️ 嵌套滚动时需配置 nestedScroll 属性避免滚动冲突', blockBg: '#FFFBF0' },
{ blockId: 21, blockType: 'warning', blockContent: '⚠️ Scroll 内子组件不要设置 height(100%) 否则无法滚动', blockBg: '#FFFBF0' },
{ blockId: 22, blockType: 'warning', blockContent: '⚠️ 列表类组件(List/Grid)有内置滚动能力,不建议再套 Scroll', blockBg: '#FFFBF0' },
]

build() {
Stack({ alignContent: Alignment.BottomEnd }) {
Column({ space: 0 }) {
// 顶部标题栏(随滚动改变透明度)
Row({ space: 0 }) {
Text('← ')
.fontSize(18)
.fontColor('#1A1A1A')
Text('Scroll 滚动容器')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A1A')
.layoutWeight(1)
Text('⋯')
.fontSize(20)
.fontColor('#1A1A1A')
}
.width('100%')
.height(52)
.padding({ left: 16, right: 16 })
.backgroundColor(`rgba(255,255,255,${Math.min(1, this.scrollTop / 100)})`)
.border({
width: { bottom: this.scrollTop > 20 ? 1 : 0 },
color: '#F0F0F0'
})

// 章节锚点横向导航
Scroll() {
Row({ space: 8 }) {
ForEach(this.sections, (sec: SectionInfo) => {
Row({ space: 4 }) {
Text(sec.sectionEmoji)
.fontSize(13)
Text(sec.sectionTitle)
.fontSize(13)
.fontColor(this.activeSection === sec.sectionId ? '#FFFFFF' : '#555555')
}
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor(this.activeSection === sec.sectionId ? '#007DFF' : '#F0F0F0')
.borderRadius(16)
.onClick(() => {
this.activeSection = sec.sectionId
this.scrollController.scrollTo({
xOffset: 0,
yOffset: sec.anchorY,
animation: { duration: 400, curve: Curve.EaseOut }
})
})
}, (sec: SectionInfo) => sec.sectionId.toString())
}
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.backgroundColor('#FFFFFF')
.padding({ top: 8, bottom: 8 })
.border({ width: { bottom: 1 }, color: '#F0F0F0' })

// 主内容区 Scroll
Scroll(this.scrollController) {
Column({ space: 0 }) {
ForEach(this.articleBlocks, (block: ArticleBlock) => {
if (block.blockType === 'heading') {
Text(block.blockContent)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A1A')
.padding({ left: 16, right: 16, top: 20, bottom: 10 })
.width('100%')
} else if (block.blockType === 'paragraph') {
Text(block.blockContent)
.fontSize(14)
.fontColor('#444444')
.lineHeight(22)
.padding({ left: 16, right: 16, top: 4, bottom: 8 })
.width('100%')
} else if (block.blockType === 'code') {
Text(block.blockContent)
.fontSize(13)
.fontColor('#A8FF78')
.fontFamily('monospace')
.padding({ left: 16, right: 16, top: 14, bottom: 14 })
.width('100%')
.backgroundColor('#1E1E2E')
.margin({ left: 12, right: 12, bottom: 8 })
.borderRadius(8)
.lineHeight(22)
} else if (block.blockType === 'tip') {
Text(block.blockContent)
.fontSize(13)
.fontColor('#1A7A4A')
.padding({ left: 14, right: 14, top: 10, bottom: 10 })
.width('100%')
.backgroundColor('#F0FFF4')
.margin({ left: 12, right: 12, bottom: 4 })
.borderRadius(8)
.border({ width: { left: 3 }, color: '#10B981' })
} else if (block.blockType === 'warning') {
Text(block.blockContent)
.fontSize(13)
.fontColor('#7A4A00')
.padding({ left: 14, right: 14, top: 10, bottom: 10 })
.width('100%')
.backgroundColor('#FFFBF0')
.margin({ left: 12, right: 12, bottom: 4 })
.borderRadius(8)
.border({ width: { left: 3 }, color: '#F59E0B' })
} else if (block.blockType === 'divider') {
Divider()
.strokeWidth(6)
.color('#F5F6FA')
.margin({ top: 12, bottom: 4 })
}
}, (block: ArticleBlock) => block.blockId.toString())

Row({ space: 0 }).height(32)
}
.width('100%')
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Auto)
.edgeEffect(EdgeEffect.Spring)
.onScroll((xOffset: number, yOffset: number) => {
this.scrollTop = this.scrollController.currentOffset().yOffset
this.showBackTop = this.scrollTop > 300
// 更新活跃章节
if (this.scrollTop < 400) {
this.activeSection = 0
} else if (this.scrollTop < 800) {
this.activeSection = 1
} else if (this.scrollTop < 1200) {
this.activeSection = 2
} else {
this.activeSection = 3
}
})
.layoutWeight(1)
.backgroundColor('#FFFFFF')
}
.width('100%')
.height('100%')

// 返回顶部按钮(浮动)
if (this.showBackTop) {
Column({ space: 2 }) {
Text('⬆️')
.fontSize(18)
Text('顶部')
.fontSize(10)
.fontColor('#007DFF')
}
.width(50)
.height(50)
.backgroundColor('#FFFFFF')
.borderRadius(25)
.shadow({ radius: 8, color: '#20000000', offsetY: 2 })
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.margin({ right: 16, bottom: 24 })
.onClick(() => {
this.scrollController.scrollEdge(Edge.Top)
})
}
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
}
}

A flowchart sketch-note detailing the state manage

关键代码解析

interface 不是摆设

很多新手写 ArkUI 页面时,会直接在 ForEach 里使用一堆临时对象。页面小的时候没问题,但字段一多,就很难知道每个字段到底用于哪里。本案例把数据结构提前声明出来,读代码时能很快判断每个 UI 区块依赖哪些字段。

后续如果接接口,也建议保留这层结构。接口可以变,页面模型尽量稳定。这样页面不会因为后端多返回一个字段、少返回一个字段就跟着大改。

@State 是交互的开关

本案例的交互反馈都围绕状态展开。点击、切换、输入、刷新这类行为,本质上都是修改某个状态值,然后让 UI 重新计算显示结果。

写这类代码时有一个简单判断:如果某个值变化后,页面应该立刻变,那它大概率应该是状态;如果它只是配置项、静态文案、固定颜色,就不要放进状态。

列表和卡片要靠数据驱动

只要页面里出现重复结构,就应该优先想到数据数组加 ForEach。这样新增一项、删除一项、调整顺序都只动数据,不用复制粘贴一段 UI。

这个案例的完整代码里已经把主要内容抽成数组或方法。你可以试着多加一条数据,看页面是否能自然渲染出来。如果能,说明结构是健康的;如果加一条就要改很多 UI,说明组件拆分还不够。

事件回调要短一点

.onClick()、onChange() 这类回调里可以直接改状态,但不建议堆太多逻辑。简单切换可以写在回调里,复杂逻辑最好拆成方法。

这样做有两个好处:一是读页面结构时不会被业务逻辑打断;二是后面要加埋点、请求接口、异常处理时,有明确的位置可以改。

样式参数别急着抽常量

这个案例里有不少颜色、间距、圆角、字号。学习阶段直接写在组件链上,反而更容易看出效果。等你确定页面风格稳定后,再把主题色、通用间距、卡片圆角提到统一常量里。

过早抽象会让示例代码变绕。先让页面清楚,再谈工程化,这是我比较推荐的顺序。

总结

这个案例真正值得学的,不只是某个 ArkUI 组件怎么写,而是一个页面如何从数据走到状态,再走到布局和交互。

你可以按这个顺序改造:先替换数据模型,再确认状态更新是否正确,接着调整布局容器,最后打磨样式细节。只要这条主线不乱,页面规模变大也不会失控。

如果后续继续扩展,我会优先把可复用的卡片、列表行、筛选栏或控制区拆成独立组件。这样主页面只保留数据和流程,代码会更接近真实项目里的写法。

赞(0)
未经允许不得转载:171主机测评 » HarmonyOS7 Scroll 嵌套滚动、章节锚点与返回顶部实战
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址