UIView Spring Animation API 的时间曲线初期上升非常快,到了后半部分速度非常缓慢,意在使用开头部分剧烈的变化吸引人们的注意力。实际上从 iOS 7 开始,系统的大部分动画都是这个 API 实现的,比如点击 App/App 文件夹,Navigation Push/Pop。
Your Spring Animations Are Bad(And It’s Probably Apple’s Fault) 这篇文章指出了 UIView Spring Animation API 与真实的弹簧行为有些许出入,这让使用这个 API 的动画效果有点怪异,这篇文章还提供了能够做到真实效果的第三方 Spring 动画库。官方 Spring 动画里有个奇怪的参数是duration
,所有的动画都有这个参数,为什么 Spring 动画有这个参数会奇怪呢?因为真实的弹簧行为不需要这个参数,但是还需要其他几个参数,正如 CASpringAnimation 的参数那样,当所有的物理参数确定后,时间就确定了。为何苹果不放出真正的弹簧动画 CASpringAnimation,而是这个简化版的 UIView Spring Animation 呢?苹果已经大规模使用了该 API,说明这个 API 足够实用。但开发者的期待不一样,我们需要。从实现交互动画的角度来看,Spring 动画是唯一能够指定初始速度的动画 API,如果完全按照真实的弹簧行为,Spring 动画的时间不可知(需要另行计算),而且真实的弹簧也有一段加速过程,或许这个动画应该改名叫做子弹动画,至少我们都认为子弹在枪膛中的加速过程是不算在射程内的。我猜测苹果舍弃了部分参数并强行增加duration
是为了方便使用而做出的妥协,实际上差不多忘光了中学物理的我第一次使用 Spring 动画时完全不知道怎么设置参数,使用参数完善的第三方 Spring 动画库的时候更是不知所措,连看 objc.io 的两位作者手动实现胡克定律都快看得要睡着了,恩,还是设定个时间使用起来方便。
Update: 06/11 大惊喜!官方终于在 iOS 9 中开放了弹簧动画的 API:iOS 9 QuartzCore Changes,更新内容见最下方。
我一直很喜欢 Photos 中点开相册后照片散开的效果,模拟了弹簧的伸缩,效果非常优雅。
iOS 7 中推出了新的 UIView 动画接口,可以使用 Spring 效果,接口如下:
现实生活中的弹簧在固定一端拉开另一端一定距离后放手的自然回弹过程中,每一次回弹的距离都比上一次短,表现为一个振荡过程直至最终停止运动,表现影响回弹距离的因素称之为阻尼系数,也就是方法中的dampingRatio 参数,取值范围0~1,阻尼越小,每次回弹的能量损失就越小,想象一下,能够回弹很久;velocity 参数用来间接指定弹簧的初始速度。文档上的描述是这样,如果你需要视图移动200像素,且让它有100像素/秒的初始速度,那么该值设定为0.5;设置为1的话,表示动画在1秒内完成。那么动画本身指定的时间又起什么作用呢?总体而言,dampingRatio 参数越小,velocity 参数越大,视图表现为剧烈的摇晃。怎么找到 Photos 那种很优雅的范呢?慢慢调参数吧。
目前为止,dampingRatio 参数设置为0.8,velocity 参数设置为10,有点感觉,但在动画的末尾回弹的感觉比较僵硬。基本上 dampingRatio 参数在0.9以上没有什么效果,在0.8以下,动画有点剧烈,在0.5以下可以称得上疯狂了,这是在 velocity 参数设置为10的情况下。而在其他的 velocity 参数情况下,这个趋势又有点复杂了。这两者的相互影响不是线性的,该使用积分了。这么摸索,总不是那么回事,而且和原生应用的效果总是差点。想起来,前些天瞅到的一篇文章讲弹簧动画的,没怎么细看,因为没有实践,不怎么理解,今天使用了官方的这个接口,总有点不大对,找出这篇文章看了看,才明白,这是苹果的错,不厚道,不把真正的弹簧动画类放出来,而弄出来这个阉割版的,根本没法模拟自然的弹簧效果。效果对比如下:
以上图片来自该文章:Your Spring Animations Are Bad And It’s Probably Apple’s Fault。可以看出官方版本的效果尽管每次回弹路径都比上一次短,但它设置了一个对称的回弹,所以衰减过程并不自然。官方放出的 API 还缺乏其他参数,所以怎么调都是不对的。
由于官方不厚道,开发者自己弄了弹簧动画,基本上使用关键帧动画 Key Animation 来实现的。普通动画需要设定动画的开始以及结束的数值以及指定动画时间以及时间曲线,而弹簧动画还有有多项参数来调整,更加复杂,要想有很好的效果,得不断摸索,如果你记得怎么计算弹簧的振荡曲线最好不过了。
以下是 Your Spring Animations Are Bad And It’s Probably Apple’s Fault 这篇文章推荐的非官方弹簧动画实现:
- JNWSpringAnimation:该库还提供了一个 Mac端的工具来帮助你调试动画效果,而不需要你在模拟器或是真机上不断编译、调整、编译了,真是良心。
- Facebook's pop:这个我一直没机会探索过,不知道来实现弹簧效果如何。
- RBBAnimation:官方的反编译版本,效果直追官方,作者说的。发现 Demo 里也提供了在 Mac端进行参数调试的工具,赞。
- AnimationEngine:特点是像 pop 一样使用 CADisplayLink 实现动画以及提供方便的 Block 接口。
最后,提供一个非常好的贝塞尔曲线生成网站 cubic bezier easing 用来制作时间曲线是很不错的,也是作者推荐的。
使用了下 RBBAnimation,基于其给出的 Spring 动画的参数配置来摸索效果比较容易点。有点需要注意的是,要将动画的 additive 属性设置为 NO,本来将设置 additive 属性为 YES 使 Core Animation 在更新 presentation layer 之前将动画的值添加到 model layer 中去。但这种情况下,可能是性能上跟不上导致 presentation layer 不流畅(最下方的效果)。目前的配置参数的效果看起来除了初速度其他的部分和官方的效果还是比较接近的。
当然,这个动画和 Photos 的版本还有很多差距,主要在细节上。Photos 的这个动画是可交互的,你可以用双指缩放来看看发生了什么,这里涉及到了 ViewController 的转场动画。一个视图的出现和消失,其动画的方式应该是从哪来回哪去,Photos 里的动画细节很好。我尝试了下返回时的动画,现在连照片原路返回的细节都不对。
Update: 2015/05/18 之前返回点位置总是不对的原因找到了。首先介绍下原来的动画的思路,点击一个相册 cell 后,push 一个 CollectionViewController,loadView 后播放动画。合适的动画应该是所有的照片从该相册的 cell 处出现然后移动到正确的位置上,返回的动画应该是个逆向过程。这里我使用 CALayer 的 position 属性进行动画,那么动画开始的位置应该是相册 cell 的 center。但是这个 center 是相对于相册 cell 所在的 CollectionView,动画开始的正确位置应该是其相对于屏幕的位置。怎么得到这个位置,很简单,cell 的 center 与CollectionView 的 contentOffset 的差值。为什么是这两个值的差值,想想 cell 的 layoutAttributes 是怎么计算的就明白了。
Push 后的动画由于初始速度快,基本上看不出动画的初始位置其实是有偏差的;但返回的动画就能清楚看到,返回点与相册 cell 的位置有不小的偏差。这个偏差怎么来的?从上面的 Gif 里可以看到我开启了 NavigationBar的显示,此时 CollectionView 的初始 contentOffset 是(0.0, -44.0),因为 NavigationBar 的高度是44,为了正确显示内容,FlowLayout 已经自动调校了 CollectionView 的初始 contentOffset,之前我以为是(0,0),所以有偏差。问题根源找到,解决就好办了,计算时加上这个初始的调校值即可,返回动画的位置偏差问题解决。
Photos 的动画中还有几个好的细节,比如点击相册 cell 后,相册里的照片的动画顺序应该与相册上的一致,展开相册时不明显,返回时这个动作顺序就很重要了,要是动画最后看到的照片与相册封面的不一致,就太破坏感觉了。我在实现这个细节时,发现利用 CollectionView 的 visibleCells得到的 cell 的索引位置并不是按顺序来的,在使用前必须先排序才行。还有两个细节很赞,Photos 里照片收回时并不是弹簧动画,不然影响归纳时的整齐感;而且还体现了照片与相册封面看到的照片的尺寸变化。
这里有一点需要注意,在
viewDidLoad
里先将 collectionView 隐藏,在播放动画时再将隐藏解除。不这样的话,就会出现所有的cell 已经处于正确的位置上,然后进行了一次从某个点移动到正确位置的动画,应该只有从某个点移动到正确位置的动画过程,然后动画停止,cell 都在正确的位置上。
关于 UICollectionView
中的 contentOffset
属性,推荐文章: 理解 ScrollView。
CASpringAnimation in iOS 9 API:
好原来使用的据说是反编译官方的 RBBAnimation 的参数套在 CASpringAnimation 身上,感觉不对劲。Xcode 和在线文档库都无法查询到该类,在线 API 倒是可以查询到,但是没有任何属性描述,不至于这么偷懒吧。