作者:Chris Wagner 原文地址:tvOS SDK: An iOS Developer’s Initial Impressions

在Walter Isaacson编写的《Steve Jobs》的最后一章中,Steve说过“我最终实现并让它拥有了你们能想象到的最简单的用户界面”。当时他指的就是Apple TV。

Steve不幸离世已经过去很久了,但是人们一直翘首以盼着这款产品。就在昨天2015年9月9日,超过四个春秋的期盼,我们终于等来了这个礼物,虽然可能与Steve对这款产品的愿景和理想有所差距,但是革命的步伐已经坚实的迈出了第一步。

作为一名iOS开发者,昨天的产品发布会让我心潮澎湃,因为Apple宣布,新的Apple TV集成了App Store,这就意味着我们可以为它开发专有的应用,并且会让我们重新认知已了解的iOS知识,以及会开启更多新的展现想法、创意的机会。

我和其他教程团队的成员已经开始深入研究tvOS SDK,并且正在努力准备一些有价值的tvOS教程(通宵达旦!),在此同时,我想从一个iOS开发者的视角与大家分享我对tvOS的初步印象。

让我们一探究竟吧!

基本概念

老款的Apple TV其实类似一个机顶盒,主要提供的功能是让用户通过及其简单的操作界面导航或浏览视频内容。昨天发布的新Apple TV在此基础上进一步的扩充功能,使一些成熟的应用能在Apple TV中运行,比如购物应用,甚至是游戏。

tvOS-1

让我们来看看以下这些信息:

  • 硬件规格: 值得一说规格是64位 A8处理器,32G或64G的存储空间,2G的内存,1080p的分辨率以及支持HDMI,Siri远程遥控,新的Apple TV遥控器。
  • 价格: 新Apple TV的定价为32G 149美元,64G 199美元。这样的定价出乎大多人的以外,原以为新Apple TV的价格会更高一些。
  • 发售时间: Apple在发布会上给出了一个非常模糊和不确定的发售时间“十月下旬”,但是Apple在发布会之后就立即向开发者们提供了tvOS SDK。现在离正式发售已时日不多,如果大家有针对新Apple TV好的想法,我建议你们要抓紧时间了。
  • 开发硬件: Apple已经宣布了一个开发者计划允许已经注册的开发者们请求Apple TV Developer Kit。该计划可以让开发者们提前对Apple TV进行开发测试。这看起来似乎是一件很美好的事,但是如果你真的要加入该开发者计划,那么仔细看看下面的建议:

-> 注意: 众所周知,iOS、OSX的开发者计划都是收费的,tvOS的开发者计划应该也不例外,不过之前对于该计划的收费信息是非常含糊不清的,但是现在我们通过Apple员工在开发者论坛上发的帖子中确认了该计划只需要话费1美元即可加入。如果大家有意为tvOS贡献一份力量,或者需要编写教程材料的话,强烈建议你们加入该开发计划。

开发 tvOS 应用

啰嗦完一堆后,显然最重要的事就是怎样为新Apple TV开发应用,这应该也是大家最感兴趣的事!

为新Apple TV开发应用实际就是在为tvOS开发应用,当然tvOS这个名字是由Apple根据不同设备而创造的。tvOS仍然是基于iOS开发的,所以有很多框架大家都应该非常熟悉了。

如果要为tvOS开发应用,大家必须要从开发者中心下载Xcode7.1进行开发。该版本是Xcode新的beta版,目前只有该版本支持tvOS以及提供tvOS的模拟器。如果你有需要的话,也可同时下载Xcode7 GM版本,他们可以共存于你们的电脑中。

Apple提供了两种在tvOS上开发应用的方法:

  1. TVML Apps: 这类应用是使用完整的新开发技术开发的,比如TVML、TVJS、TVMLKit。这对我们来说是一个巨大的惊喜,我们会在之后有较详细的介绍。
  2. Custom Apps: 这类应用是使用我们已经比较熟悉的开发技术进行开发的,比如大家熟知的一些iOS框架,Storyboard、UIKit、Auto Layout等。

当你们在为Apple TV开发应用时,你们会发现Xcode中会出现单独的程序目标(这意味着用户将会单独购买的形式购买tvOS应用)。虽然Apple在发布会上说Apple TV中的应用支持通用购买模式,也就是针对iOS和tvOS都支持的应用,用户只需要购买一次即可同时在iPhone和Apple TV上使用。但是从目前的情况来看,我们还不确定Apple是如何实现的,或许在Itunes Connect中支持连接两个程序目标的功能即将来临?

TVML Apps

先前我提过,为tvOS开发应用有两种方式,第一种就是通过TVML、TVJS和TVMLKit技术。如果这些缩写对于你们来说非常陌生,那么不要着急,这就告诉你们他们都是什么:

  • TVML是一种基于“Television Markup Language”的XML格式。
  • TVJS是基于JavaScript APIs的脚本语言,它可以根据TVML中定义的内容展示应用。
  • TVMLKit是连接TVML、TVJS和原生tvOS应用的工具包。

如果你是一名经常使用原生APIs开发原生iOS应用的开发者,那么看到这些类似前端的技术可能会有点退缩。但希望你能保持一颗学习的心,学习了解这些新的强大的特性。

我在这列举一个非常典型的Apple TV的应用场景。大家可以想象一下:你们想把一些内容或信息展现给用户,这些内容和信息储存在你们的服务器上,并且这些内容的格式、查询方式都服务于iPhone或iPad中的应用,那么你肯定会希望你的tvOS中的应用也能方便的使用这些内容,并做到与iOS中应用有相似的展现、查询、导航方式。

tvOS-2

如果我列举的这个例子正好是你们tvOS应用的需求,那么你可能需要考虑如何使用TVMLKit工具包。Apple已经为我们开发者完成了一些主要的工作,比如提供了许多可重用的tvOS界面展示模板,这些模板大都与iOS应用的界面神似,所以用户们并不会感到陌生,这些模板大概有18种之多。大多数模板都可以让你创建出脱凡的、非常适合家中电视展示的界面。大家可以访问Apple的文档查看这些我们强烈推荐使用的模板。

tvOS-3

我也推荐大家在tvOS模拟器运行TVML Catalog sample app去查看每个模板。同时你需要启动一个本地的web服务器,便于tvOS应用通过模板展示内容时访问,所以你需要仔细查阅README.md文件去了解相关内容。

诚然TVMLKit还有许多知识点,如果你希望基于它开发一款tvOS应用,那么我提到的一些文档是非常值得你们去查阅参考的。这里我着重推荐大家首先看看如何使用菜单导航类的模板,比如menuBarTemplatemainTemplatesearchTemplate等。然而,如果你希望用户不只是被动的通过你的tvOS应用观看或收听内容,而是希望用户与应用有更多的交互,给用户高质量的用户体验,那么你们就需要了解如何开发完整的自定义的tvOS应用。

