1. 首页
  2. >
  3. 技术代码
  4. >
  5. 前端技术

弹性动效的应用及原理

弹性是动态设计领域中一种常见的表达方式。不同于影视特效、动画 CG 等设计输出即为最终产物的生产环境,UI 动效始终面临着动效还原带来的种种问题,弹性动效的还原就是其中之一。

当设计师完成弹性动效的设计,与工程师进行交接时,双方会发现参数无法对齐 —— 在设计工具中调节效果的参数与在工程开发环境下设定效果的参数无论是名称还是数量都存在差异。

基于以上背景,我们通过研究一些常见原型设计工具的弹性模拟系统,深入弹性系统的动力学原理,与主流平台的弹性动效实现原理进行匹配,解决了从设计工具到工程实现的弹性动效还原问题。


在原型工具中设计弹性动效

Origami Studio

大多数情况下,我们会优先选择使用 Origami Studio(以下简称 Origami)进行高保真原型设计。想要在 Origami 中实现弹性动效,需要使用 Pop Animation Patch 来控制动画进度(Progress),下面是 Origami 中一个简单的弹性动效片段示例:

弹性动效的应用及原理

可以通过修改 Pop Animation 中的 Bounciness 与 Speed 来调节弹性的表现,这是两个易于理解的参数:Bounciness 的值越大,动效越“弹”;Speed 的值越大,动效结束的越“快”。

在 Origami 的 Patch 列表中,位于第 1 个的 Patch:Bouncy Converter 负责将 Pop Animation 的参数 Bounciness 和 Speed 转换为 Friction(摩擦)与 Tension(张力)。

弹性动效的应用及原理


Principle

以友好易用被 UI 设计师、交互设计师甚至产品经理所钟情的 Principle,在定义弹性动效时,将其归类为一种特殊的曲线 —— Spring,与 Ease In、Ease Out 同级别。不同的是这条特殊的弹性「曲线」既不是通过 2 点坐标来定义,也不能调节相应的时长。Principle 中的 Spring 曲线通过修改 Friction 与 Tension 来调节弹性的表现,通过 Principle 提供的曲线可视化预览,我们可以容易的理解这两个参数的作用:Friction 的值越大,动效越“弹”;Tension 的值越大,动效结束的越“快”。

弹性动效的应用及原理


其他

同样使用 Friction 与 Tension 参数调节弹性表现的还有 ProtoPie Studio、Flinto 等原型工具,而其效果也和 Principle 中的表现一致。

弹性动效的应用及原理

ProtoPie Studio


弹性动效的应用及原理

Flinto


在主流平台实现弹性动效

iOS

相对于将视觉风格从拟物改为扁平,iOS 7 对动效的调整方向刚好相反,采用了更加贴近真实的设计方案,这其中最重要的元素就是广泛应用自然的弹性动效。然而你可能会产生疑惑,因为并没有在 iOS 系统中见到“广泛”的弹性动效。这是因为 iOS 系统动效使用最多的,恰恰是一种没有弹性的弹性动效,即应用了弹性系统中的临界阻尼,其主要特征是:物体受弹力的作用最快的恢复到平衡位置。

从 iOS 7 开始到目前为止,在系统内的任何一个场景:页面间的切换,弹窗的出现与消失,键盘的抬起与落下,文件夹的展开与收起 等等,都使用了处于临界阻尼状态的弹性动效。同时 Apple 也提供了使用这种动效的 API:UIView.animateWithDuration:usingSpringWithDamping:initialSpringVelocity

+ (void)animateWithDuration:(NSTimeInterval)duration 
delay:(NSTimeInterval)delay
usingSpringWithDamping:(CGFloat)dampingRatio
initialSpringVelocity:(CGFloat)velocity
options:(UIViewAnimationOptions)options
animations:(void (^)(void))animations
completion:(void (^)(BOOL finished))completion;

该 API 与普通的 UIView 动画相比有两个特别的参数:dampingRatio 和 velocity。文档中对这两个参数的解释:该 API 与普通的 UIView 动画相比有两个特别的参数:dampingRatio 和 velocity。文档中对这两个参数的解释:

  • dampingRatio:与弹性动效静止时的阻尼比。值为 1 时将得到平稳减速没有弹性的效果(即临界阻尼状态),值越接近 0 震荡(弹性)程度越大。
  • velocity:弹性动效的初始速度。为了平滑的开始动效,请把这个值与之前附着的视图的速度匹配。

而在此方法中,控制效果表现快慢的是 duration 。

* 注:iOS 系统常用的临界阻尼效果,参数为:duration: 0.5, dampingRatio: 1

