ios android hos iOS安卓鸿蒙物理动画弹簧动画发展综述

0 阅读38分钟

各系统动画支持情况

概览

image.png

问题1:OS交互动画发展历程

视图/熟悉动画 --> 物理动画(拟真动力学) --> 弹簧动画(感知流畅) --> 智能共生 --> 光影与高级材质。

技术难度:

从简单曲线计算,到复杂物理物理模拟;

从简单局部特效,到复杂全局高关联度特效;

从简单的几何姿态变化,到复杂的光影材质变化;

从单一固态变化,到多视图液态融合,气态消散等复杂变化。

从简单的算力消耗,到极致压榨更多算力。

问题2:为什么要从物理动画转到Spring动画?

可以从这三个方面看转Spring动画的好处:

更精准:Spring 能确保 UI 100% 停在设计好的位置,而纯物理模拟难以控制终点。

更高效:Spring 的数学模型简单,性能开销远小于全套物理引擎。

更聚焦:UI 交互的核心需求是“跟手”和“回弹”,而不是模拟重力和碰撞,Spring 完美覆盖了这核心需求。

物理动画劣势:

不可控(黑盒状态): 物理模拟是“跑一步看一步”,系统无法预知动画确切的结束时间和总时长。这导致开发者很难精确安排后续的 UI 逻辑(比如“动画结束后立刻跳转页面”),只能依赖不稳定的监听器。

高消耗(算力陷阱): 为了保证物理准确性,积分算法(如 RK4)每帧需要进行多次迭代计算。一旦设备出现掉帧,物理引擎为了追赶时间,会在下一帧进行更多次的循环计算(补帧),导致“越卡越算,越算越卡”的恶性循环。

不稳定(累积误差): 数值积分依赖浮点数运算的累加,长时间运行或在临界值附近容易产生数学误差。这会导致 UI 元素在理应静止(归零)的时候,出现肉眼可见的微小抖动(Jitter),或者无法精确停在像素点上。

问题3:有了Spring动画还要物理动画嘛?

Spring动画本身也是物理动画,采用解析解计算方式。物理动画(拟真动力学)在iOS里对应UIDynamic物理库(包含snap attachment gravity collision field等功能),采用数值积分计算方式。

iOS 官方组件的动画演进始于iOS7(2013),当时引入UIDynamic是为了通过物理模拟(如吸附和连接)赋予界面真实质感;

然而,在iOS10(2016)发生转折,随着UIViewPropertyAnimator的推出,iOS采用更具可控性和可中断性的 Spring(弹簧)模型。

这一趋势在iOS12(2018)的"Fluid Interfaces" 设计哲学中被确立为标准,强调如流水般可中断响应,从而实现自然的交互手感。

到了SwiftUI  iOS13+(WWDC 2023 Animate with springs)时代,基于 Spring 的动画已成为构建 UI 的绝对默认机制,UIDynamicAnimator 彻底退出了常规交互组件的舞台。

从理论上UIDynamic物理动画的gravity collision field功能,无法用Spring替代,但内置组件、系统内部交互,均无相关应用场景。安卓、鸿蒙也无相关功能。

然而,在iOS10(2016)发生转折,随着UIViewPropertyAnimator的推出,iOS采用更具可控性和可中断性的 Spring(弹簧)模型。

官方视频:nonstrict.eu/wwdcindex/w…

官方演讲词:devstreaming-cdn.apple.com/videos/wwdc…

"So springs are the only type of animation that maintains continuity both for static cases and cases with an initial velocity."

  • 解读:Spring 是唯一一种在静态情况和具有初始速度的情况下都能保持连续性的动画类型。

"When that happens, a spring animation uses the velocity it had when it was retargeted as the initial velocity towards its new destination and this same velocity preservation makes these kind of interruptions feel smooth and natural."

  • 解读:当动画重定向时,Spring 动画会使用它被重定向时的速度作为通往新目的地的初始速度,这种速度保持使得中断感觉平滑自然。

这一趋势在iOS12(2018)的"Fluid Interfaces" 设计哲学中被确立为标准,强调如流水般可中断响应,从而实现自然的交互手感。

Fluid Interfaces YouTube视频

官方演讲词 devstreaming-cdn.apple.com/videos/wwdc…

"And, what's interesting about a spring is, it does this seamlessly. This seamlessness is, kind of, built in to the behavior. And, this is what makes them such versatile tools for doing fluid interface design. Is that you, kind of, get this stuff for free. It's baked in to the behavior itself."

  • 解读:虽然没有绝对地说“唯一方法”,但这段话强调了弹簧是实现无缝、流体运动的核心工具(versatile tools),并且这种特性是弹簧独有且内****置(built in/baked in)的,暗示了在实现这种自然手感时,弹簧是不可替代的首选方案。

"So, mass stiffness and damping will remain behind the scenes, they're the fundamental properties of the spring system that we're using."

  • 解读:这句话明确指出,苹果内部使用的系统就是“Spring System”(弹簧系统),这些物理属性是“Fundamental”(基础/根本)的。

"You can think of the scrolling content as a ball attached to a spring. On one end of the spring is the current value... And, the other end of the spring is where the content wants to go because of its elasticity."

  • 解读:滚动是触摸屏最基础的交互。如果连滚动都被视为“球连着弹簧”,那么弹簧模型就是整个系统交互手感的基石。

到了SwiftUI  iOS13+(WWDC 2023 Animate with springs)时代,基于 Spring 的动画已成为构建 UI 的绝对默认机制,UIDynamicAnimator 彻底退出了常规交互组件的舞台。

视频和官方演讲词 developer.apple.com/videos/play… "Because springs are such a great tool for animations, we now use them as the default animation in SwiftUI, so all you need to do is call withAnimation to start with a spring."

  • 解读:演讲稿明确指出,由于 Spring 动画的优势,它已被确立为 SwiftUI 的默认动画。 

"And we're adopting these universally across Apple's design and engineering efforts. So all of our frameworks that support springs will use them."

  • 解读:这种配置方式正在被 Apple 的设计和工程部门普遍采用

"You can also use these same parameters to create spring animations in UIKit and Core Animation."

  • 解读:新的 Spring 参数(Duration 和 Bounce)同样适用于 UIKit 和 Core Animation

问题4:为什么“弹簧动画(感知流畅)”是重要里程碑

在流畅性,用户、开发者、动效设计师感知方面,是最出色的,普遍应用到OS各层级交互场景。

统一了设计与开发的语言: 弹簧动画动画模型提炼出Duration(持续时长)+Bounce(弹性)两个参数,使动效设计师和开发人员无缝对接,形成统一认知。

从“视觉流畅”到“感知流畅”的跨越: 在弹簧动画普及之前,业界追求的是视觉流畅,即“不掉帧”、“满60fps”。弹簧动画模拟了质量和阻尼,让用户感觉屏幕里的 UI 元素是有重量的实体,而不是发光的像素。这种“感知流畅”比单纯的

“视觉流畅”更高级,大大降低了大脑处理信息的负担。

如流水般可中断: 之前动画一旦开始就必须播完,中途打断会产生生硬的跳变;弹簧动画能够继承能量(速度),叠加多个弹簧动画,即使频繁变化目标值,轨迹依然光滑。