Custom Apps

你所知道的大多数iOS框架,比如UIKit、Core Graphics、CloudKit等等都可以在tvOS中使用。你可以在Apple提供的这个文档中查看哪些框架可以用,哪些不可以使用。如果你现在正是一名iOS开发者,那么会对这个文档中的列表非常熟悉。而且不管使用Swift还是Objective-C或者C都可以开发tvOS应用。

尽管如此,如果想开发自定义的tvOS应用,还是有许多新的知识需要我们学习,作为一名iOS开发者,也应该掌握甚至精通这些新的知识。

Focus Engine

其中一个对于iOS开发者比较陌生的概念是用户输入/选择方法。在使用Apple TV的过程中,用户不会用手指去按压或滑动屏幕,取而代之的是使用Apple提供的遥控器或者是一些游戏控制器。

tvOS采用了一个被称为Focus Engine的引擎系统,它在整个tvOS中有且只有一个。该引擎负责响应当用户使用遥控器上的手势操作或按键操作对菜单或内容进行上下左右的选择。

Focus Engine会自动根据用户的选择决定要聚焦或展示的视图,你不需要在代码中做任何类似选中或导航的处理。比如说,此时展示的界面是你已经在Storyboard中设计好的视图布局,其中有一个视图是当前聚焦状态,那么当用户通过手势往右滑动时,Focus Engine会自动根据当前聚焦的视图找到与之相邻的左边的视图,并将其选中和聚焦。

tvOS-4

作为一个开发者,你必须要学习与Focus Engine相关的API,比如当聚焦的视图发生变化时如何获取通知、如何通过编码触发聚焦视图的改变扥等。更多关于Focus Engine的API可以参阅App Programming Guide for tvOS文档中的 Supporting Focus Within Your App章节。

TVServices

虽然iOS开发者在tvOS应用的开发过程中,主要使用的是一些已经比较熟悉的iOS框架,但是也有一些tvOS特有的框架需要开发者们去了解掌握,比如像新加入的TVServices框架。

TVServices的主要作用是描述你的应用的内容,以便tvOS在首页的顶部栏位显示。在首页顶部栏位显示的应用都是用户自己设置的,用户可以将他们认为最为常用的或最为重要的应用放在首页顶部栏位,便于快速打开或浏览其中主要信息。

tvOS-5

你们的应用可以在用户不进入应用的情况下,向用户提供简短的、感兴趣的信息,这就会使你的应用有了额外价值,使用TVServices无疑是提高你的应用下载量的绝佳手段。比如一个游戏应用,通过TVServices显示游戏存档,那么用户就可以直接从首页通过游戏存档进入游戏。如果是一个社交应用,那么就可以在首页显示社交动态信息,如果是一个照片分享应用,那么就可以显示近期朋友和家人分享的照片。

更多关于TVServices的信息可以查阅TVServices Framework Reference文档。使用好它对你的应用非常有益。

Parallax Images

还有一件会让你疑惑的可能就是在发布会的Keynote中,Apple着重强调了图片和应用图标的视差效果。这是一个非常棒的视图效果,但是为什么这个特性会被放在Keynote中提及,它为什么这么重要呢?

如果你使用了tvOS模拟器,你就会明白为什么这个特性如此重要。当移动聚焦视图时,你需要向左或向右滑动,但如果你只滑动了一点,Apple会对当前聚焦的视图做一个轻微的旋转处理,目的是让用户知道现在正在作以操作(但是还需要继续滑动来改变聚焦视图)。这是一个着眼于细节但又非常有用的特性。

tvOS-6

Apple把视差效果看做tvOS设计中的一个关键组成部分,并强烈建议应用图标和电影海报使用该特性。不过感谢Apple在App Programming Guide for tvOS文档中提供了Creating Parallax Artwork章节,帮助我们创建视差特性的图片资源,以及为我们提供了视察图片资源的预览应用。

tvOS-7

Controllers

每一个购买了新Apple TV的用户,都会收到一个功能强大的新版遥控器。上一代只能上下左右简单选择方位以及只有几个简单导航按键的遥控器,而新一代的Apple TV遥控器增加了更多令人激动的新特性:

  • 玻璃触控板:位于遥控器的顶部,可以让用户在其上面进行一些手势操作,比如滑动、轻拍、点击。
  • 麦克风:可以让用户通过遥控器访问Siri(Siri有使用国家的限制)以及可以通过语音控制电视的音量。
  • 陀螺仪:结合动作传感器可以为用户提供非常棒的游戏体验。用户可以通过倾斜遥控器在游戏中控制方向盘开车,或者控制英雄奔跑穿越山洞。

tvOS-8

你可以使用你知道的处理手势操作的API去监听滑动或轻点,还有一些新的API去监听遥控器上不同的按钮,比如pressesBegan()pressesEnded()pressesChanged()pressesCancelled()

遥控器通过蓝牙技术与Apple TV主机交互,这意味着为蓝牙游戏控制器敞开的大门(恕我直言,用Apple的遥控器玩游戏并不是很好的选择)。Apple已经宣布Nimbus Steelseries Controller将会支持新Apple TV。想了解更多关于这方面的信息请查阅Working with Game Controllers

tvOS and Games

我们衷心的希望游戏能在tvOS上火起来,Apple似乎是直接瞄准了任天堂漠不关心的一个游戏市场。

tvOS有很健壮的游戏技术作为支撑,SpriteKit和SceneKit都可以在tvOS中正常工作,在Keynote中Crossy Roads开发者展示了他们的tvOS游戏,该游戏使用Unity开发,所以这也暗示着在不久的将来tvOS也会支持Unity开发的游戏。

大多数用SceneKit开发的游戏都可以无缝移植到新Apple TV中。比如Ray就将教程中介绍过的Zombie Conga游戏移植到了新Apple TV中,花费时间还不到10分钟,几乎不需要修改任何代码。

-> 注:游戏视频可前往原文查看,但需要科学上网。

Limitations

或许目前关于tvOS比较有疑惑的两点就是本地存储和应用大小的限制。

Limitation: Local Storage

关于本地存储,基本确定是没有!如果你的应用需要持久化用户的数据,那么你需要使用iCloud、CloudKit或者自己的备份服务去实现。任何试图存在Apple TV中的数据都不保证在下次打开应用时还嫩存在。如果你想同步不同设置之间的数据,那么你就需要将数据线存在某个地方,但绝不是Apple TV中。

tvOS-9

一定要牢记这点,它对于你设计tvOS应用的结构时很关键。这里列出了一些规则:

  • 如果你需要存储的数据量小于1MB,iCloud的key-value存储方式是一个可以选择的方案。但是要切记,iCloud KVS严格限制了只能有所属者才可以访问数据,并且不能共享给其他用户。
  • 如果你需要分享事件或者数据给其他用户,CloudKit是一个不错的选择方案。
  • 如果你开发的是一个跨平台的应用或者有特殊的需求,你就得使用你自己的备份服务了。