到了 iOS 9,Apple 在 Core Animation 中增加了弹性动效的 API:CASpringAnimation,这个 API 与 iOS 7 提供的用 UIViewSpring 有什么区别呢?

从实现的效果来看,二者并无分别,因为 UIView Animation 本质上是对 Core Animation 的封装,其意义在于面向开发者更加友好。以 CASpringAnimation 为例,它提供了 4 个参数来定义弹性动效:damping、initialVelocity、mass 和 stiffness。这四个参数更接近弹性动效的动力学参数,所以可以使用更贴近真实的调配方式来定义弹性效果的表现。

既然 UIView Animation 是对 Core Animation 的封装,那么同为弹性动效的定义方式,UIViewSpring 与 CASpringAnimation 之间应该可以互相转换。

关于 UIViewSpring 与 CASpringAnimation 之间参数的关系在 iOS 10 的 提供的新 API

UIViewPropertyAnimator 的参数中提到:

The damping ratio for the spring is computed from the formula damping / (2 * sqrt (stiffness * mass)).


—— UISpringTimingParameters

全新的 UIViewPropertyAnimator API 相比 UIViewSpring 和 CASpringAnimation 更加强大,也同时兼容这两个旧 API 的参数定义格式,建议优先考虑使用新 API,具体使用方法不再赘述。


Android

在 2017 Google I/O 大会上,Android 平台终于有了官方支持的弹性动效实现方案 —— 基于物理的弹性动效 SpringAnimation。虽然发布的晚,但是由于这个 API 被包含在支持库中,所以的兼容性得到了很好的保障(支持库更新到 v28.0.0,能够兼容到 API 14;最新的 API 已被包含在 Android X 中,兼容性得到了更好的保证)。


findViewById<View>(R.id.imageView).also { img ->
SpringAnimation(img, DynamicAnimation.TRANSLATION_X).apply {

spring.dampingRatio = SpringForce.DAMPING_RATIO_LOW_BOUNCY
spring.stiffness = SpringForce.STIFFNESS_LOW
setStartVelocity(velocity)
}
}

SpringAnimation 提供了 3 个参数来定义弹性动效:dampingRatio、stiffness 和 setStartVelocity。

文档中对这 3 个参数进行了细致的解释与演示:

  • setStartVelocity:起始速度用于定义在动画开始时动画属性更改的速度。
  • dampingRatio:阻尼比用于描述弹簧振动逐渐衰减的状况。通过使用阻尼比,您可以定义振动从一次弹跳到下一次弹跳所衰减的速度有多快。以下列出了可使弹簧弹力衰减的四种不同方式:
    • 当阻尼比大于 1 时,会出现过阻尼现象。它会使对象快速地返回到静止位置。
    • 当阻尼比等于 1 时,会出现临界阻尼现象。这会使对象在最短时间内返回到静止位置。
    • 当阻尼比小于 1 时,会出现欠阻尼现象。这会使对象多次经过并越过静止位置,然后逐渐到达静止位置。
    • 当阻尼比等于零时,便会出现无阻尼现象。这会使对象永远振动下去。
  • stiffness:刚度定义了用于衡量弹簧强度的弹簧常量。不在静止位置的坚硬弹簧可对所连接的对象施加更大的力。


Web

在 W3C 与 ecma 制定的 Web 标准中与动效相关的内容仅有:CSS animation,而其中并不包含定义弹性动效的方法。但 Web 领域最具魅力与价值的地方就在于各种丰富的、充满创造力的库,关于定义弹性动效,应用较为广泛的库有:animejs 等。在 animejs 中定义弹性动效需要 4 个参数:springPhysicsEasing

easing: 'spring(mass, stiffness, damping, velocity)'


从设计到实现的差异

设计工具

将上述的信息归类整理,首先看各种原型设计工具之间是能够达成一致的,基本都支持 Friction 和 Tension 参数的输出,分别定义弹性的程度与效果的快慢,整理如下:

弹性动效的应用及原理

开发平台

而在开发平台上,弹性效果的实现则有共同点也有差异点,我们先看这张表:

弹性动效的应用及原理

先看共同点:初始速度,该参数在所有 API 中的定义相同,是由外部因素对弹性动效产生的影响参数,其目的是保证对象从之前的运动状态平滑的过渡到弹性动效。值默认为 0。

再看其他共同特征,如 iOS UIViewSpring 与 Android SpringAnimation 中定义弹性程度的参数都是 dampingRatio,且都没有定义对象质量的参数。根据从 UISpringTimingParameters 的参数描述中获取到的公式,得出的 dampingRatio 与 damping 之间的关系:


弹性动效的应用及原理


在 Android SpringAnimation 中,已知 dampingRatio 和 stiffness,仅差 mass 的值即可得出 damping,而在 iOS CASpringAnimation 的参数描述中,mass 的默认值为 1。这里我们进行了大胆的假设在 Android SpringAnimation 中,mass 缺省值为 1。使用假设的 mass,定义 dampingRatio 与 stiffness 的值,带入到公式求得 damping,再将求出的 damping,与定义的 stiffness、假设的 mass = 1 带入到 iOS CASpringAnimation 中,进行实现的效果对比。令人惊喜的是,两端的效果表现完全一致,那么假设验证成立:在 Android SpringAnimation 中,mass 缺省值为 1(这个结论也与 Material Design 的设计原则相吻合)。以同样的方法我们也可以假设 iOS UIViewSpring 中 mass 也是使用缺省值 1,但在 iOS UIViewSpring 中定义效果快慢的参数并不是 stiffness,从值的类型来看也不属于同一种,所以目前还无法验证这个假设。

除去 duration 未明确关系的 iOS UIViewSpring,其他的开发平台 API 已经能够达成一致关系,或相同,或可以互相转换。


差异

尽管在设计工具和开发平台中,我们通过查阅概念定义与尝试验证得到了用于定义弹性强度与效果快慢的参数,但设计工具是使用 Friction 和 Tension 来表示,而开发平台则使用 damping 和 stiffness 来表示。为了寻找这些参数之间的关系,我们更深入的去了解了弹性系统的动力学原理。


弹性系统原理

什么是弹性系统[1]

通过设定刚度(Stiffness)、质量(Mass)、阻尼(Damping)弹性体特征属性,为目标对象定义弹性,使对象表现出:被恢复力(F)驱动,受阻尼影响,从起始值向目标值(系统平衡位置) 运动的过程。此运动系统被称为阻尼谐振子系统,遵守胡克定律[2]。


质量(Mass)

弹性系统的受力对象,会对弹性系统产生惯性影响。质量越大,振荡的幅度越大,恢复到平衡位置的速度越慢。

弹性动效的应用及原理


阻尼(Damping)

是一个纯数,无真实的物理意义,用于描述系统在受到扰动后振荡及衰减的情形。阻尼越大,弹性运动的振荡次数越少、振荡幅度越小。


阻尼比(Damping Ratio)[3]

表示阻尼相对于临界阻尼的比值。

· 无阻尼:阻尼比 -> 0,系统处于永远振荡的状态;

· 欠阻尼:阻尼比 < 1,对象进行指数递减的振荡运动;

· 过阻尼:阻尼比 > 1,对象进行无振荡的减速运动;

· 临界阻尼:阻尼比 = 1,对象以最短时间结束运动。

弹性动效的应用及原理


刚度(Stiffness)[4]

是物体抵抗施加的力而形变的程度。在弹性系统中,刚度越大,抵抗变形的能力越强,恢复到平衡位置的速度就越快。

弹性动效的应用及原理

摩擦力(Friction)

常见的一种造成弹性系统能量损耗的力,摩擦力的方向始终与对象运动的方向相反。摩擦力越大,弹性系统的能量损耗越快,振荡次数越少,振荡幅度越小。


张力(Tension)[5]

是由一伸展的弦对施力者所做的反作用力,张力越大,反作用力越强,弦恢复原状的速度就越快。


结论与成果

从弹性系统原理中我们认识到 Friction 与 Damping 的特征接近,Stiffness 与 Tension 的特征接近,再次进行假设并验证:开发平台中的 damping 是否等同于设计工具中的 friction;开发平台中的 stiffness 是否等同于设计工具中的 tension。最终通过将设计工具参数直接按照假设的对应关系进行验证,并结合之前的部分结论,最终得出:

damping = friction
stiffness = tension
[default]mass = 1
dampingRatio = damping / (2 * sqrt (mass * stiffness))
[default]velocity = 0


应用

根据结论,设计师在交付弹性动效时,可以按照下面的方法提供参数:

Origami

设定 Pop Animation 参数为

  • Bounciness = 5
  • Speed = 10

使用 Bouncy Converter 得到

  • Friction = 27.0487
  • Tension = 299.61884

则:

  • damping = 27.05
  • stiffness = 299.62
  • mass = 1
  • velocity = 0
  • dampingRatio = 0.78

For Android

SpringAnimation(object, DynamicAnimation.[property]).apply {

spring.dampingRatio = 0.78f
spring.stiffness = 299.62f
setStartVelocity(0f)
}

