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

相关推荐
万少2 小时前
HarmonyOS6 接入快手 SDK 指南
前端·harmonyos
帅哥一天八碗米饭2 小时前
HarmonyOS ArkTS:自动“缓存池复用监控日志”怎么做
harmonyos
kirk_wang2 小时前
Flutter 鸿蒙项目 Android Studio 点击 Run 失败 ohpm 缺失
flutter·android studio·harmonyos
qq_463408423 小时前
React Native跨平台技术在开源鸿蒙中开发一个奖励兑换模块,增加身份验证和授权机制(如JWT),以防止未授权的积分兑换
react native·开源·harmonyos
Fate_I_C3 小时前
Flutter鸿蒙0-1开发-工具环境篇
flutter·华为·harmonyos·鸿蒙
二流小码农3 小时前
鸿蒙开发:一个底部的曲线导航
android·ios·harmonyos
特立独行的猫a3 小时前
OpenHarmony开源鸿蒙应用签名机制深度解析与工具使用指南
华为·开源·harmonyos·签名
Fate_I_C4 小时前
Flutter鸿蒙0-1开发-flutter create <prjn>
flutter·华为·harmonyos·鸿蒙
Python私教4 小时前
鸿蒙应用的网络请求和数据处理:从HTTP到本地缓存的完整方案
网络·http·harmonyos