Limitation: App Size

另一个限制是关于应用大小的限制,规定不能超过200MB。

tvOS-10

在你们掀桌之前,请回顾一下WWDC2015以及介绍过的“On-Demand Resources”相关API。这几乎像是Apple为tvOS提前铺垫的。

这些API通过按需下载资源文件减小应用初始安装时的大小。开发者可以在Xcode中给多媒体资源文件用标签进行标记,将应用提交后,App Store会自动根据标记将多媒体资源文件拆分为一个一个下载包。当用户在使用应用时如果需要用到某类标记的资源文件,就可以请求下载该标记的资源文件。当然你得有预期的判断,要先于用户使用资源前开始下载他们需要的资源,这样对于用户来说他们根本不会感受到因为下载资源带来的不好的用户体验。

举一个简单的例子,比如你有一款游戏应用有10个关卡。在用户安装该游戏时可以只包含两个关卡的多媒体资源文件。一旦用户完成了第一个关卡,你应该发送一个下载请求,下载第三个关卡的多媒体资源文件。当用户完成了第二个关卡时,第三个关卡需要的资源文件早已下载好准备妥当。在用户的整个游戏过程中,你就可以运用该机制一步一步去下载所需的资源文件。

虽然这种机制对于开发者来说无疑增加了工作量,但是这对用户来说是一件非常愉悦的事情。你试想一下,用户是愿意等待下载一个1GB大小的游戏呢,还是更愿意下载一个100MB大小的游戏并立即开始游戏呢。不过该机制也存在一个隐患,如果用户的网络条件很差,他们一般都更愿意在晚上挂着下载。那么如果用户在白天玩你的游戏,然后同时用龟速的网络下载后面关卡的资源文件,这简直是令人发指的用户体验。不幸的是作为tvOS开发者,在这种情形下目前也无能为力。

本文首发CSDN,如需转载请与CSDN联系。

上一篇文章通过两个动画示例带大家了解和认识了CALayer动画,包括如何使用CAShapeLayer、CABasicAnimation、CAAnimationGroup等。在这篇文章中,依然会通过两个示例向大家讲解更多CALayer动画的知识。

老规矩,先让我们看看最终要实现的动画效果:

ReplicatorAnimation-17

经常听音乐的人对第一个动画效果肯定有会觉得很眼熟,类似播放音乐时音频高低起伏的动画,这种动画在应用中常被用作标识正在播放音乐或广播。第二个动画依然是一个等待加载的动画,在我的印象中肯定是有应用使用过,具体的已经记不清了。下面就让我们来实现这两个动画吧。

Replicator Animation

新建一个项目,名为ReplicatorAnimation,打开Main.storyboard,添加一个UIView,颜色位置按大家喜好设定:

ReplicatorAnimation-1

添加该UIView在ViewController.swift中的Outlet:

ReplicatorAnimation-2

接下来我们在ViewController.swift中添加一个方法firstReplicatorAnimation(),在该方法中编写如下代码:


let replicatorLayer = CAReplicatorLayer()
replicatorLayer.bounds = CGRect(x: replicatorAnimationView.frame.origin.x, y: replicatorAnimationView.frame.origin.y, width: replicatorAnimationView.frame.size.width, height: replicatorAnimationView.frame.size.height)
replicatorLayer.anchorPoint = CGPoint(x: 0, y: 0)
replicatorLayer.backgroundColor = UIColor.lightGrayColor().CGColor
replicatorAnimationView.layer.addSublayer(replicatorLayer)

这里出现的CAReplicatorLayer是一个新面孔,它也是CALayer的子类,正如它的名称一样,CAReplicatorLayer可以对它自己的子Layer进行复制操作。创建了CAReplicatorLayer实例后,设置了它的尺寸大小、位置、锚点位置、背景色,并且将它添加到了replicatorAnimationView的Layer中:

ReplicatorAnimation-4

这里要啰嗦几句,Layer的默认锚点坐标是(0.5, 0.5),也就是Layer的中心点位置,而Layer的position又是根据锚点计算的,所以如果你设置Layer的position属性为(10, 10),就相当于设置了Layer的中心位置为(10, 10),并不是你期望的左上角位置。所以如果Layer想使用它父视图的坐标位置,就需要将锚点位置设置为(0, 0),这样一来Layer的position属性标识的就是Layer左上角的位置:

ReplicatorAnimation-3

然后我们继续在firstReplicatorAnimation()方法中添加代码:


let rectangle = CALayer()
rectangle.bounds = CGRect(x: 0, y: 0, width: 30, height: 90)
rectangle.anchorPoint = CGPoint(x: 0, y: 0)
rectangle.position = CGPoint(x: replicatorAnimationView.frame.origin.x + 10, y: replicatorAnimationView.frame.origin.y + 110)
rectangle.cornerRadius = 2
rectangle.backgroundColor = UIColor.whiteColor().CGColor
replicatorLayer.addSublayer(rectangle)

通过上面的代码,再次创建了一个Layer,这次使用的是CALayer,因为我们只需要一个很普通的Layer,为其设置位置、尺寸、背景色、圆角属性,然后添加在replicatorLayer中:

ReplicatorAnimation-5

动画的主体之一已经绘制好了,下面我们让它动起来。在上述代码后面,接着添加如下代码:


let moveRectangle = CABasicAnimation(keyPath: "position.y")
moveRectangle.toValue = rectangle.position.y - 70
moveRectangle.duration = 0.7
moveRectangle.autoreverses = true
moveRectangle.repeatCount = HUGE
rectangle.addAnimation(moveRectangle, forKey: nil)

首先我们创建了按Y轴移动的动画实例,然后设置了移动的目标位置,动画持续时间,重复次数设置为无限大。这里有一个属性大家可能比较陌生,那就是autoreverses,这个属性为Bool类型,设置为true时,开启自动反向执行动画,比如示例中的白色长方形的移动动画为向上移动50个像素,如过autoreverses设置为false,那么动画结束后,会根据重复次数,白色长方形重新回到初始位置,继续向上移动,如果autoreverses设置为true,则当动画结束后,白色长方形会继续向下移动至初始位置,然后再开始第二次的向上移动动画。

编译运行看看效果:

ReplicatorAnimation-6

至此,大家应该也已经看出来了,这个白色的长方形就是动画中第一个上下移动的白色长方形,那么后两个如何创建呢?还需要再写两遍上面的代码吗?请大家在下面的文章中寻找答案。

在上述代码下面再添加一行代码:


replicatorLayer.instanceCount = 3

显而易见,这是CAReplicatorLayer的能力了,这行代码的意思是将replicatorLayer的子Layer复制3份,复制Layer与原Layer的大小、位置、颜色、Layer上的动画等等所有属性都一模一样,所以这时编译运行代码我们看不到任何不同的效果,因为三个白色长方形是重合在一起的,所以我们需要设置每个白色长方形的间隔:


replicatorLayer.instanceTransform = CATransform3DMakeTranslation(40, 0, 0)

这行代码涉及到CAReplicatorLayer的另一个属性instanceTransform,它的作用是设置每个子Layer如何变化。CATransform3DMakeTranslation这个类的含义是使Layer根据X、Y、Z轴进行平移。现在再编译运行看看效果如何:

ReplicatorAnimation-7

现在三个白色长方形的运动轨迹和时刻都是一直的,这显然不是我们想要的结果,我们需要三个白色长方形有上下起伏的视觉效果,所以我们继续添加一行代码:


replicatorLayer.instanceDelay = 0.3

instanceDelay这个属性使CAReplicatorLayer中的每个子Layer的动画起始时间逐个递增。这里我们设置为0.3秒,也就是第一个长方形先执行动画,过0.3秒后第二个开始执行动画,再过0.3秒后第三个开始执行动画。我们编译运行看看效果:

ReplicatorAnimation-8

显然我们只想显示replicatorLayer区域里的内容,我们并不想看到超出它边界的内容,所以我们再添加一行代码:


replicatorLayer.masksToBounds = true

masksToBounds是CALayer的属性,作用是将Layer视为一个遮罩,只显示遮罩区域内的内容。最后我们回到初始化replicatorLayer的地方,找到这行代码replicatorLayer.backgroundColor = UIColor.lightGrayColor().CGColor,将replicatorLayer的背景色改为无色replicatorLayer.backgroundColor = UIColor.clearColor().CGColor。再次编译运行看看最终效果:

ReplicatorAnimation-9

Replicator Indicator Animation

CAReplicatorLayer的功能是很强大的,这一节将通过另一个加载动画的实例向大家介绍它的其他特性。

首先打开Main.storyboard,拖进一个新的UIView,位置颜色随大家喜好:

ReplicatorAnimation-10

随后添加该UIView在ViewController.swift中的Outlet activityIndicatorView

ReplicatorAnimation-11

然后在ViewController.swift中添加一个方法activityIndicatorAnimation,和上一个动画示例一样,我们先创建一个CAReplicatorLayer:


let replicatorLayer = CAReplicatorLayer()
replicatorLayer.bounds = CGRect(x: 0, y: 0, width: activityIndicatorView.frame.size.width, height: activityIndicatorView.frame.size.height)
replicatorLayer.position = CGPoint(x: activityIndicatorView.frame.size.width/2, y: activityIndicatorView.frame.size.height/2)
replicatorLayer.backgroundColor = UIColor.lightGrayColor().CGColor
activityIndicatorView.layer.addSublayer(replicatorLayer)

上述代码和上个示例中的差不多,唯一不同的就是replicatorLayer的锚点使用的是默认值,即锚点就是中点,position属性代表亦是中点,所以将position属性设置为父视图的中点即可。这里意在让大家多多理解CALayer中anchorPointposition属性。接下来添加如下代码:


let circle = CALayer()
circle.bounds = CGRect(x: 0, y: 0, width: 15, height: 15)
circle.position = CGPoint(x: activityIndicatorView.frame.size.width/2, y: activityIndicatorView.frame.size.height/2 - 55)
circle.cornerRadius = 7.5
circle.backgroundColor = UIColor.whiteColor().CGColor
replicatorLayer.addSublayer(circle)

上述代码的目的是用CALayer创建一个圆形,其实CALayer创建出的形状默认是矩形,但是把四个角的弧度设置为边宽的一半,矩形就变成了圆形。将这个圆形的位置设置在父Layer的中间靠上位置,背景色设置为白色。此时该圆形就是文章开头效果图中第二个动画里的主体了:

ReplicatorAnimation-12

不过在动画中我们看到有许多个小圆形组成一个大圆,如果重复上面的代码,一个一个设置位置,那绝对是令人发指的行为,好在我们有CAReplicatorLayer帮助我们实现,下面就来看看如何使用CAReplicatorLayer复制子Layer,并让子Layer形成一个圆形。让我们接着添加如下代码:


replicatorLayer.instanceCount = 15
let angle = CGFloat(2 * M_PI) / CGFloat(15)
replicatorLayer.instanceTransform = CATransform3DMakeRotation(angle, 0, 0, 1)

上述的代码中,首先对子Layer,也就是白色圆形复制了15份。然后将360°除以15份,算出每一个圆形针对它前一个圆形应该偏移的角度。最后我们用到了CATransform3DMakeRotation,它同样是CATransform3D的一个结构,含义是使Layer在X、Y、Z轴根据给定的角度旋转。这样我们复制的15份圆形就会按照我们计算的角度排列,并形成一个大圆:

ReplicatorAnimation-13

接下来让我们分析一下这个动画,整体看上去感觉像一颗流星拖着尾巴在不停的转圈,但细看每一个小圆点,其实是在不停的进行放大缩小的动画,只不过每个小圆点的动画对于它前一个小圆点的动画有一定的延迟。所以首先我们需要实现小圆点放大缩小的动画,在上述代码后面接着添加如下代码:


let scale = CABasicAnimation(keyPath: "transform.scale")
scale.fromValue = 1
scale.toValue = 0.1
scale.duration = 1
scale.repeatCount = HUGE
circle.addAnimation(scale, forKey: nil)

首先创建一个按比例缩放类型的动画,设置起始比例为1,也就是当前大小。再设置希望缩放到的比例为0.1。动画持续时间为1秒,重复无限次。最后将该动画添加在小圆点中。编译运行看看效果:

ReplicatorAnimation-14

目前每个小圆点是同时执行动画,我们需要设置小圆点的动画延迟时间,接着添加如下代码:


replicatorLayer.instanceDelay = 1/15

这里为什么是1/15呢,因为整个动画的时间是由每个小圆点的动画时间决定的,这里也就是1秒,所有小圆点的延迟时间加起来要等于整个动画的持续时间,所以这里就是用1秒除以小圆点的数量15。编译运行看看效果:

ReplicatorAnimation-15

从效果图中可以看到,刚开始的动画不是很自然,那是因为小圆点的初始比例是1,所以一开始会先看到小圆点,然后才会慢慢开始正常的动画。这个问题很好解决,我们让小圆点的初始比例为0.1,也就是刚开始看不到小圆点,这样就可以避免这个情况了,我们接着加一行代码:


circle.transform = CATransform3DMakeScale(0.01, 0.01, 0.01)

同时将replicatorLayer的背景色改为无色,再次编译运行看看效果:

ReplicatorAnimation-16

展现在我们眼前的是一个完美的加载动画。

总结

今天主要给大家介绍了CAReplicatorLayer的用法,大家领悟其核心功能复制、延迟后也可以尝试实现其他有趣的动画效果。下篇文章我会向大家介绍CALayer另一个子类的用法,敬请期待吧。

本文首发CSDN,如需转载请与CSDN联系。