概览综合对比

系统视图 & 属性
(基础构建)
拟真物理动画
(拟真动力学)
弹簧动画
(感知流畅)
智能共生
(AI + 生命感)
光影与高级材质
(沉浸美学)
阶段六:多端智能
(未来)
iOS2007-2012
理念: 时间驱动与缓动。
模拟惯性,建立平滑标准。
交互: 发射后不管。
单向输出,一旦开始用户难以干预。
2013-2015
理念: 物理模拟与重力。
界面是有物理属性的实体。
交互: 交互式转场。
手指动多少界面动多少,建立空间感。
2016-2023
理念: 感官物理 (Fluid)。
如水流般可中断,符合直觉。
交互: 可中断与重定向。
目标频繁变化时轨迹依然圆润。
2024
理念: 有机光影与生成式流体。
让 AI 可见,实时生成反馈。
交互: 意图导向。
像素级控制,实时显现,有生命感。
2025
理念: 液态玻璃。
弯曲光线的数字超材料。
交互: 空间流转。
透镜效应,内部辉光,凝胶回弹。
?
Android2008-2012
理念: 视觉欺骗。
从“看起来动”到“属性改变”。
交互: 像素移动。
早期交互与视觉分离,略显生硬。
2013-2020
理念: 空间连贯 (Material)。
强调纸张物理特性与空间关系。
交互: 引导视觉焦点。
共享元素、涟漪效果,具有重量感。
2021-2023
理念: 状态驱动 (Compose)。
完美融入协程,UI随状态自动变化。
交互: 响应式状态。
解决打断跳变,无需手动管理过程。
2024
理念: 直觉与生命感。
AI具象化,无负担交互。
交互: 并行逻辑。
全量可打断,意图驱动的视觉反馈。
2025
理念: 智慧光感。
光影秩序、生命力自然光。
交互: 流体变形。
Q弹手感、水滴融合、光场动效。
?
HarmonyOS2019-2020
理念: 功能优先。
补齐基础位移与淡入淡出能力。
交互: 机械式过渡。
线性缓动,无手势跟随,不可打断。
2021-2022
理念: 声明式物理。
状态驱动,模拟基础阻尼感。
交互: 跟手性弹簧。
分段式线性跟随,组件无缝流转。
2023
理念: 流畅与连贯。
注重用户感知的流畅度。
交互: 极致跟手。
全程带阻尼,随时打断,引入粒子。
2024
理念: 和谐美学与重力。
基于引力/磁力,确保符合直觉。
交互: 全场景可打断。
毫秒级响应反向操作,灵动跟手。
2025
理念: 气态动效与情感。
引入光语言,模拟环境光线。
交互: 灵动粒子。
粒子轨迹绑定指尖,场景化光感反馈。
?

iOS一直处于领跑地位(时间和设计理念),安卓和鸿蒙处于跟随状态。

共同点是:连续、流畅、可打断、如流水般可中断、retarget、感知流畅、符合直觉、AI意图导向、生命感、光影和高级材质。

不同点:iOS先行和领跑,安卓及其阵营(如vivo oppo 小米等)、鸿蒙不同维度追赶。

以下结合动态视频逐阶段对比。

视图/属性动画对比

视图/属性动画相对成熟和普遍,差异性不大,这里不讨论。

物理动画对比

iOS UIKit Dynamic物理框架,是参考box2d的物理引擎,完全模拟物理摩擦力、弹力、阻力、重力、力场等真实物理条件,进而驱动UI运动。

其优点是:符合物理规律,看起来更真实;缺点是计算复杂,不适合常规UI交互,由于是积分计算,过程也不容易控制。

目前iOS主要采用Spring控制交互动画,远离UIKit Dynamic。

对于安卓、安卓阵营(vivo oppo等)、鸿蒙等仅支持Spring弹簧动画,官方不支持类似iOS的UIKit Dynamic物理动画。

虽然鸿蒙有类似Box2D的碰撞锁屏,但仅作为PR宣传的局部feature,不是框架级动画引擎。

iOS(UIKit Dynamics纯物理模拟)
物理动画  重力 ios-gravity.gif 弹性 ios-elasticity.gif attachment & snap ios-snap-attachment-animation.gif

弹簧动画对比

iOS、安卓、安卓阵营(vivo oppo等)、鸿蒙等弹簧计算均采用“解析解”(后续详细介绍),但在设计理念、参数定义、底层实现、开发者使用和视觉手感上仍有显著差异。

共同点:回弹、速度继承、可打断、全流程可打断、additive、无缝丝滑。 不同点:

iOS:动画参数体现动效设计、研发、用户统一语言:Duration + Bounce,通过动画时长和弹性来描述一个动画,非常符合直觉。缺点是根据Duration + Bounce + 移动距离反推算出刚度,同一个View在不同target下,弹性效果不同,好像一个物体在不同情况有不同弹性表现,这是不符合物理常识的。

Android:纯粹物理,动画参数保持物理原始属性:刚度+阻尼,动画跑多久是物理属性决定的,不是研发指定。优点是同一个View,弹簧属性始终一致,缺点是动画时长不可控,设计、开发参数不直观、不统一。

鸿蒙:吸收iOS和安卓的经验,设计上强调”跟手性“,细化跟手动画ResponsiveSpringMotion和离手动画SpringMotion,提供Response(响应时间)+阻尼+Overlap(跟手动画重叠系数)参数,Response(响应时间)不是iOS那种直接决定动画duration,鸿蒙会兼顾物体移动距离不同,适当缩放duration,使弹簧更符合真实弹簧运动。

retargeting不同:iOS通过速度继承+弹簧参数插值方式实现平滑过渡;vivo通过速度继承+输入信号平滑+视觉模糊补偿;oppo通过速度继承+动态阻力(根据继承过来的速度动态调整);鸿蒙:通过速度继承+overlap重叠系数,对轨迹进行融合。

  • spring曲线对比

iOS:iOS26.2 iPhone13

Android: Jetpack Compose  1.7.0 (Android15) Vivo X Note

iOS弹簧参数:             Spring(mass: 1.0, stiffness: 157.9, damping: 17.6)

等效的安卓弹簧参数:Spring(dampingRation = 0.7f, stiffness = 157.9)

dampingRation = damping / (2 * sqrt(stiffness * mass)) = 17.6 / (2 * sqrt(157.9 * 1.0)) ≈ 0.70

android spring动画 (x: 0 --> 600px)

android-spring-normal.gif

ios spring动画 (x: 0 --> 600px)

ios-spring-normal.gif

ios vs android spring动画曲线对比 (x: 0 --> 600px)

Android 的弹簧似乎更“急”,冲得更高,到达峰值的时间略快。 iOS 的弹簧略微柔和,过冲稍微小一点点,到达峰值的时间稍晚,符合 iOS 追求的“优雅”和“平滑”。

image.png

  • 跟手/离手曲线对比

Android跟手性稍微弱点,跟手/离手切换斜率较大 ,实测下来,离手后的距离估算非常不稳定,手感极差。这条曲线是经过mock滑动距离得出的。

