HarmonyOS ArkTS 实战:阅读器顶部栏“高度收缩 + 背景透明度过渡”(@AnimatableExtend 方案,能直接抄)

HarmonyOS ArkTS 实战:阅读器顶部栏"高度收缩 + 背景透明度过渡"(@AnimatableExtend 方案,能直接抄)

一起来构建生态吧~

需求场景很典型:阅读器页面一滚动,顶部栏从"有点高、信息完整"逐步收缩成"更薄、更沉浸";同时背景从透明慢慢变成不透明(避免内容顶到状态栏后看不清)。

这类动效如果用"手动 onScroll 改样式",很容易出现:跳变、卡顿、代码散。 我这里给你一套更工程化的写法: 用 @AnimatableExtend 把"高度"和"背景透明度"都做成可动画参数,你只负责给目标值,系统负责补间过渡。


1. 效果目标(先把体验说清楚)

我们把顶部栏拆成两个维度:

1)高度收缩(Height)

  • 初始:比如 64
  • 收缩后:比如 44
  • 滚动时随进度线性变化(或你自己换曲线)

2)背景透明度(Alpha)

  • 初始:0(完全透明,沉浸)
  • 收缩后:1(完全不透明,保证可读性)
  • 同样随滚动进度变化

同时再加两个"人写出来的细节":

  • 标题渐隐 / 渐显:大标题在展开态更明显,收缩后变淡或缩小
  • 阴影出现:背景变实后加一点阴影,像真实 App 顶部栏一样"压住内容"

2. 整体结构(别急着写动画,先搭骨架)

我建议阅读器页结构像这样(你之前有 Web 阅读器,照样适用):

sql 复制代码
 Stack(全屏)
 ├── 内容层(Web / List / Scroll)
 └── 顶部栏(Overlay,永远浮在最上)

顶部栏用 Stack/Row 都行,关键是: 它不参与内容布局(不然高度变化会挤压内容,体验很怪)。


3. 核心思路:用 1 个"折叠进度"驱动一切

我们定义一个折叠进度 collapseT:

  • 取值范围:0 ~ 1
  • 0:完全展开
  • 1:完全收缩

然后把所有视觉变化都变成数学映射:

  • height = lerp(expandedH, collapsedH, collapseT)
  • alpha = lerp(0, 1, collapseT)
  • titleOpacity = 1 - collapseT(或者更柔一点)
  • shadowOpacity = collapseT

这样代码会特别干净: 滚动只负责更新 collapseT,其它都是派生出来的。


4. 关键:@AnimatableExtend 两个扩展方法

为什么要用 @AnimatableExtend? 因为你会发现:有些属性你想"丝滑过渡",直接写 .height() .backgroundColor() 并不一定能跟你预期一样顺。 用 @AnimatableExtend 的思路是: 让参数可动画,参数每帧变化,你每帧把它应用到组件属性上。

4.1 扩展 1:可动画高度

java 复制代码
 @AnimatableExtend(Column)
 function animHeight(h: number) {
   this.height(h)
 }

这里我扩展在 Column 上,是因为顶部栏我会用 Column/Row/Stack 包一层。 你也可以写 @AnimatableExtend(Stack) 或 @AnimatableExtend(Row),看你实际结构。

4.2 扩展 2:可动画背景透明度(用 alpha 控制)

背景透明度最稳的做法: 用 rgba(或 Color + alpha 的方式)生成颜色,把 alpha 作为动画参数。

typescript 复制代码
 @AnimatableExtend(Stack)
 function animBgAlpha(alpha: number) {
   // alpha: 0~1
   // 用 RGBA 生成颜色(下面是白底示例,你可以换成你的主题色)
   this.backgroundColor(`rgba(255, 255, 255, ${alpha})`)
 }

注意:有的同学会直接在 stateStyles 里写背景,但我们这里是"连续渐变",更适合用 animatable 参数驱动。


5. 完整示例:阅读器页(滚动驱动顶部栏动效)

这里我用 Scroll + Column 模拟内容滚动。 你如果是 Web,只要把"滚动位置 scrollTop / progress"换成你 Web 上报的值即可(你前面已经做过 onMessage 上报进度,那套能直接接)。

5.1 关键工具函数:lerp + clamp

typescript 复制代码
 function clamp01(x: number): number {
   if (x < 0) return 0
   if (x > 1) return 1
   return x
 }
 ​
 function lerp(a: number, b: number, t: number): number {
   return a + (b - a) * t
 }

5.2 页面代码(可直接抄,逻辑尽量写得像真实项目)