View vs. Layer

这篇文章通过两个加载动画向大家介绍CALayer的动画。按照面向对象的思想说,Layer其实就是一个模型类,像Java里的POJO类一样,它包含若干属性,并没有任何处理逻辑的方法,这些属性影响着显示在Layer中的内容。我们先来看看UIView和CALayer之间有什么区别和联系。

  • 联系:Layer是View背后的那个女人。每一个UIView后面都有对应的CALayer,大家看到的在UIView中显示的内容其实是在CALayer中。
  • 区别:
    • View有复杂的、各种组合的布局机制。Layer只有极简单的布局。
    • View可以响应用户交互。Layer不能响应用户交互。
    • View中的绘画逻辑有CPU执行。Layer中的绘画直接有GPU执行。
    • View有丰富的、功能强大的子类。Layer只有很少的几个子类。
    • View动画属性较少,局限性较大。Layer由于更底层、动画属性更多,所以可以实现出更灵活、更丰富的动画。

第一个CALayer动画

Layer动画系列的文章,我不准备系统的从简单到复杂的知识进行讲解,我会通过各种实战示例,示例中用到什么知识点就讲什么知识点。

第一个动画让我们来实现Google Chrome浏览器加载时页签上的等待动画:

GoogleLoadingAnimation-1

新建项目GoogleLoading,打开Main.storyboard,拖拽一个UIView到ViewController中,添加好约束,自行设置ViewController和UIView的背景色,这里UIView的背景色我设置为无色:

GoogleLoadingAnimation-2

然后添加该View的Outlet到ViewController中,这个UIView就是要展示加载动画的View:


@IBOutlet weak var loadingView: UIView!

打开ViewController.swift,申明一个常量属性ovalShapleLayer


let ovalShapeLayer: CAShapeLayer = CAShapeLayer()

ovalShapleLayer的类型是CAShapleLayer,它是CALayer的为数不多的子类之一。它的作用是在屏幕上画出各种形状,不论是简单的圆形、方形还是复杂的五角星或不规则图形都难不住它。CAShapeLayer有如下一些主要属性:

  • strokeColor:笔画颜色。
  • strokeStart:笔画开始位置。
  • strokeEnd:笔画结束位置。
  • fillColor:图形填充颜色。
  • lineWidth:笔画宽度,即笔画的粗细程度。
  • lineDashPattern:虚线模式。
  • path:图形的路径。
  • lineCap:笔画未闭合位置的形状。

我们之所要申明一个CAShapeLayer,是因为要用它在屏幕上画出一个圆形。下面在viewDidLoad()方法中添加如下代码:


ovalShapeLayer.strokeColor = UIColor.whiteColor().CGColor
ovalShapeLayer.fillColor = UIColor.clearColor().CGColor
ovalShapeLayer.lineWidth = 7

这几个属性刚才已经向大家介绍过了,这三行代码的意思是我们画出的圆形笔画颜色是白色,没有填充色,笔画的宽度为7。接着我们申明这个圆形的半径,使这个圆形的大小为容纳它视图大小的80%:


let ovalRadius = loadingView.frame.size.height/2 * 0.8

最后我们设置ovalShapeLayer的路径,这是最关键的一步,因为你要告知CAShapeLayer按照什么路径绘制图形,让我们接着添加如下代码:


ovalShapeLayer.path = UIBezierPath(ovalInRect: CGRect(x: loadingView.frame.size.width/2 - ovalRadius, y: loadingView.frame.size.height/2 - ovalRadius, width: ovalRadius * 2, height: ovalRadius * 2)).CGPath

这里出现了新面孔UIBezierPath,它可以创建基于矢量的路径,是Core Graphics框架关于path的封装。UIBezierPath可以定义简单的形状路径,如椭圆、矩形,或者有多个直线和曲线段组成的形状。在这里我们要使用它的初始化方法init(ovalInRect rect: CGRect)定义一个正圆的路径。设置完路径后,将ovalShapeLayer添加到loadingView视图的Layer中,它就可以按照设定好的路径在loadingView中绘制图形了:


loadingView.layer.addSublayer(ovalShapeLayer)

编译运行看看效果:

GoogleLoadingAnimation-3

完美的一个圆形。接下来我们要做的是让这个圆只显示一部分,因为Google的加载动画只有大概五分之二的圆形轮廓。让我们继续将目光集中在viewDidLoad()方法中,在loadingView.layer.addSublayer(ovalShapeLayer)这行代码上面添加另一行代码:


ovalShapeLayer.strokeEnd = 0.4

上面的代码将ovalShapeLayerstrokeEnd属性设置为0.4,意思是ovalShapeLayer在绘制圆形时只画整个圆形的五分之二,即笔画结束的位置在整个圆形轮廓的五分之二处。编译运行看看效果:

GoogleLoadingAnimation-4

看来是我们想要的效果,但是仍有一处细节需要我们完善,看看Google的加载动画,蓝色的部分圆形轮廓两头是圆形的,而我们的圆形轮廓两头是方形的。这个问题很好解决,仍然在loadingView.layer.addSublayer(ovalShapeLayer)这行代码上面添加一行代码:


ovalShapeLayer.lineCap = kCALineCapRound

这行代码的意思是将笔画两头的形状设置为圆形,对应的还有两个常量kCALineCapButtkCALineCapSquare,大家可以试试。再次编译运行看看效果:

GoogleLoadingAnimation-5

到目前为止,我们通过CALayer绘制出了动画的主体,接下来要让它动起来。在ViewController.swift中添加beginSimpleAnimate()方法:


func beginSimpleAnimation() {
let rotate = CABasicAnimation(keyPath: "transform.rotation")
rotate.duration = 1.5
rotate.fromValue = 0
rotate.toValue = 2 * M_PI
rotate.repeatCount = HUGE
loadingView.layer.addAnimation(rotate, forKey: nil)
}

在这个方法中,我们又看到了新面孔CABasicAnimation,该类提供了基本的、单关键帧的Layer属性动画,通过animationWithKeyPath:初始化方法,根据keyPath创建不同的CAPropertyAnimation实例。常用的keyPath有如下一些:

  • transform.rotation:旋转动画。
  • transform.ratation.x:按x轴旋转动画。
  • transform.ratation.y:按y轴旋转动画。
  • transform.ratation.z:按z轴旋转动画。
  • transform.scale:按比例放大缩小动画。
  • transform.scale.x:在x轴按比例放大缩小动画。
  • transform.scale.y:在y轴按比例放大缩小动画。
  • transform.scale.z:在z轴按比例放大缩小动画。
  • position:移动位置动画。
  • opacity:透明度动画。

以上只是一部分常用的动画keyPath,更多的希望大家在实际运用中去挖掘。在beginSimpleAnimation()方法中,我们使用了transform.rotation,创建了一个旋转动画的实例,然后给该动画设置了四个属性:

  • duration:动画持续时间。
  • fromValue:动画起始值。
  • toValue:动画结束值。
  • repeatCount:重复次数。