iOS跟手性,滑动距离预测非常稳定,在离手速度预测和滑动距离预测方面,跟服务交互直觉,表现非常稳定。

iOS与Android跟手-离手参数分别保持一致:

iOS跟手弹簧参数:       interactiveSpring(response: 0.15, dampingFraction: 0.86)

iOS离手弹簧参数:       spring(response: 0.35, dampingFraction: 0.55)

Android跟手弹簧参数:spring(stiffness = 1755f, dampingRatio = 0.86f)

Android离手弹簧参数:spring(stiffness = 322f, dampingRatio = 0.55f)

android drag-fling spring动画 android-drag-fling.gif ios drag-fling spring动画 ios-drag-fling.gif ios vs android drag-fling spring动画(为了对比,android惯性距离是mock ios的)

image.png

  • android实际跟手离手动画曲线

android跟手-离手切换,不稳定,预测滑动距离很容易不符合预期。

android-drag-fling-more.gif

image.png

  • ios实际跟手离手动画曲线

iOS跟手-离手切换非常稳定,预测距离计算符合直觉

ios-drag-fling-more.gif

image.png

  • 综合对比
iOSvivooppo鸿蒙
弹簧动画spring曲线 ios-springs-curves.gif
速度继承 &  retarget ios-spring-retarget.gif
vivo-origin6-spring.gifoppo-light0.gifhos-spring-curves.gif
  • 智能共生动画对比

从动效看,各家实现的都挺好,从流畅度来说,iOS更胜一筹。

具体原因:缺少更进一步分析。

iOSvivooppo鸿蒙
智能共生ios-ai-tmp.giforiginos6-ai-glow.gifoppo-color15-ai-ask.gifhos-ai-temp.gif
  • 光影与高级材质

iOS断崖式领先,除鸿蒙有较全面追赶外,安卓阵营几乎停留在宣传话术上。

iOS的wwdc 2025的演讲里有大致介绍实现原理和过程,暂时缺少进一步研究和分析。

iOS鸿蒙
光影与高级材质liquid glass01.gifhos6-ps2.gif

ios动画发展概要

统一阶段视图 & 属性动画
(基础构建)
物理动画
(拟真动力学)
弹簧动画
(感知流畅)
智能共生光影与高级材质多端智能
(未来)
对应版本iOS 1-6
(2007-2012)
iOS 7-9
(2013-2015)
iOS 10-17
(2016-2023)
iOS 18
(2024)
iOS 26
(2025)
?
设计理念时间驱动与缓动
模拟惯性的过渡手段
物理模拟与重力
界面不再是平面的,而是有物理属性的实体。
感官物理与声明式弹簧
如水流般可中断,符合直觉,而非冷冰冰的物理参数。
有机光影与生成式流体
让 AI 可见,从预设反馈进化为实时生成。
液态玻璃:弯曲光线的数字超材料
界面是具有凝胶般柔韧性的有机体。
?
交互特点发射后不管
单向输出,用户难以干预
交互式转场
手指移动多少,界面动多少,建立空间感。
可中断与平滑重定向
目标值频繁变化时轨迹依然圆润
意图导向
实时显现,像素级控制,有生命感。
空间流转
透镜效应,内部辉光,凝胶回弹。
?
关键技术• Core Animation
• CATransaction
• CADisplayLink
• CALayer
• Implicit Animation
• ease-in/out
• UIKit Dynamics
• Spring Animation
• UIVisualEffectView (Blur)
• Damping / Velocity
• UIViewPropertyAnimator
•  SwiftUI Animation
Interpolating Spring
Duration + Bounce
• Phase/KeyframeAnimator
• TimelineView / Canvas
• Apple Intelligence Glow
• Mesh Gradients
• Metal Shaders
• Text Renderer
• Liquid Glass
• Lensing
• Intelligent Adaptive Layering
?
官方链接WWDC 2010: Core Animation in Practice
WWDC 2011: Core Animation Essentials
WWDC 2013: Getting Started with UIKit Dynamics
WWDC 2014: Advanced User Interfaces with Collection Views
WWDC 2014: Building Interruptible and Responsive Interactions
WWDC 2016: Advances in UIKit Animations and Transitions
WWDC 2018: Designing Fluid Interfaces (经典)
WWDC 2019: SwiftUI Essentials
WWDC 2021: Add rich graphics to your SwiftUI app
WWDC 2023: Explore SwiftUI animation
WWDC 2023: Animate with springs
WWDC 2024: Apple Intelligence
WWDC 2024: Create custom visual effects with SwiftUI
WWDC 2025: Meet Liquid Glass

android动画发展概要

统一阶段视图 & 属性动画
(基础构建)
物理动画
(拟真动力学)
弹簧动画
(感知流畅)
智能共生光影与高级材质多端智能
(未来)
对应版本Android 1.0 - 4.0
(2008 - 2012)
Android 5.0 - 11
(2013 - 2020)
Android 12 - 14
(Jetpack Compose)
(2021 - 2023)
Android 15
(AI+自然流畅 )
(2024)
Android16
(2025)
?
设计理念视觉欺骗 & 属性改变
从“看起来动”到“真实属性改变”
空间连贯 & 真实物理
引入 Material 强调空间关系
引入物理弹簧,解决打断与跳变
状态驱动与协程集成
通过状态驱动动画,完美融入 Kotlin 协程环境。
从“工具属性”转向“生命感”与“直觉”
AI具象化、无负担交互
智慧光感与沉浸美学
vivo: 光影秩序、流畅舒适
oppo: 生命力自然光与沉浸美学
?
交互特点像素移动
早期交互与视觉分离
引导视觉焦点:通过共享元素和涟漪,解释界面变化关系,打破页面物理隔离。
速度驱动与可中断:动画可被手势无缝接管,键盘与UI同步,非线性交互,具有重量感和弹性。
响应式状态
UI 随状态自动变化,无需手动管理繁琐的动画过程。
并行逻辑与意图感知
全量可打断、多任务并行、意图驱动的视觉反馈
vivo: 弹性反馈与流体变形(Q弹手感;水滴融合;一镜到底;AI意图表达)
oppo:高阶特效与生动沉浸(光场动效:粒子、辉光、涟漪、光边、渐变模糊;呼吸感和生命力)
?
关键技术• Tween / Frame Animation
• ValueAnimator
• ObjectAnimator
• ViewPropertyAnimator
Interpolators
vsync
• Transition Framework
• SpringAnimation
• FlingAnimation
• SharedElement Transition
• AnimatedVectorDrawable
• RenderThread
• animate*AsState
• updateTransition
• animateContentSize
• Kotlin Coroutines
• Compose Shared Element
• LookaheadScope
•vivo原子动效5.0(弹性、磨砂)
• oppo极光引擎(并行、多任务)
vivo:蓝河流畅引擎(虚拟显卡、帧级物理模拟)
oppo:极光引擎(光场动效、高阶粒子、动态光晕)
?
官方链接View Animation
Property Animation Overview
Animate layout changes
Vector Drawable
Physics-based motion
Animations in Composeoriginos5简介
vdc2024回放
coloros15
originos6官网
originos6简介
coloros16

hos动画发展概要