scss 复制代码
 @Entry
 @Component
 struct ReaderPage {
   // 顶部栏展开/收缩配置(你可以按设计稿改)
   private readonly expandedH: number = 64
   private readonly collapsedH: number = 44
 ​
   // 这个是"动画目标值",我们只改它,不直接改 UI
   @State private collapseT: number = 0
 ​
   // 真实滚动高度(示例用 scrollY 近似)
   @State private scrollY: number = 0
 ​
   // 当 scrollY 到达这个阈值时,顶部栏基本收缩完成
   private readonly collapseRange: number = 120
 ​
   build() {
     Stack() {
       // ========= 内容层 =========
       Scroll() {
         Column({ space: 12 }) {
           // 模拟内容
           ForEach(new Array(60).fill(0).map((_, i) => i), (i: number) => {
             Text(`第 ${i + 1} 段内容:这里模拟阅读器正文...`)
               .fontSize(16)
               .fontColor('#222')
               .padding({ left: 16, right: 16, top: 8, bottom: 8 })
           })
         }
         .padding({ top: this.expandedH + 8 }) // 给内容让出顶部栏空间(很关键!)
       }
       .onScroll((xOffset: number, yOffset: number) => {
         this.scrollY = yOffset
 ​
         // 把滚动距离映射成 0~1
         const t = clamp01(yOffset / this.collapseRange)
 ​
         // 只更新目标值,让动画系统去补间
         // 这里用 animateTo 更"人写",可控性更好
         animateTo({ duration: 220, curve: Curve.EaseInOut }, () => {
           this.collapseT = t
         })
       })
 ​
       // ========= 顶部栏(覆盖层) =========
       this.TopBar()
     }
     .width('100%')
     .height('100%')
     .backgroundColor('#F6F7F9')
   }
 ​
   @Builder
   TopBar() {
     // 派生:高度与背景透明度
     const h = lerp(this.expandedH, this.collapsedH, this.collapseT)
     const bgAlpha = lerp(0.0, 1.0, this.collapseT)
     const titleOpacity = lerp(1.0, 0.0, this.collapseT)      // 大标题渐隐
     const smallTitleOpacity = lerp(0.0, 1.0, this.collapseT) // 小标题渐显
     const shadowAlpha = lerp(0.0, 0.12, this.collapseT)      // 阴影随收缩出现
 ​
     Stack() {
       // 顶部栏内容
       Row() {
         Text('返回')
           .fontSize(14)
           .fontColor('#2F7BFF')
           .padding(8)
           .onClick(() => {
             // TODO: router back
           })
 ​
         Blank()
 ​
         // 收缩后显示的小标题(更像真实阅读器)
         Text('章节标题')
           .fontSize(16)
           .fontWeight(FontWeight.Medium)
           .fontColor('#222')
           .opacity(smallTitleOpacity)
 ​
         Blank()
 ​
         Text('目录')
           .fontSize(14)
           .fontColor('#2F7BFF')
           .padding(8)
           .onClick(() => {
             // TODO: open chapter list
           })
       }
       .padding({ left: 12, right: 12 })
       .height('100%')
       .alignItems(VerticalAlign.Center)
 ​
       // 展开态的大标题(更沉浸、更像"阅读页顶部信息")
       Column() {
         Text('书名 / 阅读器')
           .fontSize(20)
           .fontWeight(FontWeight.Bold)
           .fontColor('#111')
           .opacity(titleOpacity)
       }
       .position({ x: 16, y: 10 })
     }
     // 关键:可动画高度 + 可动画背景透明度
     .animHeight(h)
     .animBgAlpha(bgAlpha)
     // 轻微阴影:背景变实的时候出现一点压住正文的感觉
     .shadow({
       radius: 12,
       color: `rgba(0,0,0,${shadowAlpha})`,
       offsetX: 0,
       offsetY: 6
     })
     .width('100%')
     .position({ x: 0, y: 0 })
   }
 }

6. 这段代码里最容易忽略的"真实细节"

6.1 内容顶部 padding 必须给出来

你看到我这里写了:

css 复制代码
 .padding({ top: this.expandedH + 8 })

这是很现实的问题: 顶部栏是 overlay,如果内容不让空间,正文会被盖住,看起来像"顶到状态栏",体验会很差。

你也可以做得更精细:padding 跟着顶部栏高度动态变化。 但一般阅读器里为了稳定,直接用展开高度固定 padding 就够用。

相关推荐
梦想不只是梦与想13 小时前
HarmonyOS应用分层架构设计
harmonyos·分层架构·一次开发,多端部署
MardaWang15 小时前
当滚动逃离了框架 ——HarmonyOS Web 与原生混排滚动的冲突本质与解法
harmonyos·arkts·鸿蒙·deveco studio
tsqtsqtsq030916 小时前
DevEco Studio 介绍
harmonyos
HwJack2020 小时前
【共创稿事节】HarmonyOS 7文旅展陈展厅大空间 3DGS 重建的分块策略与拼接踩坑
3d·华为·harmonyos
m0_738185821 天前
Flutter 鸿蒙化实战:media_info 适配 OpenHarmony,媒体信息与缩略图
flutter·华为·harmonyos·鸿蒙·媒体
m0_738185821 天前
Flutter 鸿蒙化实战:just_audio 适配 OpenHarmony,功能强大的播放器
flutter·华为·harmonyos·鸿蒙
翼辉cto1 天前
Kotlin Multiplatform 三方库 SQLDelight 的 OpenHarmony 鸿蒙化适配实战
开发语言·kotlin·harmonyos
SuperHeroWu71 天前
TraeCode 国内版接入 DevEco CLI:用官方知识开发鸿蒙应用
ai编程·harmonyos·知识库·trae·aicoding·skills·deveco cli
2501_919749031 天前
华为鸿蒙免费口算练习APP—小羊口算
华为·harmonyos·鸿蒙