该方法设置这几个属性的含义为使动画主体不停的旋转,旋转一圈的时间为1.5秒。以上这几个概念在UIView的动画中同样存在,大家应该都已经比较熟悉了。然后使用Layer的addAnimation(anim: CAAnimation, forKey key: String?)方法将旋转动画实例添加到目标Layer中,该方法的key是用来标示添加的动画,便于以后重复使用时能方便的检索,如果没有需求可以传值nil。最后viewWillAppear方法中调用beginSimpleAnimation()方法:


override func viewWillAppear(animated: Bool) {
beginSimpleAnimation()
}

编译运行看看效果:

GoogleLoadingAnimation-6

至此我们的第一个简单的CALayer动画就完成了,在下一节我们一起实现一个更加有意思的加载动画,从而向大家介绍新的动画类型及动画组合。

Stroke Animation与Animation Group

让我们先看看要实现的效果:

GoogleLoadingAnimation-7

这种加载动画在很多应用中都出现过,比如网易新闻、Win版的谷歌浏览器中都有使用。下面就让我们一步一步来实现吧,首先打开Main.storyboard,新添加一个UIView,在ViewController.swift中添加Outlet:

GoogleLoadingAnimation-8

然后定义一个新的CAShapeLayer:


let anotherOvalShapeLayer: CAShapeLayer = CAShapeLayer()

viewDidLoad()方法中对它进行设置,并将其添加到刚才创建的complexLoadingView中:


anotherOvalShapeLayer.strokeColor = UIColor.whiteColor().CGColor
anotherOvalShapeLayer.fillColor = UIColor.clearColor().CGColor
anotherOvalShapeLayer.lineWidth = 7

let anotherOvalRadius = complexLoadingView.frame.size.height/2 * 0.8
anotherOvalShapeLayer.path = UIBezierPath(ovalInRect: CGRect(x: complexLoadingView.frame.size.width/2 - anotherOvalRadius, y: complexLoadingView.frame.size.height/2 - anotherOvalRadius, width: anotherOvalRadius * 2, height: anotherOvalRadius * 2)).CGPath
anotherOvalShapeLayer.lineCap = kCALineCapRound

complexLoadingView.layer.addSublayer(anotherOvalShapeLayer)

这些操作在上一个动画都已经做过一遍了,这里就不再解释。编译运行看看是否屏幕上又出现了一个圆圈呢:

GoogleLoadingAnimation-9

接下来在ViewController.swift中添加一个方法beginComplexAnimation()


let strokeStartAnimate = CABasicAnimation(keyPath: "strokeStart")
strokeStartAnimate.fromValue = -0.5
strokeStartAnimate.toValue = 1

let strokeEndAnimate = CABasicAnimation(keyPath: "strokeEnd")
strokeEndAnimate.fromValue = 0.0
strokeEndAnimate.toValue = 1

let strokeAnimateGroup = CAAnimationGroup()
strokeAnimateGroup.duration = 1.5
strokeAnimateGroup.repeatCount = HUGE
strokeAnimateGroup.animations = [strokeStartAnimate, strokeEndAnimate]
anotherOvalShapeLayer.addAnimation(strokeAnimateGroup, forKey: nil)

这里出现了两个新的动画类型,笔画开始动画和笔画结束动画,我们虽然使用CAShapeLayer绘制了一个圆圈,但是它也存在笔画起始位置和笔画终止位置,只不过它俩在同一个位置而已,笔画动画的位置取值在0-1之间,0代表绘制路径的起始位置,1代表绘制路径的终止位置。所以strokeStartAnimate动画让绘制圆圈的笔画起始位置从-0.5开始,目的是让笔画起始绘制时等待一段时间,也就是起始位置延迟绘制。而strokeEndAnimate动画让绘制圆圈的笔画终止位置正常的从0绘制到1。这样一来笔画两头绘制的时间就会不一样,会有一个时间差,这样就有圆圈不断绘制又不断被擦除的效果。

strokeStartAnimatestrokeEndAnimate是两个动画,如何作用于一个Layer上呢?这时就要用到CAAnimationGroup,顾名思义它是将多个动画组成一个组,在一个动画组里,子动画会同时进行。动画组可以设置动画持续时间、重复次数以及子动画数组。最后将动画组加在Layer上即可。

最后在viewWillAppear()方法中调用beginComplexAnimation()方法:


override func viewWillAppear(animated: Bool) {
beginSimpleAnimation()
beginComplexAnimation()
}

编译运行看看效果:

GoogleLoadingAnimation-10

总结

CALayer动画可以实现比UIView动画更丰富、更底层、效率更高的动画。但是在实际的应用开发中,我们应该按需所用,能用UIView动画实现的我们就可以不用CALayer动画,它俩没有谁优谁劣之分。这篇文章只是CALayer动画的引子,让大家对CALayer动画有初步的了解和认识,之后我在文章中会通过更多的实例帮大家更深入的认识CALayer动画,从而提升自己应用的用户体验。

本文首发CSDN,如需转载请与CSDN联系。

前言

自适应、适配、布局这几个关键词一直伴随着iOS开发,从以前的单一尺寸屏幕,到现在的多尺寸屏幕,Apple一直致力于让开发人员尽可能少在这些事上耗费过多的精力,所以Apple在2012年推出了Auto Layout特性,2014年又推出了Adaptive Layout、Size Classes,2015年又推出了Stack View。这些无一不是我们开发者做适配的利器。今天就让我们看看StackView是怎么一回事。

注:示例开发环境为Xcode7 Beta2

我们先来看看下面这个常见的布局:

StackView-1

这是一种很常见的汉堡布局,如果我们需要让它自适应不同尺寸的屏幕,我们要添加哪些约束呢?

StackView-2

看看图中这些约束,你们会不会感到莫名的烦躁呢,至少我是的。更让人抓狂的是,如果需要在中间再添加一个长方形,那么你要修改关联的好几个约束,有没有掀桌的冲动呢?

不过,我们开发人员总是幸运的,Apple在Xcode7中添加了一个新的特性StackView,它能完美的解决这个闹心的问题。

StackView其实一个视图容器,不过它会对它的子视图根据一定规则自动布局,将子视图按栈的排列方式进行布局,并且有几个主要的属性:

  • 方向
    StackView有水平和垂直两个方向的布局模式:
    StackView-3

  • 间隔
    StackView可以设置子视图之间的间隔:
    StackView-4

  • 对齐方式
    StackView可以设置子视图的对齐方式(水平方向和垂直方向的该属性值有所区别):
    StackView-5

    • Fill:子视图填充StackView。
    • Leading:靠左对齐。
    • Trailing:靠右对齐。
    • Center:子视图以中线为基准对齐。
    • Top:靠顶部对齐。
    • Bottom:靠底部对齐。
    • First Baseline:按照第一个子视图中文字的第一行对齐。
    • Last Baseline:按照最后一个子视图中文字的最后一行对齐。
  • 分布比例
    StackView可以设置子视图的分布比例:
    StackView-6

    • Fill:默认分布方式。
    • Fill Equally:子视图的高度或宽度保持一致。
    • Fill:Proportionally:StackView自己计算出它认为合适的分布方式。
    • Equal Spacing:子视图保持同等间隔的分布方式。
    • Equal Centering:每个子视图中心线之间保持一致的分布方式。