统一阶段视图 & 属性动画
(基础构建)
物理动画
(拟真动力学)
弹簧动画
(感知流畅)
智能共生光影与高级材质多端智能
(未来)
对应版本HM OS 1.0 - 2.0
(2019 - 2020)
HM OS 3.0
(2021 - 2022)
HM OS 4.0
(2023)
HM OS 5 (NEXT / 纯血鸿蒙)
(2024)
HM OS 6
(2025)
?
设计理念功能优先
补齐基础能力
声明式 & 物理真实
状态驱动,模拟阻尼
流畅与连贯
强调可打断性,平滑衔接
注重用户感知流畅度
和谐美学:自然物理与流畅秩序
重力感应系统: 基于真实世界物理规律(如弹簧曲线、阻尼感),确保动画符合用户直觉。
可打断动画: 动画在播放过程中可随时响应新的交互指令,消除操作迟滞感,强调“手随心动”。
空间布局: 强调层次感,通过阴影、渐变和缩放动画引导用户关注核心操作。
和谐美学:智慧光感与情感共鸣
沉浸光感视效: 引入光语言,模拟环境光线与交互的实时融合。例如,唤醒语音助手时,光效会随声波波动;点击屏幕时会产生灵动粒子扩散。
气态动效: 界面切换从简单的物理平移升级为类似“气态”的流动与扩散,视觉效果更加通透、丝滑。
AI 深度融合: 利用 AIGC 能力实现“艺术签名锁屏”和“情绪主题”,让动效根据用户的心情或个性化输入动态生成,提升系统的个性化与温度感。
?
交互特点基础位移,传统过渡
简单的位移动画
固定时长的淡入淡出
线性或简单缓动曲线
机械式的页面切换
无手势更随能力
动画不可打断
缺乏物理感和真实感
跟手性弹簧
分段式线性跟随
组件与页面无缝流转
极致跟手,随时打断
全程带阻尼的物理跟随
随时打断(组件级)
解决突变
丝滑布局变化
粒子系统引入
速度继承
全场景可打断与极致跟手
1. 全手势可打断 (Fully Interruptible):
这是 HarmonyOS 5 最大的交互亮点。无论是应用启动、退出,还是复杂的页面转场,用户可以在动画播放的任意时刻通过手势打断它,并立即反向操作,系统毫秒级响应,无迟滞、无掉帧。

2. 灵动跟手 (Elastic Follow):
手指移动多少,界面就动多少。操作具有类似橡胶或弹簧的“牵引感”,松手后界面会根据惯性自然回弹或归位,给予用户极强的掌控信心。

3. 沉浸式模糊 (Immersive Blur):
在下拉控制中心、多任务后台等场景,采用实时的、高斯模糊处理,将背景内容“推”向后方,让前景信息更聚焦,同时保持视觉上的连贯性。
智能预判:AI预测用户操作
灵动粒子与光感反馈
1. 交互灵动粒子 (Responsive Particles):
点击屏幕会产生类似水波纹或光晕扩散的粒子效果。粒子的数量、轨迹、光感反射随操作类型(点击/滑动/删除)智能调整,赋予交互极强的生命力。

2. 极致跟手与流态 (Elastic & Fluid):
在通知中心左滑或移动图标时,粒子轨迹与指尖坐标实时绑定,速度与加速度模拟物理惯性。滑动界面时,元素泛起微妙光晕,伴随流动的粒子动效,仿佛光线在指尖流淌。

3. 场景化光感反馈 (Contextual Light Feedback):
动效具备语义理解能力。修图调节滑块时,光效随力度变化;输密码时,按键迸发流动粒子。光效成为操作结果的直观物理映射。
?
关键技术View Animation
Property Animation
Ease Function
• Service Widgets
• Spring Curve
• animateTo(状态驱动)
• sharedTransition
• transition (组内转场)
• RenderService 优化
• Particle System
• Interruptible Animation
ResponsiveSpringMotion(滤波与响应)
Interpolating Spring(retarget, additive)
Real-time Blur
• Velocity Inheritance
• 统一渲染引擎Rosen
并行渲染管线 (Parallel Rendering)
全域物理(弹簧 引力 重力等)
• 自研轻量粒子渲染引擎
• 环境光自适应系统
• 透明材质渲染
实时光影渲染
粒子涟漪
气态消散
?
官方链接HarmonyOS Java UI 动画开发 (注: 早期文档)ArkUI 动画与转场指南ArkUI 渲染与图形HarmonyOS Design 动效规范
HarmonyOS5
•HarmonyOS6

flutter动画发展概要

统一阶段视图 & 属性动画
(基础构建)
物理动画
(拟真动力学)
弹簧动画
(感知流畅)
智能共生光影与高级材质多端智能
(未来)
对应版本Flutter 1.x
(2017 - 2019)
Flutter 2.0
(2019 - 2021)
Flutter 3.0 - 3.10
(2022 - 2023)
Flutter 3.19+
(AI 辅助)
(2024)
Flutter 4.x
(3D 融合)
(2025)
?
设计理念万物皆 Widget
显式控制,解决“能不能动”
声明式动画
隐式过渡,解决“好不好写”
预编译与零卡顿
Impeller 引擎,解决“流畅”
UI 游戏化
AI 辅助生成 Shader
空间化交互
打破 App 与游戏边界
?
交互特点Hero 飞跃
手动管理 Controller
Rive 交互
基于状态机的实时交互
丝滑滚动
动态主题,像素级特效
预测性回退
与系统手势深度融合
3D 混合排版
物理质感,受重力影响
?
关键技术• AnimationController
• Hero Widget
• Staggered Animations
• Flare
• Implicitly Animated Widgets
• animations 包
• Rive 2.0 Runtime
• Impeller 引擎
• Fragment Shaders(毛玻璃、水波纹、像素风溶解)
• Material 3 (动态色彩)
• AI 辅助生成代码
• Predictive Back
• Flutter GPU API
• Impeller Scene (3D)
• High-perf Graphics
?
官方链接• Flutter 动画教程 (显式)• 隐式动画指南• Impeller 引擎架构• Flutter GPU & Shaders-

弹簧技术方案对比

弹簧模型简介

核心公式:牛顿第二定律的变体

弹簧模型的本质是一个二阶微分方程。在计算机里,我们主要计算两个力:

拉力 (Spring Force):由胡克定律决定。你离目标越远,拉力越大。

F弹 = −k ⋅ x (k 是刚度,x 是距离)

阻力 (Damping Force):模拟空气摩擦或粘滞力。你速度越快,阻力越大。

F阻 = −c ⋅ v  (c 是阻尼系数,v 是速度)

最终公式(合力 = 质量 × 加速度):

F合 = F弹  + F阻  = m ⋅ a = −k ⋅ x − c ⋅ v

齐次方程为:

m ⋅ a  + c ⋅ v + k ⋅ x = 0

 三个关键参数(你可以调节的“旋钮”)

在开发动画时(无论是 iOS、Android 还是 Web),通常调整这三个参数:

刚度 (Stiffness / Tension)

  • 含义:弹簧有多“硬”。
  • 效果:刚度越高,拉力越大,物体飞向目标的速度越快,回弹力度也越猛。

