当前位置: 首页 > news >正文

【鸿蒙开发避坑】使用全局状态变量控制动画时,动画异常甚至动画方向与预期相反的原因分析以及解决方案

【鸿蒙开发避坑】使用全局状态变量控制动画,动画异常甚至动画方向相反的原因分析以及解决方案

    • 一、问题复现
      • 1、问题描述
      • 2、问题示意图
    • 二、原因深度解析
      • 1、查看文档
      • 2、调试
      • 3、原因总结:
        • (1)第一次进入播放页面功能一切正常的原因
        • (2)第二次再次进入播放页面出现问题的原因
    • 三、解决方案
    • 四、总结与最佳实践
      • (一)核心问题归纳
      • (二)最佳实践方案
      • (三)开发建议
    • 问题版本完整代码可在代码仓库的Issues中查看

一、问题复现

1、问题描述

定义了AvPlayManager工具类对象来管理APP中歌曲的播放控制,AVPlayManager中有个应用了AppStorageV2状态存储的用于放置共享播放数据,并且使用**@ObservedV2装饰的GlobalMusic类对象currentSong**,其在AppStorageV2中的key值为'SONG_KEY',之后会在多个与音乐播放相关的页面读取这个数据。

//代码已简化,完整代码见文章末尾gitee仓库链接class AvPlayerManager {//属性+方法//播放器player: media.AVPlayer | null = null//共享播放数据currentSong: GlobalMusic = AppStorageV2.connect(GlobalMusic, 'SONG_KEY', () => new GlobalMusic())!//篇幅限制只展示部分代码}

GloabalMusic有一个使用**@Trace装饰的Boollean类型的成员isPlay**,当歌曲在播放时其值为 true,其他时候都为false

@ObservedV2
export class GlobalMusic {@Trace img: string = ""  // 音乐封面@Trace name: string = ""  // 音乐名称@Trace author: string = ""  // 作者@Trace url: string = ""  // 当前播放链接@Trace time: number = 0  // 播放时间@Trace duration: number = 0  // 歌曲总时长//歌曲列表@Trace playIndex:number=0//当前播放的歌曲索引@Trace playList:SongItemType[]=[]//播放歌曲列表//播放和暂停@Trace isPlay:boolean=false//播放的状态,默认false//播放模式@Trace playMode:'auto'|'random'|'repeat'='auto'//重置数据reset(){this.img=''this.name=''this.author=''this.url=''this.time=0this.duration=0this.playIndex=0this.playList=[]this.isPlay=falsethis.playMode='auto'}
}

在歌曲播放Play页面获取到共享播放数据全局UI状态变量引用:

  // 当前播放的歌曲,全局UI状态变量@Local playState: GlobalMusic = AppStorageV2.connect(GlobalMusic, 'SONG_KEY', () => new GlobalMusic())!

使用playState.isPlay的值来驱动歌曲播放页面的歌曲封面无限顺时针旋转动画和唱片机唱针图片旋转动画:

//代码已简化,完整代码见文章末尾gitee仓库链接//专辑封面Image(this.playState.img).rotate({angle: this.playState.isPlay?360:0,centerX: '50%',centerY: '50%'}).animation({duration: 4000, //动画时长4秒iterations: -1, //无限循环curve: Curve.Linear//匀速运动}).width('70%').borderRadius(400)// 唱针Image($r('app.media.ic_stylus')).width(200).aspectRatio(1).rotate({angle: this.playState.isPlay ? -40 : -60,centerX: 100,centerY: 30}).animation({duration: 500})

在第一次进入播放页面的时候,两个动画都能被isPlay正确触发同步(isPlay为true时,歌曲封面旋转,唱片机唱针图片旋转到歌曲封面上,isPlay为false时,歌曲封面停止旋转,唱片机唱针图片回到原位置),当点击暂停按钮(isPlay变为false)后返回上一级页面,然后再次进入这个页面时(isPlay为true),唱片机动画触发与isPlay的值是正常对应的,但是歌曲封面旋转动画不触发(即isPlay值为true,但是封面不旋转),且当下一次isPlay改变时(变为false),歌曲封面旋转动画才触发开始旋转,之后歌曲封面的旋转动画触发逻辑会与isPlay的值不对应,即isPlay为ture,但是歌曲封面却不旋转,isPlay值为false,歌曲封面却旋转,还是逆时针旋转。(有点绕。。。。。。)

2、问题示意图

在这里插入图片描述

二、原因深度解析

1、查看文档

查看鸿蒙官方开发文档中对animation动画的描述如下图:
在这里插入图片描述
鸿蒙开发文档-实现属性动画

通过文档中对于animation属性动画的原理,发现重要的线索:识别组件的可动画属性变化
重点是变化

2、调试

在包裹Image组件的父组件Row的onAppear()函数中输出日志查看isPlay的值
//代码已简化,完整代码见文章末尾gitee仓库链接Row() {//专辑封面Image(this.playState.img).rotate({angle: this.playState.isPlay?280:0,centerX: '50%',centerY: '50%'}).animation({duration: 4000, //动画时长4秒iterations: -1, //无限循环curve: Curve.Linear//匀速运动}).width('70%').borderRadius(400)}.onAppear(()=>{console.info('this.playState.isPlay值为:',this.playState.isPlay)})
}
第一次进入歌曲播放页面时,onAppear中打印日志结果如下

