写点什么

iOS 交互式动画详解(上):iOS 10 以下的实现

  • 2016-07-24
  • 本文字数:4839 字

    阅读完需:约 16 分钟

不久前结束的 WWDC 2016 Session 216: Advances in UIKit Animations and Transitions 介绍了 iOS 10 的新动画 API,让动画与交互无缝连接,这是「开发者的大事、大快所有人心的大好事」。两年前 objc.io 在「交互式动画」一文在探讨了这个话题,本文先来探讨 iOS 10 以下的系统对交互动画的支持,在下篇中深度解读 iOS 10 新 API。

交互动画类型

其实交互式动画在 iOS 系统里可以说是司空见惯的。在可交互动画的执行过程中交互手段(一切控制当前动画的手段,主要是手势) 会随时切入动画过程,根据交互结束后是否更改了动画流程可以将交互动画分为两种:一种会更改动画流程,比如 UIScrollView 的滑动动画,如今看来很普通,在 iPhone 问世之初这个效果可是征服人们的一大利器,「乔布斯在第一次展示 iPhone 时,他特别指出当他给别人看了这个滑动例子,别人说的一句话: 当这个界面滑动的时候我就已经被征服了。」(出自「交互式动画」一文),在这个滑动动画里每次手指在界面上滑动时,前一个滑动动画被中止,当手指离开屏幕后,添加一个新的滑动动画;另一种仅仅控制动画进度而不修改动画,典型代表是交互转场动画,除了带来便利的操作,惊艳的转场动画也是个有力的视觉征服利器。

这两种交互动画的实现手法是不一样的。后一种涉及暂停、恢复和逆转动画,在系统支持的交互转场里,只需要提供一个 UIPercentDrivenInteractiveTransition实例并在交互过程中使用updateInteractiveTransition:来更新进度即可,完全不用我们操心其他事情,实现非常简单。如何在普通的动画上实现这种控制呢?可以参考我之前发表的「iOS 视图控制器转场详解」中的「自定义容器控制器转场」章节:暂停和恢复动画采用官方提供的方法: How to pause the animation of a layer tree? ;手动控制动画进度则需要在暂停动画的基础上更新 CAMediaTiming 协议 (CALayer 遵守该协议) 中的timeOffset属性;而在交互结束后逆转动画则需要CADisplayLink的帮助。iOS 10 引入的新 API 对这些操作进行了封装,实现会简单得多,同时兼容了前一种交互动画的实现方法,打破了两种交互动画的界限。

objc.io 在「交互式动画」一文中探索了前一种交互式动画,实现了下面这种类似控制中心的效果:

这个简单的位移动画里包含了两套交互:滑动控制 (pan 手势) 和点击控制 (tap 手势),要解决三个转换问题,也是所有交互动画需要解决的问题:

  1. Animation to Gesture:动画过程中切入滑动控制,需要中止当前的动画并由手指来控制控制板的移动;
  2. Gesture to Animation:滑动结束后添加新的动画,并与当前的状态平滑衔接;
  3. Animation to Animation:动画过程中每次点击视图后使动画逆转。

objc.io 的两位作者使用了三种方法来实现这个交互动画,手法都是实现弹簧动画 (Spring Animation) 去驱动控制板视图的移动:

  1. 基于 UIKit Dynamics 框架,这是 iOS 7 引入的模拟真实物理行为的动画框架,对控制板视图赋予了弹簧的行为,每次移动都如同有一个弹簧将视图拉向目标位置;
  2. 自己动手实现弹簧动画,所谓动画就是数值的连续变化,作者根据弹簧的胡克定律实现一个算法来计算物体在运动过程中的位置,前面提到的CADisplayLink是个能够与屏幕刷新频率同步的定时器,通过调用指定的方法,每次屏幕刷新时更新视图位置,效果与普通的动画无异。
  3. 将在 2 中实现的弹簧动画使用 Facebook 的 POP 框架驱动。

这三种方法都没有使用 UIView Animation 和 Core Animation(前者是后者的封装),这样实现普通动画的交互就比较困难,接下来讨论如何使用这两种动画 API 来实现上面的交互效果。