For iOS

let spring = CASpringAnimation(keyPath:[property])
spring.stiffness = 299.62
spring.damping = 27.05
spring.mass = 1
spring.initialVelocity = 0


Principle

设定 Spring 曲线参数为

  • Tension = 381.47
  • Friction = 20.17

则:

  • damping = 20.17
  • stiffness = 381.47
  • mass = 1
  • velocity = 0
  • dampingRatio = 0.52

For Android

SpringAnimation(object, DynamicAnimation.[property]).apply {

spring.dampingRatio = 0.52f
spring.stiffness = 381.47f
setStartVelocity(0f)
}

For iOS

let spring = CASpringAnimation(keyPath:[property])
spring.stiffness = 381.47
spring.damping = 20.17
spring.mass = 1
spring.initialVelocity = 0


UIViewSpring 的 duration 参数

上述的结论中并不包含关于 iOS UIViewSpring 的 duration 参数的相关信息,我们继续进行深入研究。

首先这次 Apple 并没有像 dampingRatio 一样在文档中提供 duration 与其他参数直接的转换关系,也仅有 iOS UIViewSpring API 唯一一个是以 duration 定义弹性动效的效果快慢,从开发平台能获取到的信息就有限了。

在设计工具中,确实有能够支持以 duration 和 dampingRatio 定义弹性动效的,其中有我们熟悉的 Flinto 和以开源的 Framer.js 构建的原型设计工具 Framer Studio。

Framer Studio 可以默认使用 time 与 damping 定义弹性动效,在弹性动效的代码区域,右键菜单可以看到 Copy Animation 中包含 2 个选项,复制 Damping and Duration 的值,可以看到在 Framer Studio 中用于定义弹性动效的默认方法中:time 对应的是 duration,damping 对应的是 dampingRatio。

同时 Framer Studio 也支持使用 Friction 与 Tension 来定义弹性动效,在相应的代码区域仍旧可以右键 Copy Animation,也就是 Framer Studio 实现了 dampingRatio、duration 与 Friction、Tension 参数之间的互相转换。

弹性动效的应用及原理

得益于 Framer.js [7] 项目是开源的,我们在 Github 上,找到了关于两套参数互相转换的参数,其中具有关键意义的已知 Friction 与 Tension,转换 duration 的方法详情如下:


# Tries to compute the duration of a spring,
# but can't for certain velocities and if dampingRatio >= 1
# In those cases it will return null
epsilon = 0.001
computeDuration = (tension, friction, velocity = 0, mass = 1) ->
dampingRatio = computeDampingRatio(tension, friction)
undampedFrequency = Math.sqrt(tension / mass)
# This is basically duration extracted out of the envelope functions
if dampingRatio < 1
a = Math.sqrt(1 - Math.pow(dampingRatio, 2))
b = velocity / (a * undampedFrequency)
c = dampingRatio / a
d = - ((b - c) / epsilon)
if d <= 0
return null
duration = Math.log(d) / (dampingRatio * undampedFrequency)
else
return null
return duration

引用自:SpringCurveValueConverter.coffee host with ❤️Github


转换器

从 Framer.js 开源项目中获取到了 dampingRatio、duration 与 Friction、Tension 互相转换的方法,而从 Origami [6] 的开源项目中我们也获取到了将 Bouncy Converter:将 Pop Animation 的 Bounciness 和 Speed 转换为 Friction 与 Tension。由此为了方便使用,我们开发了一个简单的 Web 转换器,通过选择各种原型工具或开发平台的定义弹性动效的参数,能够获取到任意平台的弹性动效还原代码参考。

弹性动效的应用及原理

在图中可以看到我们也提供了 CSS 版本的关键帧动画代码参考,我们对关键帧的输出进行了平滑处理,在体积、性能与效果之间得到了良好的平衡关系。

弹性动效的应用及原理

Origami Patch

强大的自定义能力是我们选择优先使用 Origami Studio 的一个原因,在已知公式的情况下,我们可以模拟出各个平台 API 定义弹性动效的参数组合来设计动效:

弹性动效的应用及原理

插值模拟

自定义动态插值器

在打通了弹性动效参数的从设计工具到 iOS UIViewSpring、iOS CASpringAnimation、Android SpringAnimation、CSS Keyframe 之间的参数转换后,弹性动效的还原问题已经能在绝大多数场景解决了,但由于 Android 的弹性动效 API 需要引入支持库,而在一些特殊情况下可能无法引入,这时候我们可以选择一种降级方案:使用自定义插值器模拟弹性动效。