一切让示例来说话,在这篇文章中我们将逐步开发一个电影影评的应用来详细讲解如何使用StackView。

第一个StackView

打开Xcode,新建一个项目,命名为MovieRate,语言选择Swift。打开Storyboard,删掉当前的ViewController视图,重新拖入TableViewController视图,然后内嵌一个NavigationController,配色大家随意:

StackView-7

然后再删掉初始的ViewController.swfit文件,新建一个UITableViewController名为MovieListTableViewController.swift,以及一个UITableViewCell名为MovieTabelViewCell.swift

StackView-8

这两个文件的作用不言而喻:

StackView-9

接下来新建一个plist文件Movie.plist,当做TableView的数据源:

StackView-10

然后打开StoryBoard,选择MovieListTableViewController,给MovieTableViewCell设置一个标示符MovieCell,往MovieTableViewCell中拖入两个UIImageView和一个UILabel,不用理会他们的位置和尺寸:

StackView-11

按着command键,同时选中这三个视图,然后点击右下角的Stack按钮:

StackView-12

这时,我们便创建的一个UIStackView,包含刚才选中的那三个视图,或者说我们将选中的那三个视图组合成了一个UIStackView:

StackView-13

选中这个StackView,打开右侧的属性编辑窗口,可以看到在文章前言中讲过的四个主要属性:

StackView-14

虽然被包含的两个的UIImageView和UILabel可以由UIStackView的属性设置布局,但是就UIStackView本身而言,我们还是需要设置它的相关约束:

StackView-15

这两个UIImageView一个展示电影封面图片,一个展示评分,我们在StoryBoard中设置它们的image属性,并将Model设置为Aspect Fit,看看效果:

StackView-16

理想总是很丰满,现实总是很骨感,设置了图片后并不是我们想要的结果,图片的大小以及与文字的间隔都有问题。而且还出现了约束错误:

StackView-17

意思是StackView里的这三个内容没有指定x坐标的约束或者宽度的约束。但是作为StackView的子视图,它们的布局情况是由StackView通过各个属性设置的,断然不能在自身上添加约束,那么如何解决这个问题呢?其实很简单,我们选中中间的UILabel,然后打开右侧的尺寸属性面板,找到这两栏:

StackView-18

Content Hugging Priority是水平、垂直方向的拉伸优先级,我们将Horizontal设置为250。这样就可以告诉StackView,中间的UILabel水平拉伸的优先级是最高的,当StackView的宽度变大时优先拉伸UILabel的宽度,这样前后两个UIImageView的宽度是保持不变的,否则会默认拉伸StackView中的第一个子视图,所以我们看到第一个UIImageView的宽度被拉伸了。

Content Compression Resistance Priority是水平、垂直方向的压缩优先级,道理和上面的拉伸优先级是一样的,我们将UILabel的水平压缩优先级设为749。

现在你们会发现约束错误消失了,第一个UIImageView的宽度也正常了,而中间的UILabel被拉伸了:

StackView-19

接下来在MovieTableViewCell中添加这两个UIImageView和UILabel的IBOutlet

StackView-20

然后在MovieListTableViewController中实现TableView的数据展示:

StackView-21

然后编译运行程序看看效果:

StackView-22

横屏也很完美:

StackView-27

一个完美的Movie List页面已经展现在我们眼前,电影封面、电影名称、电影评分被StackView按合理的布局组织在TableViewCell里,我们只是给StackView设置了简单的四个约束而已。可见StackView在视图布局方面可以给我们带来很大的便利。

StackView的嵌套

既然UIStackView继承了UIView,那么UIStackView是否可以看做是一个UIView而被包含在UIStackView内呢?答案是肯定的,这一节向大家介绍UIStackView的嵌套。

一些界面发杂的App,只要你仔细分析,总会发现StackView嵌套的布局,或看似相似的布局:

StackView-23

在MovieRate这个示例中,我们将StackView的嵌套运用在电影详情页面里:

StackView-24

上图是电影详情页的布局图,从图中我们可以看到一共有三个StackView,一个嵌套在一个里面。我们在Storyboard中把这个页面创建出来(StackView的创建方法在上一节中已经介绍过了,这里就不再累赘了):

StackView-25

详情页的数据直接写在Storyboard中了,因为只是说明StackView的嵌套特性,所以界面比较粗糙。点击TableViewCell跳转到详情页的过程不再累赘了,编译运行看看效果:

StackView-26

虽然有点丑,但是这个页面展示出了StackView的嵌套特性。

总结

诚然UIStackView不是万能的,但是无疑它可以在布局和自适应方面给开发者带来便利,在恰当的情形下使用StackView可以事半功倍。而且因为UIStackView是UIView的子类,所以也可以将动画效果作用于UIStackView上,在方便布局之余还能提高用户体验,何乐而不为呢,那么让我们用起来吧。

本文首发CSDN,如需转载请与CSDN联系。

前言

有些时候大家可能会遇到制作复杂、具有连贯性UIView动画的需求,这时大家可能会使用在completion闭包中衔接一段一段的动画,使之成为一段连续的动画。

如果我们只是连接2个,或者3个动画,这种方式或许还行得通,但如果有更多的动画片段需要连接的时候,这种方式会带来灾难性的问题,你的代码会非常的冗余,不断的在completion闭包中嵌套代码,使代码维护起来相当的困难。所以今天向大家介绍能更好地实现这个需求的方法,Keyframe动画。

Keyframe动画可以让我们有效的拆分由若干段动画连接而成的复杂动画,可以较为精准的定义每段动画的起始点及持续时间,并且在代码组织方面也非常清晰。先看看今天要带大家实现的动画Demo:

使用场景

我们先来认识一下,在什么样的场景下需要使用Keyframe动画。如图下所示,这是一个由四段动画组成的一个复杂动画,让UIView沿着长方形的轨迹运动:

Keyframe-1

我们来看看用代码如何实现:


UIView.animateWithDuration(1, animations: {
view.center.x += 200.0
}, completion: { _ in
UIView.animateWithDuration(1, animations: {
view.center.y += 100.0
}, completion: { _ in
UIView.animateWithDuration(1, animations: {
view.center.x -= 200.0
}, completion: { _ in
UIView.animateWithDuration(1, animations: {
view.center.y -= 100.0
}, completion: nil)
})
})
})