阻尼 (Damping / Friction)

  • 含义:环境有多“粘稠”。

  • 效果:阻尼主要用来消耗能量。

    • 阻尼小 = 像在真空中,晃很久才停。
    • 阻尼大 = 像在蜂蜜中,移动缓慢且不回弹。

质量 (Mass)

  • 含义:物体有多“重”。
  • 效果:质量越大,惯性越大。启动慢,停下来也慢(很难刹车)。
  • 注:在很多简化的 UI 库(如 iOS)中,质量通常默认设为 1,只需要调刚度和阻尼。

三种运动状态(视觉效果)

根据阻尼和刚度的比例关系,弹簧动画会呈现三种截然不同的形态:

欠阻尼 (Under-damped) —— “Q弹”
  • 现象:阻尼较小。物体冲过头(Overshoot),然后像果冻一样来回摆动几次才停下。
  • 场景:适合表现活泼、趣味的界面,如点赞按钮、游戏道具。
过阻尼 (Over-damped) —— “缓慢”
  • 现象:阻尼过大。物体还没到终点,能量就被耗光了,只能慢慢蹭到终点,绝对不会冲过头。
  • 场景:适合表现沉重、稳重的物体,或者为了避免视觉干扰(比如菜单慢慢淡出)。
临界阻尼 (Critically Damped) —— “完美丝滑”
  • 现象:阻尼刚刚好。物体以最快的时间到达终点,且刚好不冲过头
  • 场景:这是 UI 交互的黄金标准。iOS 和 Android 的大部分系统动画(如窗口打开、页面切换)都追求临界阻尼,因为它既快又稳,不会让用户感到晕眩。

弹簧模型两种解法

解析解

以iOS Android 鸿蒙 respring(开源库)为代表,使用的是解析解,核心步骤为:

(1)弹簧齐次方程为:

m ⋅ a  + c ⋅ v + k ⋅ x = 0

(2)变成微分方程

速度 v 是位移 x 的一阶导数(记作 x′)。

加速度 a 是位移 x的二阶导数(记作 x′′)。

由此得微分方程:

m ⋅ x''  + c ⋅ x' + k ⋅ x = 0

(3)求解微分方程

因为 et(指数函数)求导后还是它自己,因此设 x = ert。

把 x = ert  代入方程:

  • x   = ert 
  • x′  = rert 
  • x′′ = r2 ert 

代入原方程:

m(r2ert) + c(rert) + k(ert) = 0

把 ert 约掉(因为它不等于0),剩下的就是一元二次方程(特征方程):

mr2 + cr + k = 0

求这个方程的根 :

r = (-c ± sqrt(c2 - 4mk))  / 2m

(4)求根三种情况(对应三种动画公式)

根据根号里的数字(判别式 ΔΔ)是正数、零、还是负数,结果分成了三条路:

情况 1:根号里是正数(过阻尼 Over-damped)

  • 条件:阻尼 c 很大,大到 c2 > 4mk。

  • 数学结果:方程有两个不同的实数根 r1, r2​。

  • 通解公式

    x(t) = C1er1t + C2er2t

  • 通俗解释:全是指数函数。指数函数就是一条平滑的曲线,没有波浪

  • 视觉效果:像推开一扇很重的防火门,门慢慢合上,绝对不会夹到手,也不会回弹。

情况 2:根号里等于 0(临界阻尼 Critically Damped)

  • 条件:阻尼 c 刚刚好,c2 = 4mk。

  • 数学结果:方程只有两个相同的实数根 r = −c/2m。

  • 通解公式

    x(t) = (C1 + C2t)ert

  • 通俗解释:还是指数衰减,但多乘了一个 t。这是数学上不产生震荡的最快回归方式。

  • 视觉效果:这是苹果最爱的效果。丝般顺滑,停得刚刚好,不墨迹也不晃荡。

情况 3:根号里是负数(欠阻尼 Under-damped)

  • 条件:阻尼 c 很小,c2 < 4mk。

  • 数学结果:根号里是虚数!根变成了复数:r = α ± iω。

  • 欧拉公式登场:数学界最美的公式 eiθ = cos⁡θ + isin⁡θ 告诉我们,虚数指数就是正弦波

  • 通解公式

    x(t) = e−αt(C1cos⁡(ωt) + C2sin⁡(ωt))

  • 通俗解释

    • e−αt:负责让波浪越来越小(衰减)。
    • cos⁡/sin:负责制造波浪(震荡)。
  • 视觉效果:像松开手里的皮筋,它会“嗡嗡”震几下才停下来。

数值积分解

以facebook开源的rebound库为例,使用的是数值积分求解。

为了更精确地模拟加速度随位置和速度变化的过程,Rebound 使用了 四阶龙格-库塔法 (Runge-Kutta 4th Order, RK4)。

核心思想

RK4 不仅仅使用当前时刻的导数(速度和加速度)来推算下一时刻,而是在一个时间步长内进行4次采样,计算出加权平均的斜率,从而获得极高的精度。

RK4主要步骤:

开始 RK4 计算

--> 采样点 A: 初始状态 --> 计算 A 点的导数 -> 预测 B 点状态

--> 采样点 B: 中点 1      --> 计算 B 点的导数 -> 预测 C 点状态

--> 采样点 C: 中点 2      --> 计算 C 点的导数 -> 预测 D 点状态

--> 采样点 D: 终点

--> 加权平均 4 个导数

--> 更新最终位置和速度

通过这种方式,能在极小的计算开销下,模拟出非常平滑、符合物理直觉的弹簧动画。

解析解 vs 数值积分解

由于弹簧解析解计算精确、性能消耗低、预测能力强,主流OS普遍采用解析解计算方式。

对比维度解析解数值积分
核心算法数学公式直接代入时间 t 算出位置 x将时间切片,一步步迭代推演
库/平台iOS: UIView.animate(spring...)(swift)Android(2017年后): SpringAnimation (Jetpack)(java, kotlin)鸿蒙:springMotion/responsiveSpringMotion(c++)Respring (rust)Facebook: Rebound (Java/JS/ObjC)Android(2017年前)React Native: Animated (JS driver)Design: Origami Studio
计算方式O(1) 瞬间计算无论动画进行到第几秒,计算量恒定。直接输入时间 t,输出位置。O(n) 迭代计算必须依赖上一帧的状态。若要计算第 10 秒的状态,理论上需模拟前 9.9 秒的所有步骤。
性能消耗极低:适合大规模批量使用(如列表中的每个 Item 都带弹簧)。较高:每帧需要循环采样 4 次(RK4 特性),大量使用会占用 CPU。
预测能力强 (可预知未来): 可以轻松计算出动画何时结束、总位移是多少。弱:很难直接知道 2 秒后在哪里,除非先跑一遍模拟。不支持随机访问时间轴。
参数直观性高 (阻尼比/响应时间) 低(张力/摩擦力)

iOS-androidx-鸿蒙弹簧动画关键源码解读

总体结论

1 ios 是怎么划分弹簧动画api的:Animator,reverse, repeat, chain等

A:iOS弹簧动画api层面支持reverse和repeat,但不建议这么做;SwiftUI采用PhaseAnimator;UIKit采用CAKeyframeAnimation;鸿蒙支持reverse, repeat