Animation to Gesture

添加到 CALayer 上的动画在结束前如果被取消会造成视觉突变,比如在一个右移的动画结束前取消该动画就会造成如下所示的跳跃,从中途直接跳到了终点:

因此交互动画首要解决的就是一个很知乎的问题:「如何优雅地中止运行中的动画而不造成画面突变?」答案是:取消动画时让 modelLayer 的状态与当前 presentationLayer 的状态同步。在手势切入控制板的动画过程后这样做:

这里有个需要注意的地方,如果你使用 UIView Animation,一定要使用带options的 API,且必须将.AllowUserInteraction作为选项之一,不然在动画运行过程中视图不会响应触摸事件,使用 Core Animation 则不受此影响。

Gesture to Animation: Spring Animation

上面的目标是:滑动结束后添加新的动画,并与当前的状态平滑衔接。这需要手指离开屏幕后添加的新动画应该以手指离开屏幕时沿 Y 轴的速度开始,否则速度曲线不连续,看着很不自然。离开速度可以从手势获取,但是指定动画的初始速度,在 iOS 7 公开弹簧动画 (Spring Animation) 接口之前,现有的动画 API 里没有能够直接做到这点的,iOS 7 中引入的 UIKit Dynamics 动画框架也可以实现这个目标,除此之外,要么像 objc.io 的两位作者那样自己动手打造 Spring 效果要么借助第三方的动画库。

弹簧动画的 API:

复制代码
animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:

这个 API 在时间曲线上模拟弹簧的简谐运动 (简单来讲就是来回振荡),实现位移动画时模拟真实弹簧的行为。

其中的速率参数initialSpringVelocity是个CGFloat,这显得很奇怪,为什么不是一个向量呢?「交互式动画」文中对此提出了质疑:「当我们给一个移动 view 的动画在其运动的方向上加一个初始的速率时,你没法告知动画这个 view 现在的运动状态,比如我们不知道要添加的动画的方向是不是和原来的 view 的速度方向垂直。为了使其成为可能,这个速度需要用向量来表示」。实际上尽管速率参数是个数值而非向量,但弹簧动画的初始速度是有方向的:不管视图从 (100, 100) 移动到 (200, 0),还是从 (100, 100) 移动到 (200, 200),初始速度始终是沿着起点到终点的直线方向的。我觉得在这里这两位作者陷入了一个误区,且不说在这个场景里动画的方向是明确的 (Y 轴,起点和终点我们也知道),他们似乎想用弹簧动画来实现添加反向的动画 (即视图在动画中途返回原点,这是第三个转换问题),这个质疑的本质是指弹簧动画无法合成速度,这类似一枚火箭在飞行中启动引擎在相反方向上添加推动力来减速直至反向运动。但弹簧动画和其他的动画 API 都并非由力学引擎驱动,在两位作者发布这篇文章的 iOS 7 时期,弹簧动画是无法做到这点的,从 iOS 8 开始就可以了,但是原因和这个 API 本身没有关系,下一节来解释。两位作者最终放弃了使用这个 API,从而使得整个探索走向了完全不一样的方向。

另外速率参数如何设置也很令人费解,文档里的解释是这样的:

A value of 1 corresponds to the total animation distance traversed in one second. For example, if the total animation distance is 200 points and you want the start of the animation to match a view velocity of 100 pt/s, use a value of 0.5.

initialSpringVelocity并非直接指定初始速率,动画初始 (变化) 速率 = (toValue - fromValue) * initialSpringVelocity,这种相对值的设计避开了动画的具体变化值,方便使用者估算和设置动画时间。那么从 (100, 100) 移动到 (300, 300),如果你希望视图沿着目标方向的初始速度为 (150, 150),即合成速度约为 150 X 1.4(2 的开方值) = 210,直线距离约为 200 X 1.4 = 280,那么initialSpringVelocity约为 210/280 = 0.75。

回到这个阶段的问题本身,怎么解决?

Animation to Animation: Additive Animation