通过上面的伪代码可以看到,我们使用了completion闭包的方式连接每一段的动画,代码看起来尚且算清晰,可读性也马马虎虎。但是大家想象一下,如果我们使UIView按照一个复杂的路线运行,这一段动画可能有十几、几十段动画组成的,那么如果再使用completion闭包这种方式连接,那代码是多么的惨不忍睹。幸好我们有Keyframe动画,下面就让我们来看看如何使用Keyframe动画。

Keyframe动画

首先我们会使用到UIView的另一个动画方法`animateKeyframesWithDuration(_: delay: options: animations: completion: )
`:


UIView.animateKeyframesWithDuration(2, delay: 0, options: [], animations: {
// add keyframes
}, completion: nil)

这个方法的几个参数与前几个使用过的动画方法参数一样。上面代码片段的意思是整个关键帧动画的持续时间为2秒、无延迟、无动画选项、执行完毕后无后续执行的代码。

注:该方法的动画选项不再是UIViewAnimationOptions,而是UIViewKeyframeAnimationOptions。具体的内容大家可以去查阅Apple的文档。

接下来我们要在animations闭包中添加关键帧了:


UIView.animateKeyframesWithDuration(2, delay: 0, options: [], animations: {
UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0.25, animations: {
view.center.x += 200.0
})
}, completion: nil)

addKeyframeWithRelativeStartTime(_: relativeDuration: animations: )是UIView添加关键帧的方法,该方法有三个参数:

  • startTime:关键帧开始时间,该时间是相对整个关键帧动画持续时间的相对时间,一般值在0到1之间。如果为0,则表明这一关键帧从整个动画的第0秒开始执行,如果设为0.5,则表明从整个动画的中间开始执行。
  • relativeDuration:关键帧持续时间,该时间同样是相对整个关键帧动画持续时间的相对时间,一般值也在0到1之间。如果设为0.25,则表明这一关键帧的持续时间为整个动画持续时间的四分之一。
  • animations:设置视图动画属性的动画闭包。

我们解释一下上面这段代码。整个关键帧动画的持续时间为2秒,第一个关键帧从第0秒开始,运行0.5秒结束。下面我们完成其他三个关键帧:


UIView.animateKeyframesWithDuration(2, delay: 0, options: [], animations: {
UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0.25, animations: {
view.center.x += 200.0
})
UIView.addKeyframeWithRelativeStartTime(0.25, relativeDuration: 0.25, animations: {
view.center.y += 100.0
})
UIView.addKeyframeWithRelativeStartTime(0.5, relativeDuration: 0.25, animations: {
view.center.x -= 200.0
})
UIView.addKeyframeWithRelativeStartTime(0.75, relativeDuration: 0.25, animations: {
view.center.y -= 100.0
})
}, completion: nil)

第二个关键帧的开始时间为0.25,也就是从整个动画时间的第0.5时开始执行,同样持续0.5秒。后两个关键帧的参数就不难理解了。

现在整个代码看起来非常整洁,条理清晰,可读性非常好,而且可以有更精确的控制。即使再多几个关键帧也同样可以从容应对。

关键帧动画不仅仅用于同一个视图的分段动画,也可使使用于不同视图的组合动画,由于我们还没讲到图层动画,所以,开篇的示例动画中就使用了关键帧动画实现了多个视图的组合动画。

示例动画

在这个示例中虽然看起来是一个纸飞机视图的连续动画,但其实是由三个纸飞机视图组合而成的:

Keyframe-2

从图中可以看到其实是有三个纸飞机视图,只不过在界面加载之前2号和3号纸飞机视图的透明度都是为零。

整个动画是由这三个纸飞机视图通过关键帧动画组合而成:

Keyframe-3

图中标示出了三个飞机视图的运行轨迹、视图大小、视图透明度的状态,我们来看看代码如何实现:


let zoomInScaleTransform = CGAffineTransformMakeScale(0.2, 0.2)
UIView.animateKeyframesWithDuration(3, delay: 0, options: [], animations: {
UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0.2, animations: {
self.customHeaderView.paperAirplane.center.x += self.view.frame.width
self.customHeaderView.paperAirplane.center.y += -180
self.customHeaderView.paperAirplane.transform = zoomInScaleTransform
})
UIView.addKeyframeWithRelativeStartTime(0.3, relativeDuration: 0.01, animations: {
self.customHeaderView.paperAirplaneOpposite.alpha = 1
self.customHeaderView.paperAirplaneOpposite.transform = zoomInScaleTransform
})
UIView.addKeyframeWithRelativeStartTime(0.3, relativeDuration: 0.5, animations: {
self.customHeaderView.paperAirplaneOpposite.transform = CGAffineTransformIdentity
self.customHeaderView.paperAirplaneOpposite.center.x -= self.view.frame.width
self.customHeaderView.paperAirplaneOpposite.center.y += 90
})
UIView.addKeyframeWithRelativeStartTime(0.9, relativeDuration: 0.01, animations: {
self.customHeaderView.paperAirplaneComeBack.alpha = 1
})
UIView.addKeyframeWithRelativeStartTime(0.9, relativeDuration: 0.2, animations: {
self.customHeaderView.paperAirplaneComeBack.center.x += 33
})
}, completion: { _ in
self.restorePaperAirplaneStatus()
})

大家看到这大家可能会有疑问了,三段动画怎么会有五个关键帧呢,我们来刨析一下:

  • 第一个关键帧:完成1号纸飞机视图运动到右上角并移出屏幕,视图逐渐变小的动画。该关键帧从整个动画的第0秒开始执行,持续时间为0.6秒。
  • 第二个关键帧:由于2号纸飞机视图的初始透明度为零,所以在第二个关键帧将透明度设为1,并且缩小视图。注意这两个动作需要在瞬间完成,所以relativeDuration设为0.01,一个极短的时间。开始时间为整个动画的第0.9秒开始,较第一个关键帧延迟0.3秒。
  • 第三个关键帧:与第二个关键帧同时开始执行,完成2号纸飞机视图从小变大、并且往左下角运动,一直移出屏幕。持续时间为1.5秒。
  • 第四个关键帧:与第二个关键帧作用相似,改变3号纸飞机视图的透明度,同样是在瞬间完成。
  • 第五个关键帧:与第四个关键帧同时执行, 完成向右移动的动画,持续0.6秒。

关键帧完成之后,在completion闭包中调用restorePaperAirplaneStatus()方法,恢复3个纸飞机视图的状态及位置,以便再次执行动画。

总结

大家在使用关键帧动画时,对于关键帧的开始时间和持续时间需要仔细设置,保证每个关键帧在合适的时间开始,执行恰当的持续时间。在必要时候也需要在关键帧里修改视图的一些状态,但要设置极短的持续时间,表示瞬间完成。

下一篇会向大家介绍在使用Auto Layout的情况下,如何通过约束实现动画,好了今天就先到这里吧。

参考文献来自:iOS Animations by Tutorials