在 Android 开发中,插值器其实就是设计师常说的动效曲线,用于描述在定义时间内值的变化规律。理论上你可以利用此特性定义出任何动画形式,而我们想要定义弹性动效,首先需要的就是用于描述弹性运动的函数公式。

再次感叹开源的伟大,在 Juraj Novák 的 Interpolator 项目中,提供了一个以 factor 为调整弹性程度参数的弹性运动描述函数。

弹性动效的应用及原理

所以根据这个函数我们可以在 Android 中自定义插值器:


import android.view.animation.BaseInterpolator;
public class SpringInterpolator extends BaseInterpolator{
private float mFactor;

public SpringInterpolator() {
this.mFactor = 0.5f;
}

public SpringInterpolator(float mFactor) {
this.mFactor = mFactor;
}
@Override
public float getInterpolation(float input) {
if (input == 0.0f || input == 1.0f)
return input;
else {
float value = (float) (Math.pow(2, -10 * input) * Math.sin((input - mFactor / 4.0d) * (2.0d * Math.PI) / mFactor) + 1);
return value;
}
}
}

在 Android 的插值动效中,描述动效快慢的参数统一为 duration,我们可以利用来自 Framer.js 的转换方法得出一个 duration 参数。但为了还原效果,我们还需要知道 factor 与 Friction 和 Tension 之间的关系。遗憾的是我们并没有查阅到相关资料,也没能够通过假设证明,最终我们选择了使用使用数学办法进行从 Friction、Tension 到 factor、duration 的转换。

首先我们将 iOS UIViewSpring 中的 duration 直接赋予到 Android SpringInterpolator 动画,用于描述效果的快慢。

由于弹性运动是一个衰减过程,所以我们可以把运动过程中达到的最大值当作是与弹性程度相关的量。在遵循胡克定律的条件下,当两个弹性运动的最大值相同,且运动快慢也相同的时候,可以近似的认为二者的弹性效果是相同的。于是在已知 Friction 和 Tension 的情况下,求出当前弹性动效的最大值,再对 Android SpringInterpolator 进行不同 factor 的最大值的遍历与匹配,最终得出一个近似效果对应的 factor 值。由于需要大量的重复计算,且没有通用公式,此方法仅被应用在 Web 转换器中。


自定义 XML 插值器

在一些极端情况下,Android 无法使用动态方法定义动画,必须使用 XML 时,我们也可以利用 XML 定义插值器,但此时的灵活性就就更低了,因为需要把弹性效果,画出来。

插值器是定义在点 (0, 0) 到点(1, 1) 区域内的一段连续曲线,Android 支持使用路径信息的形式来描述插值器,那么关键点就在于如何输出符合效果的路径信息。

弹性动效的应用及原理

使用工具(如 D3.js)按照弹性系统的运动学公式生成 SVG,将起点和终点限制在 1*1 px 的画布内,同时注意调整坐标轴,画布坐标轴以左上角为原点 (0, 0),插值坐标轴却是以左下角为原点 (0, 0)。

弹性动效的应用及原理

把输出的路径信息从SVG中拷贝到自定义 XML 插值器的 pathData 中,我们就得到了一个效果固定的自定义 XML 弹性插值器:

<?xml version="1.0" encoding="utf-8"?>
<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
android:pathData="M-0,0 C0.0748570523,0.246566772 0.0748570523,1.15869141 0.261230469,1.15869141 C0.324264526,1.15869141 0.363204714,1.03922335 0.477279663,1 C0.590576172,0.961044312 0.687408447,1.03922335 1,1"/>


总结

在经历了复杂的研究与验证过程,我们知道了:设计工具中的参数是能够与开发平台的参数对齐的,只是换了种叫法 —— Friction 就是 Damping,Tension 就是 Stiffness;大胆假设有的时候是凭直觉,但直觉背后可能存在着一些潜在因素的影响——在没有定义 Mass 的时候,使用的是缺省值 1;虽然很艰难,但是我们终究还是在 Android 的全场景还原了弹性动效;开源的力量是伟大的,再次感谢开源社区。


附录

1. 谐振子系统:https://en.wikipedia.org/wiki/Harmonic_oscillator

2. 胡克定律:https://en.wikipedia.org/wiki/Hooke%27s_law

3. 阻尼比:https://en.wikipedia.org/wiki/Damping_ratio

4. 刚度:https://en.wikipedia.org/wiki/Stiffness

5. 张力:https://en.wikipedia.org/wiki/Tension_(physics)

6. ReboundJS:http://facebook.github.io/rebound/

7. FramerJS:https://github.com/koenbok/Framer