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 就够用。

相关推荐
前端菜鸟日常12 小时前
鸿蒙开发实战:100 个项目疑难杂症汇编
汇编·华为·harmonyos
jin12332213 小时前
基于React Native鸿蒙跨平台移动端表单类 CRUD 应用,涵盖地址列表展示、新增/编辑/删除/设为默认等核心操作
react native·react.js·ecmascript·harmonyos
摘星编程14 小时前
OpenHarmony环境下React Native:DatePicker日期选择器
react native·react.js·harmonyos
一起养小猫15 小时前
Flutter for OpenHarmony 实战:番茄钟应用完整开发指南
开发语言·jvm·数据库·flutter·信息可视化·harmonyos
一起养小猫15 小时前
Flutter for OpenHarmony 实战:数据持久化方案深度解析
网络·jvm·数据库·flutter·游戏·harmonyos
不爱吃糖的程序媛15 小时前
Cordova/Capacitor 在鸿蒙生态中的实践与展望
华为·harmonyos
大雷神16 小时前
HarmonyOS智慧农业管理应用开发教程--高高种地--第26篇:考试系统 - 题库与考试
harmonyos
前端菜鸟日常18 小时前
2026 鸿蒙原生开发 (ArkTS) 面试通关指南:精选 50 题
华为·面试·harmonyos
木斯佳18 小时前
HarmonyOS 6实战(源码教学篇)— PinchGesture 图像处理【仿证件照工具实现手势交互的canvas裁剪框】)
图像处理·交互·harmonyos
听麟19 小时前
HarmonyOS 6.0+ PC端手绘板协同创作工具开发实战:压感交互与跨端流转落地
华为·交互·harmonyos