在动画中途点击控制板视图后让视图返回到原来的位置,做法是再次添加一个同样动画属性的动画 (使用 Core Animation 时注意使用不同的 key),但在效果上完全抵消,效果有如下几种:

使用 UIView Animation 或者 Core Animation 不做特殊设置的话,效果是第一种;使用 UIView Animation 时指定 BeginFromCurrentState 选项的效果是第二种,位置不会突变但速度有突变;我们需要的是第三种效果,使用 Additive 类型的动画时,在控制板打开或者关闭过程的任何时刻点击视图,视图将会向反方向移动,动画不会有位置和速度突变,但 UIView Animation 没有这个选项。

在 objc.io 的这篇文章发布后的半个多月正是 WWDC 2014 大会,在 Session 236: Building Interruptible and Responsive Interactions 里介绍了解决上述三个转换问题的方法,上面的动图都截取自该 session,前两个问题的解决办法就是上面说的那些,也提到了 objc.io 这篇文章里中使用的 UIKit Dynamics 这个技巧,而最为棘手的第三个问题需要实现 Additive 类型的动画,该效果来自 CAAnimation 子类 CAPropertyAnimation 的additive属性。

additive属性自 iOS 2 起就存在,文档解释:

If YES, the value specified by the animation will be added to the current render tree value of the property to produce the new render tree value. The addition function is type-dependent, e.g. for affine transforms the two matrices are concatenated. The default is NO.

使用 CAKeyframeAnimation 时必须将该属性指定为true,否则不会出现期待的结果。不过,在 CABasicAnimation 里使用这个属性很需要一番技巧,我在尝试使用这个属性时总是得不到想要的效果,直到观看了这个 session 才恍然大悟,原来是这么设计的,文档的解释是正确的废话。

如何使用 CABasicAnimation 实现上面的效果呢?非 Additive 的动画的变化范围是绝对值设计,添加到 presentationLayer 的动画的变化范围是:fromValue -> toValue,Additive 的动画采用的是相对值设计,添加到 presentationLayer 的动画的变化范围是:modelLayerValue + fromValue -> modelLayerValue + toValue。假设控制板开关后的 Y 轴差距为 500,这样实现 Additive 效果:

注意指定timingFunction,该值默认为 nil,效果是线性曲线 (Linear),两个动画叠加后的效果与 BeginFromCurrentState 等同。但 Core Animation 也没有提供 Spring Timing Function,虽然从 iOS 6 起就有人发现了上面的 CASpringAnimation,但是这个 API 才到 iOS 9 才公开,而且没有文档。而 UIView Animation 没有提供实现 Additive 效果的选项,只能退而求其次实现 BeginFromCurrentState 的效果。所以点击后逆转动画在 iOS 7 上的效果无法完全满足设计的要求,可以依靠一些第三方弹簧动画来弥补,比如 RBBAnimation ,基于 CAKeyframeAnimation,支持 iOS 6。

iOS 8 中 UIView Animation 默认实现了 Additive 效果,所以从 iOS 8 开始,解决第三个转换问题就太容易了,直接添加反向的动画即可:

小结

从代码上看,无比简单。不过别忘了没有 Additive 类型的动画,objc.io 在「交互式动画」中做出的艰辛探索,实现成本要高出许多。在 iOS 7 中利用 UIView Animation/Core Animation 实现交互动画还有不完美的地方,而 UIKit Dynamics 框架是个非常好的替代选项。从 iOS 8 开始没有了限制,而 iOS 7 以下的系统则需要自己打造 Spring 动画或者依靠第三方动画库。

我为这个动画写的Demo 参见 ControlPanelAnimation

https://github.com/seedante/ControlPanelAnimation

参考

  1. objc.io 第 12 期专题:动画:http://objccn.io/issue-12/
  2. WWDC 2014 Session 236: Building Interruptible and Responsive Interactions:https://developer.apple.com/videos/play/wwdc2014/236/
  3. WWDC 2014 Session 221: Creating Custom iOS User Interfaces:https://developer.apple.com/videos/play/wwdc2014/221/
  4. 使用 Facebook Pop 框架填补手势与动画间的差距:http://www.infoq.com/cn/news/2014/05/facebook-pop-engine
  5. iOS 视图控制器转场详解: https://github.com/seedante/iOS-Note/wiki/ViewController-Transition
  6. How to pause the animation of a layer tree?: https://developer.apple.com/library/ios/qa/qa1673/_index.html