2 用户改变手指速度时,底层有没有新建弹簧、或者多弹簧混合? retarget怎么实现的,blend如何实现的(是直接改变target,还是根据手指速度新建模型)

A:iOS理论上新替换旧,速度继承,duration blend; 鸿蒙新替旧,速度继承和response blend,不是resume_with_target();androidx新替旧,速度继承,无blend

3 统一属性能否跑多个动画(可以)

A:iOS和鸿蒙可以(additive字段决定覆盖、还是累加);androidx通过回调自己控制

4 速度继承的是手指速度,还是弹簧速度,两者如何使用

A:三者都是继承手指up时速度

iOS动画体系

iOS弹簧动画和属性动画统一管理,同一属性可以存在多个动画,可以叠加或覆盖(取决于additive变量)

弹簧动画api层面支持reverse和repeat,但不建议这么做(可以设置,但效果不可控):SwiftUI采用PhaseAnimator;UIKit采用CAKeyframeAnimation

android动画体系

结论

弹簧/属性动画都继承自FloatAnimationVec : AnimationSpec

retargeting:新动画替换旧动画,状态跟踪,停止当前动画,继承当前速度,开启新的弹簧动画;

跟手/离手切换:没有弹簧参数插值,只有速度继承

类图
流程图

Retargeting

Jetpack Compose 实现类似 iOS 的 Retargeting(重定向/动态目标变更)  且保持速度连续性(Velocity Continuity),主要依赖于 Animatable 类中的状态保持协程取消机制以及默认参数设计

核心逻辑在于:当一个动画被打断时,系统会捕获当前的瞬时速度,并将其作为新动画的初始速度(Initial Velocity)注入到新的物理模型(通常是 Spring/弹簧)中。

1. 自动捕获当前速度 (Default Argument)

在 Animatable 类的 animateTo 方法中,有一个非常关键的设计:initialVelocity 参数的默认值直接指向了当前的 velocity 属性。

kotlin // 来自 Animatable.kt public suspend fun animateTo( targetValue: T, animationSpec: AnimationSpec<T> = defaultSpringSpec, // 【关键点】:如果没有显式传入 initialVelocity,默认读取当前 Animatable 的实时速度 initialVelocity: T = velocity, block: (Animatable<T, V>.() -> Unit)? = null, ): AnimationResult<T, V> { // ... 创建动画对象并运行 return runAnimation(anim, initialVelocity, block) }

当连续调用 animateTo(newTarget) 时,如果未指定速度,Kotlin 会自动读取该对象当前的 velocity。这个 velocity 是上一帧动画计算出的瞬时速度。

2. 协程互斥与状态保留 (MutatorMutex)

Animatable 使用 MutatorMutex 来确保同一时间只有一个动画在运行。当新的 animateTo 被调用时,旧的动画会被取消。

// 来自 Animatable.kt private val mutatorMutex = MutatorMutex() // ... public suspend fun animateTo(...) { // ... return runAnimation(anim, initialVelocity, block) } private suspend fun runAnimation(...) { // 1. 记录开始时间 val startTime = internalState.lastFrameTimeNanos // 2. 使用互斥锁。如果已有动画在运行,mutate 会取消前一个动画的 Job (抛出 CancellationException) return mutatorMutex.mutate { try { // 3. 将新动画的初始速度设置为传入的 initialVelocity (即上一个动画的结束速度) internalState.velocityVector = typeConverter.convertToVector(initialVelocity) targetValue = animation.targetValue isRunning = true // ... 开始新的动画循环 } // ... } }

关键逻辑:

  1. 打断: mutatorMutex.mutate 会取消前一个正在运行的协程。
  2. 状态驻留:  虽然前一个协程被取消了,但 internalState(存储了 value 和 velocityVector)是保留在 Animatable 实例中的。它停留在了被取消那一刻的数值上。
  3. 无缝衔接:  新的协程启动时,使用步骤1中捕获的 initialVelocity 更新 internalState.velocityVector,从而继承了动量。
3. 物理引擎的计算 (SpringSimulation)

Retargeting 能平滑过渡的数学基础在于弹簧物理(Spring Physics) 。不同于贝塞尔曲线通常需要从 0 速度开始,弹簧公式天然支持非零的初始速度。

在 SpringSimulation.kt 中,updateValues 方法利用 lastVelocity(初始速度)来计算新的运动轨迹:

// 来自 SpringSimulation.kt``internal fun updateValues(``    ``lastDisplacement: Float,``    ``lastVelocity: Float, ``// 这里接收继承过来的速度``    ``timeElapsed: Long,``): Motion {``    ``// ...``    ``// 根据阻尼比(dampingRatio)和初始速度计算新的位移和速度``    ``if (dampingRatio > 1) {``        ``// 过阻尼计算,包含 lastVelocity``        ``val coeffB = (gammaMinus * adjustedDisplacement - lastVelocity) / (gammaMinus - gammaPlus)``        ``// ...``    ``} ``else {``        ``// 欠阻尼计算,包含 lastVelocity``        ``val sinCoeff = ((1 / dampedFreq) * (((-r * adjustedDisplacement) + lastVelocity)))``        ``// ...``    ``}``    ``// ...``}

鸿蒙动画体系

类图

RenderService侧负责动画执行,弹簧动画和属性动画统一管理。

后面章节展开讲解。

动画是否叠加

当前动画覆盖,还是叠加?

常规属性动画:

class RSB_EXPORT RSRenderPropertyAnimation : ``public RSRenderAnimation {`` ``bool isAdditive_ { ``true };``}

additive为true时,增量累加,否则直接set

gitee.com/openharmony… 118行