在这里插入图片描述

暂停歌曲播放,再退出播放界面,第二次进入歌曲播放界面时,onAppear中打印日志结果如下

在这里插入图片描述

3、原因总结:

(1)第一次进入播放页面功能一切正常的原因

首先在逻辑上,在进入播放页面时,isPlay的值并非手动设置的,而是在AvPlayerManager管理类的成员player: media.AVPlayer的绑定的状态改变监听函数中,当监听到歌曲准备就绪时再设置isPlay的值为true,且音频使用的是网络音频,需要异步网络获取资源。因此在第一次进入播放页面时,在时间线上,由于网络请求的异步性,出现animation动画先读取到isPlayfalse,然后网络音频准备就绪并设置isPlaytrueanimation识别到组件属性变化,自动添加动画。
在这里插入图片描述

(2)第二次再次进入播放页面出现问题的原因

逻辑上和第一次大部分相同,不同点在于,由于只是暂停,全局控制歌曲播放的AvPlayerManager还在,其media.AVPlayer类型成员player已经初始化过了,歌曲资源也还在内存中,当再次点击同一首歌曲进入歌曲播放界面时,歌曲能够马上准备就绪,导致在animation动画读取isPlay的值之前,歌曲已经准备就绪,isPlay已经是true了,所以animation动画相关的组件属性初始值就为true,没有发生改变,自然不会触发动画绘制,歌曲封面一开始就处于旋转了360度的状态。当点击暂停按钮时,设置isPlay的值为true,这时animation才监听到组件属性值发生变化变成了0度,触发动画360度到0度的逆时针旋转动画,就出现了逆时针旋转的现象。
在这里插入图片描述

//代码已简化,完整代码见文章末尾gitee仓库链接class AvPlayerManager {//属性+方法//播放器player: media.AVPlayer | null = null//共享播放数据currentSong: GlobalMusic = AppStorageV2.connect(GlobalMusic, 'SONG_KEY', () => new GlobalMusic())!//定义一个方法 创捷播放器+监听播放器的状态async init() {if (!this.player) {this.player = await media.createAVPlayer()}//状态监听this.player.on('stateChange', (state) => {if (state === 'initialized') {//this.player?.prepare()} else if (state === 'prepared') {this.player?.play()this.currentSong.isPlay = true} else if (state === 'completed') {this.nextPlay(true) //自动下一首} else if (state === 'released') {this.currentSong.reset()}})}
}
  • 关键矛盾点
    ▸ 首次进入时状态从falsetrue触发动画过渡
    ▸ 二次进入时状态持续为true无状态变化信号→动画系统判定无需更新

  • 动画方向异常根源
    ▸暂停操作 → isPlay: true → false → rotate(360 → 0)

  • 组件差异表现解析

组件动画类型正常/异常原因
唱片封面无限循环动画异常▶ 依赖持续的状态变化信号
▶ 角度累计导致插值异常
唱针组件定点位移动画正常✅ 目标角度固定(如45度)
✅ 状态切换时总有角度差值
  • 系统级运行机制
    在这里插入图片描述
  • 核心原因
    ▸ animation属性动画仅响应组件属性值的变更而非当前值
    ▸ 持续相同的状态值会被判定为"无变化需要处理"

三、解决方案

要使animation动画正确触发,就需要组件的可动画属性值发生变化,在本文的案例中就是组件变换中的rotate属性。

  • 1、单独设置一个状态变量angel用来控制歌曲专辑图片的旋转角度,初始化为0。当isPlay的值发生改变时angel的值也需要发生改变。可以使用@MonitorisPlay添加监视,当其值发生变化时,根据isPlay当前值改变angel的值。
  • 2、但是这样完全同步改变还不行,这样依旧会出现animation读取isPlay的值早于isPlay值发生改变的情况(当歌曲准备就绪isPlaytrue,但是动画还未初始化完成,旋转角度直接设置成360),依旧会导致动画无法正确触发。还需要判断animation是否已经初始化完毕。
  • 3、我们可以添加一个判断变量isUIReady初始值为false,在包裹图片的外层Row组件的组件生命周期函数onAppear()中对isUIReady设值为true,用于判断动画是否初始化完成。
  • 4、回到1中的@Monitor监视器,在对angle的值根据isPlay的改变进行对应改变前,需要增加一层判断逻辑:判断animation是否以及初始化,即isUIReady是否为true,当isUIReadytrue才可以对angel值进行更改。这个判断主要是应对刚进页面时防止animation初始化时根据已经变化后的isPlay值将旋转角度设置为了360
  • 简化后的示例代码如下:
// 代码已简化,完整代码见文章末尾gitee仓库链接
@Entry
@Component
struct Play {@Local playState: GlobalMusic = AppStorageV2.connect(GlobalMusic, 'SONG_KEY', () => new GlobalMusic())!;// 动画是否就绪@Local isUIReady: boolean = false;// 歌曲专辑图片旋转角度,初始化为0@Local angel: number = 0;// 监控playState的成员isPlay的值变化,应对播控中心同步及播放/暂停控制@Monitor('playState.isPlay')isPlayChange(monitor: IMonitor) {// 需要动画已经初始化好if (this.isUIReady) {this.angel = this.playState.isPlay ? 360 : 0;}}build() {Column() {// 专辑封面容器Row() {Image(this.playState.img).rotate({angle: this.angel,centerX: '50%',centerY: '50%'}).animation({duration: 4000,  // 动画时长4秒iterations: -1, // 无限循环curve: Curve.Linear}).width('70%').borderRadius(400)}.backgroundImage($r('app.media.ic_cd')).backgroundImageSize(ImageSize.Cover).onAppear(() => {this.isUIReady = true;this.angel = this.playState.isPlay ? 360 : 0;}).onDisAppear(()=>{this.isUIReady=falsethis.angel = 0})}}
}

四、总结与最佳实践

(一)核心问题归纳

问题维度技术本质典型表现
动画触发机制依赖属性值变化量而非当前值▶ 首次进入正常
▶ 二次进入失效
状态同步机制AppStorageV2数据更新时点控制▶ 异步资源加载时序冲突
▶ 组件生命周期钩子触发顺序
动画方向异常插值计算逆向路径▶ 360°→0°逆时针旋转
▶ 动画系统默认最短路径

(二)最佳实践方案

  1. 状态管理分离原则
// 状态分层架构
┌──────────────┐       ┌──────────────┐
│ 业务逻辑状态 │──────▶│ 动画驱动状态 │
  (isPlay)   (angle)    │
└──────────────┘       └──────────────┘
  • 实现方式:

    @Monitor('playState.isPlay')
    syncAnimationState() {if (this.isUIReady) {this.angle = this.playState.isPlay ? 360 : 0}
    }
    
  1. 动画初始化保障机制
Component Lifecycle:onAppear() → 设置isUIReady=true → 触发首次状态同步onDisappear() → 重置isUIReady=false
  1. 动画参数优化配置
// 防抖动配置
.animation({duration: 4000,iterations: -1,curve: Curve.EaseOut,delay: 50 // 增加启动延迟
})

(三)开发建议

这个案例体现了鸿蒙动画系统的两个重要特性:

  1. 动画触发严格依赖状态变化过程而非当前状态值
  2. 角度动画会自动计算最优路径,可能产生方向偏差

开发者需要建立「状态变更驱动动画」的思维模型,而非「状态值决定动画」的静态认知

  1. 状态监控三原则
    ✅ 监控粒度细化到具体属性
    ✅ 添加防抖/节流控制
    ✅ 结合生命周期管理

  2. 动画调试技巧

    // 调试标记
    .onClick(() => {console.debug(`[AnimationState] isPlay:${this.isPlay}, angle:${this.angle}`)
    })
    
  3. 性能优化方向

    优化策略实施方法收益点
    动画缓存预加载关键帧减少首帧延迟
    分层渲染分离静态/动态元素降低GPU负载
    精度控制使用整数角度值减少插值计算量

问题版本完整代码可在代码仓库的Issues中查看

完整解决方案源码:Gitee仓库链接
更多鸿蒙开发技巧:鸿蒙专栏目录–持续更新中

文章内容如果有误欢迎指正

相关文章:

  • C语言_动态内存管理
  • vue引用cesium,解决“Not allowed to load local resource”报错
  • ProfibusDP主站转modbusTCP网关与ABB电机保护器数据交互
  • 典籍知识问答模块AI问答bug修改
  • 信息与信息化
  • LeetCode 第 45 题“跳跃游戏 II”
  • BBR 的 buffer 动力学观感
  • 【ant design】ant-design-vue 4.0实现主题色切换
  • C语言中字符串函数的详细讲解
  • 【软考 程序流程图的测试方法】McCabe度量法计算环路复杂度
  • 无线信道的噪声与干扰
  • 恢复因 oh-my-zsh 安装导致丢失的 zsh 环境变量
  • Python自学笔记3 常见运算符
  • C语言:在 Win 10 上,gcc 如何编译 gtk 应用程序
  • 【VSCode】快捷键合集(持续更新~)
  • python3GUI--多功能WiFi网络工具箱 By:PyQt5(详细分享)
  • 如何根据竞价数据判断竞价强度,是否抢筹等
  • LLM-Based Agent综述及其框架学习(五)
  • FreeCAD源码分析: Transaction实现原理
  • 安全性(一):加密算法总结
  • 回家了!子弹库帛书二、三卷将于7月首次面向公众展出
  • 著名心血管病学专家李国庆教授逝世,享年63岁
  • 特朗普再提“接管”加沙,要将其变为“自由区”
  • 科普|男性这个器官晚到岗,可能影响生育能力
  • 长三角首次,在铁三赛事中感受竞技与生态的共鸣
  • 南方降水频繁暴雨连连,北方高温再起或现40°C酷热天气