感谢徐川对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2016-07-24 18:165661

评论

发布
暂无评论
发现更多内容

睁眼、耸肩、觉醒:人形机器人的吊诡与最终幻想

脑极体

性能监控之Filebeat+Kafka+Logstash+Elasticsearch+Kibana 构建日志分析系统

zuozewei

ELK 性能监控 日志监控分析 签约计划第二季

Android单页应用如何在Activity与Fragment中共享状态

Changing Lin

12月日更

iKuai与DNSPod合作,搞了一个大动作!

网络安全 DNS DNS劫持

性能工具之常见性能工具一览

zuozewei

工具 性能测试 签约计划第二季

Linux之ls命令

入门小站

Linux

实用机器学习笔记九:数据部分总结

打工人!

机器学习 算法 学习笔记 12月日更

性能基础之CPU、物理核、逻辑核概念与关系

zuozewei

Linux 性能测试 基础 签约计划第二季

【Promise 源码学习】第十六篇 - 了解 co 库

Brave

源码 Promise 12月日更

深度揭秘技术创新:全球首个知识增强千亿大模型是怎样炼成的?

百度大脑

人工智能

工业4.0时代:低代码的兴起,或将掀起制造业格局的变革

优秀

低代码 工业4.0

数据库大赛50强之「华东师范大学」:恰同学少年,代码激扬!

OceanBase 数据库

数据库 学习 开源 oceanbase

Xcode13 适配之打印启动时间

CRMEB

Apache Log4j 2 报高危漏洞,CODING 联手腾讯安全护卫软件安全

CODING DevOps

Apache DevSecOps CODING Log4j 2 腾讯安全

40 K8S之Calico网络插件

穿过生命散发芬芳

k8s 28天写作 12月日更

XTransfer技术专家康康:从普通程序员到架构师的进化之路

XTransfer技术

程序员 创业心态 创业公司 跨境支付 XTransfer

记录docker,k8s,oneops,.netcore搭建个人博客过程

哔啵哔啵

.net Docker k8s .net core oneops

数据情报在金融行业的探索系列

nexpose

数据分析 目标追踪 风险识别 数据分析预测 数据情报

使用 Apache APISIX serverless 能力快速拦截 Apache Log4j2 的高危漏洞

API7.ai 技术团队

Serverless log4j APISIX

即时通讯(IM)开源项目OpenIM本周版本发布- v1.0.7web端一键部署

OpenIM

性能工具之Java分析工具BTrace入门

zuozewei

Java 性能测试 性能分析 签约计划第二季

伙伴大会报名截止倒计时3天!

明道云

性能分析之构建 Linux 操作系统分析决策树

zuozewei

Linux 性能测试 性能分析 签约计划第二季

性能监控之Sleuth+Zipkin 实现 SpringCloud 链路追踪

zuozewei

链路追踪 性能测试 SpringCloud 性能监控 签约计划第二季

性能监控之Telegraf+InfluxDB+Grafana+Python实现Oracle实时监控

zuozewei

数据库 oracle 性能监控 签约计划第二季

性能分析之单条SQL查询案例分析(mysql)

zuozewei

MySQL 性能测试 性能分析 签约计划第二季

TypeScript 之模块

冴羽

JavaScript typescript 翻译 前端 web前端

下周上海见!超越商业,创业邦100未来独角兽峰会议程抢先看

创业邦

皮皮APP x 武汉市残疾人福利基金会 共建成长乐园

联营汇聚

时间紧资金少人才缺?8位产业专家带你破局AI智能化升级

百度大脑

人工智能

5G与2021的双向奔赴

脑极体

iOS交互式动画详解(上):iOS 10以下的实现_移动_seedante_InfoQ精选文章