void RSRenderPropertyAnimation::SetAnimationValue(``const std::shared_ptr<RSRenderPropertyBase>& value)``{``    ``// ...``    ``std::shared_ptr<RSRenderPropertyBase> animationValue;``    ``// 如果开启了 Additive(累加模式)``    ``if (GetAdditive() && (property_ != nullptr)) {``        ``// 新值 = 当前属性值 + (当前帧计算值 - 上一帧计算值)``        ``// 这是一种增量更新方式,允许多个动画产生的增量叠加到同一个属性上``        ``animationValue = property_->Clone() + (value - lastValue_);``        ``lastValue_ = value->Clone();``    ``} ``else {``        ``// 否则直接覆盖``        ``animationValue = value->Clone();``        ``lastValue_ = value->Clone();``    ``}``    ``SetPropertyValue(animationValue);``}

RSRenderSpringAnimation(RSRenderInterpolatingSpringAnimation类似):

void RSRenderSpringAnimation::OnAnimate(``float fraction)``{``  ``springValueEstimator_->UpdateAnimationValue(prevMappedTime_, GetAdditive());``}  class RSSpringValueEstimator : ``public RSSpringValueEstimatorBase {``public``:``    ``void UpdateAnimationValue(``const float time``, ``const bool isAdditive) override``    ``{``        ``auto animationValue = GetAnimationValue(``time``, isAdditive);``        ``if (property_ != nullptr) {``            ``property_->Set(animationValue);``        ``}``    ``}      ``T GetAnimationValue(``const float time``, ``const bool isAdditive)``    ``{``        ``T currentValue = startValue_;``        ``constexpr ``static float TIME_THRESHOLD = 1e-3f;``        ``if (ROSEN_EQ(``time``, duration_, TIME_THRESHOLD)) {``            ``currentValue = endValue_;``        ``} ``else if (springModel_) {``            ``currentValue = springModel_->CalculateDisplacement(``time``) + endValue_;``        ``}          ``auto animationValue = currentValue;``        ``if (isAdditive && property_) {``            ``animationValue = property_->Get() + currentValue - lastValue_;``        ``}``        ``lastValue_ = currentValue;``        ``return animationValue;``    ``}``}

鸿蒙常规属性动画是否支持同一属性同时存在多个属性动画

支持

gitee.com/openharmony… 57行

void RSAnimationManager::AddAnimation(``const std::shared_ptr<RSRenderAnimation>& animation)``{``    ``AnimationId key = animation->GetAnimationId();``    ``// using AnimationId = uint64_t;``    ``if (animations_.find(key) != animations_.end()) {``        ``ROSEN_LOGE(``"RSAnimationManager::AddAnimation, The animation already exists when is added"``);``        ``return``;``    ``}``    ``auto it = std::find(pendingCancelAnimation_.begin(), pendingCancelAnimation_.end(), key);``    ``if (it != pendingCancelAnimation_.end()) {``        ``pendingCancelAnimation_.erase(it);``        ``ROSEN_LOGE(``"RSAnimationManager::AddAnimation, animation is a pendingCancelAnimation"``);``        ``return``;``    ``}``    ``animations_.emplace(key, animation); ``// key是uint64_t,可以不同``}

鸿蒙同一属性是否支持同时多个弹簧动画

不支持

gitee.com/openharmony… 255行

void RSAnimationManager::RegisterSpringAnimation(PropertyId propertyId, AnimationId animId)``{``    ``springAnimations_[propertyId] = animId; ``// 统一属性,直接覆盖``}

当一个新的弹簧动画在一个属性上启动时,它会接管旧动画的状态,并停止旧动画。

gitee.com/openharmony… 117行

void RSRenderSpringAnimation::OnAttach()``{``    ``// ...``    ``auto propertyId = GetPropertyId();``    ``// 1. 查询当前属性上是否已经有弹簧动画在跑``    ``auto prevAnimation = target->GetAnimationManager().QuerySpringAnimation(propertyId);``    ``// 2. 注册自己为当前属性的“正牌”弹簧动画(顶替掉旧的)``    ``target->GetAnimationManager().RegisterSpringAnimation(propertyId, GetAnimationId());``    ``// 3. 继承旧动画的状态``    ``InheritSpringAnimation(prevAnimation);``}  void RSRenderSpringAnimation::InheritSpringAnimation(...)``{``    ``// ...``    ``// 4. 关键一击:强制结束旧动画!``    ``prevSpringAnimation->FinishOnCurrentPosition();``}

连续跟手和Retargeting

如果上层代码(ArkUI/JS)在手势过程中每一帧都调用了 animateTo 且改变了目标值,那么客户端确实会每帧创建一个新的 RSSpringAnimation 并发送命令给 RS,RS 端也会相应创建新的 RSRenderSpringAnimation但是,RS 端有专门的机制(InheritSpringAnimation)来“继承”上一个动画的速度和状态,从而实现平滑过渡,而不是简单的销毁重建导致跳变。

第一步:动画创建 (Client Side)

当你在 ArkUI 中使用 responsiveSpringMotion 触发动画时,会创建一个客户端的 RSSpringAnimation 对象。查看 rs_spring_animation.cpp 中的 OnStart 方法:

// rs_spring_animation.cpp``void RSSpringAnimation::OnStart()``{``    ``RSPropertyAnimation::OnStart();``    ``// 1. 这里创建了 Render 端的动画描述对象 RSRenderSpringAnimation``    ``auto animation = std::make_shared<RSRenderSpringAnimation>(GetId(), GetPropertyId(),``        ``originValue_->GetRenderProperty(), startValue_->GetRenderProperty(), endValue_->GetRenderProperty());``    ``// ... 设置参数 ...``    ``// 2. 将这个 animation 对象通过 IPC 命令发送给 Render Service``    ``if (isCustom_) {``        ``// ...``    ``} ``else {``        ``StartRenderAnimation(animation); ``// 内部发送 RSAnimationCreateSpring 命令``    ``}``}
第二步:Render Service 接收与注册

gitee.com/openharmony… 117行

Render Service 接收到命令后,会将这个 RSRenderSpringAnimation 挂载到对应的 RSRenderNode 上。查看 rs_render_spring_animation.cpp 中的 OnAttach 方法:

// rs_render_spring_animation.cpp``void RSRenderSpringAnimation::OnAttach()``{``    ``auto target = GetTarget(); ``// 获取 RSRenderNode``    ``// ...``    ``auto propertyId = GetPropertyId();``    ``// 3. 查询是否已经有同属性的 Spring 动画在运行``    ``auto prevAnimation = target->GetAnimationManager().QuerySpringAnimation(propertyId);``    ``// 4. 注册当前动画到 Node 的管理器中``    ``target->GetAnimationManager().RegisterSpringAnimation(propertyId, GetAnimationId());``    ``// 5. 继承上一个动画的状态(这是 responsiveSpringMotion 顺滑的关键)``    ``InheritSpringAnimation(prevAnimation);``}  void RSRenderSpringAnimation::InheritSpringAnimation(``const std::shared_ptr<RSRenderAnimation>& prevAnimation)``{``    ``// ...``    ``// 6. 提取上一个动画的状态(主要是速度)``    ``auto prevSpringAnimation = std::static_pointer_cast<RSRenderSpringAnimation>(prevAnimation);      ``// 7. 继承状态(位置、速度)``    ``if (!InheritSpringStatus(prevSpringAnimation.get())) {``        ``blendDuration_ = 0;``        ``return``;``    ``}      ``// ... 混合逻辑 ...      ``// 8. 结束上一个动画,让当前新动画接管``    ``prevSpringAnimation->FinishOnCurrentPosition();``}
第三步:每帧执行 (Render Service Loop)

在动画运行期间,Render Service 的主循环会驱动 RSRenderNode 进行动画计算。

查看 rs_render_node.h 和 rs_render_spring_animation.cpp

  1. RSRenderNode::Animate 会被每帧调用。
  2. 它会调用 RSRenderSpringAnimation::OnAnimate(float fraction)
// rs_render_node.cpp``std::tuple<``bool``, ``bool``, ``bool``> RSRenderNode::Animate(int64_t timestamp)``{``  ``auto animateResult = animationManager_.Animate(timestamp);``}  // rs_animation_manager.cpp``std::tuple<``bool``, ``bool``, ``bool``> RSAnimationManager::Animate(``    ``int64_t ``time``, int64_t& minLeftDelayTime, ``bool nodeIsOnTheTree, RSSurfaceNodeAbilityState abilityState)``{``  ``EraseIf(animations_, [``this``, ...](auto& iter) -> ``bool {``    ``auto& animation = iter.second;``    ``bool isFinished = animation->Animate(``time``, minLeftDelayTime);``  ``}``}  // rs_render_animation.cpp``bool RSRenderAnimation::Animate(int64_t ``time``, int64_t& minLeftDelayTime)``{``  ``OnAnimate(fraction);``}  // rs_render_spring_animation.cpp``void RSRenderSpringAnimation::OnAnimate(``float fraction)``{``    ``// ...``    ``// 6. 每一帧只是更新估算器的时间和数值,没有创建新对象``    ``prevMappedTime_ = GetDuration() * fraction * MILLISECOND_TO_SECOND;``    ``springValueEstimator_->UpdateAnimationValue(prevMappedTime_, GetAdditive());      ``// ... 检查是否结束 ...``}

Regarting和blendDuration

Q: 鸿蒙的弹簧动画,在跟手、离手动画频繁切换时,blendDuration_大于0,会不会产生很多个动画叠加,鸿蒙是怎么处理的,详细分析

A: 在鸿蒙的这套实现中,不会产生多个动画叠加(Stacking) 。 无论 blendDuration 是否大于 0,鸿蒙采用的都是  “状态继承 + 销毁旧动画(Retargeting with Velocity Preservation)”  的机制。

以下是结合源代码的详细分析,解释鸿蒙是如何处理频繁跟手、离手切换的:

1. 核心机制:单例接管,而非叠加

在 iOS 的加法动画中,新旧动画是共存的。但在鸿蒙的这段代码中,旧动画会被强制停止

代码证据 ( OnAttach InheritSpringAnimation ):

gitee.com/openharmony… 117行 129行

void RSRenderSpringAnimation::OnAttach()``{``    ``// ...``    ``auto propertyId = GetPropertyId();``    ``// 1. 查询当前属性上是否已经有弹簧动画在跑``    ``auto prevAnimation = target->GetAnimationManager().QuerySpringAnimation(propertyId);``    ``// 2. 注册自己为当前属性的“正牌”弹簧动画(顶替掉旧的)``    ``target->GetAnimationManager().RegisterSpringAnimation(propertyId, GetAnimationId());``    ``// 3. 继承旧动画的状态``    ``InheritSpringAnimation(prevAnimation);``}  void RSRenderSpringAnimation::InheritSpringAnimation(...)``{``    ``// ...``    ``// 4. 关键一击:强制结束旧动画!``    ``prevSpringAnimation->FinishOnCurrentPosition();``}

分析:

当你频繁触发跟手(onChanged)时,每一次新的 RSRenderSpringAnimation 被 Attach 上去,它都会找到上一次的动画,继承它的遗产(速度、位置),然后直接调用 FinishOnCurrentPosition() 把上一次动画杀掉

因此,内存里永远只有一个活跃的弹簧在控制这个属性,不存在“多个弹簧动画叠加”的情况。

2. 状态继承:解决“跳变”的关键

既然旧动画被杀掉了,为什么画面不会闪烁?因为新动画继承了旧动画的“物理遗产”。

代码证据 ( InheritSpringStatus ):

bool RSRenderSpringAnimation::InheritSpringStatus(``const RSRenderSpringAnimation* from)``{``    ``// ...``    ``// 1. 获取旧动画当前的:位置(lastValue)、速度(velocity)``    ``auto [lastValue, endValue, velocity] = from->GetSpringStatus();      ``// 2. 将新动画的“起始位置”设为旧动画的“当前位置”``    ``startValue_ = lastValue->Clone();``    ``// 3. 将新动画的“初始速度”设为旧动画的“当前速度”``    ``initialVelocity_ = velocity;      ``// 4. 将这些参数注入到底层估算器中``    ``springValueEstimator_->UpdateStartValueAndLastValue(startValue_, lastValue_);``    ``springValueEstimator_->SetInitialVelocity(velocity);``    ``return true``;``}
3. blendDuration 作用?

在鸿蒙的实现里,blendDuration 不是用来混合位置轨迹的,而是用来混合弹簧参数(Response/Stiffness)的

代码证据 ( InheritSpringAnimation OnInitialize ):

// 在 InheritSpringAnimation 中``if (ROSEN_EQ(springValueEstimator_->GetResponse(), response_, RESPONSE_THRESHOLD)) {``    ``// 如果新旧动画的 response(刚度)一样,直接把 blendDuration 置为 0``    ``blendDuration_ = 0;``}  // 在 OnInitialize 中``if (blendDuration_) {``    ``// ...``    ``// 线性插值计算当前的 response``    ``auto blendRatio = ``static_cast``<``float``>(blendTime) / ``static_cast``<``float``>(blendDuration_);``    ``auto response = springValueEstimator_->GetResponse();``    ``// 动态调整 response:从旧的 response 逐渐过渡到新的 response_``    ``response += (response_ - response) * blendRatio;``    ``springValueEstimator_->SetResponse(response);``    ``// ...``}

详细解释:

  • 如果在 onChanged 里一直使用相同的 response(比如 0.5),那么 blendDuration会被代码直接忽略(置为 0)
  • 只有当你从一个 response: 0.5 的动画切换到一个 response: 0.1 的动画时,blendDuration 才会生效。它会让弹簧的“软硬度”在指定时间内平滑变化,而不是让位置轨迹混合。

responsiveSpringMotion == springMoition

两者在源码层面相同,都是ResponsiveSpringMotion,RSClient为RSSpringAnimation,RSService为RSRenderSpringAnimation

gitee.com/openharmony… 98行 143行

std::string GetSpringMotionCurveString(ani_env *env,``    ``ani_object response, ani_object dampingFraction, ani_object overlapDuration)``{``    ``float responseValue = OHOS::Ace::ResponsiveSpringMotion::DEFAULT_SPRING_MOTION_RESPONSE;``    ``float dampingFractionValue = OHOS::Ace::ResponsiveSpringMotion::DEFAULT_SPRING_MOTION_DAMPING_RATIO;``    ``float overlapDurationValue = OHOS::Ace::ResponsiveSpringMotion::DEFAULT_SPRING_MOTION_BLEND_DURATION;``         ``auto curve = OHOS::Ace::AceType::MakeRefPtr<OHOS::Ace::ResponsiveSpringMotion>(``        ``responseValue, dampingFractionValue, overlapDurationValue);``    ``return curve->ToString();``}

gitee.com/openharmony… 312行 357行

std::string GetSpringResponsiveMotionCurveString(ani_env *env,``    ``ani_object response, ani_object dampingFraction, ani_object overlapDuration)``{``    ``float responseValue = OHOS::Ace::ResponsiveSpringMotion::DEFAULT_RESPONSIVE_SPRING_MOTION_RESPONSE;``    ``float dampingFractionValue = OHOS::Ace::ResponsiveSpringMotion::DEFAULT_RESPONSIVE_SPRING_MOTION_DAMPING_RATIO;``    ``float overlapDurationValue = OHOS::Ace::ResponsiveSpringMotion::DEFAULT_RESPONSIVE_SPRING_MOTION_BLEND_DURATION;``         ``auto curve = OHOS::Ace::AceType::MakeRefPtr<OHOS::Ace::ResponsiveSpringMotion>(``        ``responseValue, dampingFractionValue, overlapDurationValue);``    ``return curve->ToString();``}