动画是通过动作解释我们的应用程序功能以及让用户满意的好方法。在正确的位置添加动画可以真正使UI看起来更加精致和美观。

但是创建出色的动画需要进行大量的调整和迭代,因为我们经常需要尝试不同的动画步骤,持续时间和曲线来让事情感觉恰到好处。

这就是为什么使用能够让我们轻松更改内容和调整参数的工具来构建动画非常重要的原因。本周,让我们来看看SpriteKit如何作为某种动画的工具。对于Swift,有更好的见解,想要更好的探讨,可以进入iOS技术群,一起探讨交流

基础

SpriteKit是Apple用于2D游戏开发的内置框架(自iOS 7起)。因此,虽然它主要针对制作游戏,但对于任何类型的2D绘图和动画来说,它也是一个非常好的工具。事实上,在WWDC 2017上,Apple透露他们实际上正在使用SpriteKit为Xcode中的内存调试器构建UI。

对于动画,SpriteKit在构建更复杂,自包含的场景时非常有用。例如,当您创建某种形式的全屏加载动画时,插图作为入职流程的一部分,或包含多个动画步骤的任何其他内容,并且不涉及直接设置视图和UI控件的动画。

举个例子,我们将构建一个包含4个表情符号的加载动画,它看起来像这样:

设置场景

所有SpriteKit内容都在场景中呈现,由SKScene类的实例管理。然后使用基于节点的系统定义内容,使您能够创建层次结构,就像使用UIViews或CALayers时一样。

您可以使用各种子类创建节点SKNode,例如SKSpriteNode基于精灵(图像)的内容或SKLabelNode文本内容。

最后,使用动作(由SKAction类表示)使节点在场景中执行各种动画(例如移动,缩放,旋转等)。

入门

让我们首先创建一个SKScene动画容器。我们给它一个从视图控制器视图的最小尺寸取得的正方形尺寸,并设置一个白色背景颜色:

extension AnimationViewController {
func makeScene() -> SKScene {
let minimumDimension = min(view.frame.width, view.frame.height)
let size = CGSize(width: minimumDimension, height: minimumDimension)
let scene = SKScene(size: size)
scene.backgroundColor = .white
return scene
}
}

你提出了一个SKScene使用an SKView(这是UIViewiOS上的一个子类)。我们将这个视图添加到视图控制器中,并设置其大小和中心点。最后,我们告诉它呈现我们的场景,如下所示:

class AnimationViewController: UIViewController {
private lazy var animationView = SKView()
override func loadView() {
super.loadView()
view.addSubview(animationView)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Make sure we don't recreate the scene when the view re-appears
guard animationView.scene == nil else {
return
}
let scene = makeScene()
animationView.frame.size = scene.size
animationView.presentScene(scene)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
animationView.center.x = view.bounds.midX
animationView.center.y = view.bounds.midY
}
}

添加节点

所以我们现在有一个要渲染的场景,以及一个将在视图控制器中显示它的视图。我们开始添加一些内容。如果您使用关键帧渲染图像和动画,则需要SKSpriteNode用于渲染。但是在这个例子中我们将坚持使用一些简单的表情符号,所以我们将使用SKLabelNode(基本上是SpriteKit的等价物UILabel)。

让我们首先创建一个扩展SKLabelNode,让我们渲染一个表情符号:

extension SKLabelNode {
func renderEmoji(_ emoji: Character) {
fontSize = 50
text = String(emoji)
// This enables us to move the label using its center point
verticalAlignmentMode = .center
horizontalAlignmentMode = .center
}
}

然后我们将在视图控制器上创建另一个扩展方法,以添加我们场景的所有表情符号(我们将从中调用makeScene()):

extension AnimationViewController {
func addEmoji(to scene: SKScene) {
let allEmoji: [Character] = [" ", " ", " ", " "]
let distance = floor(scene.size.width / 4)
for (index, emoji) in allEmoji.enumerated() {
let node = SKLabelNode()
node.renderEmoji(emoji)
node.position.y = floor(scene.size.height / 2)
node.position.x = distance * (CGFloat(index) + 0.5)
scene.addChild(node)
}
}
}

让我们动画吧!

有了所有的设置,让我们来到有趣的部分 - 实际上创建我们的动画。我们将通过告诉每个表情符号节点按比例放大然后退回来执行动画,稍微延迟取决于节点的索引:

func animateNodes(_ nodes: [SKNode]) {
for (index, node) in nodes.enumerated() {
// Offset each node with a slight delay depending on the index
let delayAction = SKAction.wait(forDuration: TimeInterval(index) * 0.2)
// Scale up and then back down
let scaleUpAction = SKAction.scale(to: 1.5, duration: 0.3)
let scaleDownAction = SKAction.scale(to: 1, duration: 0.3)
// Wait for 2 seconds before repeating the action
let waitAction = SKAction.wait(forDuration: 2)
// Form a sequence with the scale actions, as well as the wait action
let scaleActionSequence = SKAction.sequence([scaleUpAction, scaleDownAction, waitAction])
// Form a repeat action with the sequence
let repeatAction = SKAction.repeatForever(scaleActionSequence)
// Combine the delay and the repeat actions into another sequence
let actionSequence = SKAction.sequence([delayAction, repeatAction])
// Run the action
node.run(actionSequence)
}
}

上面的代码有效,但很难阅读和推理,特别是如果我们要删除评论。但好消息是我们可以轻松解决它!感谢Swift的令人敬畏的点符号语法,我们可以大大减少代码的冗长,并摆脱所有临时let任务:

extension AnimationViewController {
func animateNodes(_ nodes: [SKNode]) {
for (index, node) in nodes.enumerated() {
node.run(.sequence([
.wait(forDuration: TimeInterval(index) * 0.2),
.repeatForever(.sequence([
.scale(to: 1.5, duration: 0.3),
.scale(to: 1, duration: 0.3),
.wait(forDuration: 2)
]))
]))
}
}
}

我们现在可以通过调用上面的方法来启动我们的动画,其中所有场景的节点都在makeScene():

animateNodes(scene.children)

扭曲

现在我们有了易于阅读(和非常具说明性)的动画代码,我们可以开始使用它了。假设我们想要在我们的动画中添加一点扭曲(字面意思!),让我们的表情符号在它们向上和向下缩放的同时旋转360度。

我们要做的就是将缩放操作和旋转操作组合到一个组中,如下所示:

extension AnimationViewController {
func animateNodes(_ nodes: [SKNode]) {
for (index, node) in nodes.enumerated() {
node.run(.sequence([
.wait(forDuration: TimeInterval(index) * 0.2),
.repeatForever(.sequence([
// A group of actions get performed simultaneously
.group([
.sequence([
.scale(to: 1.5, duration: 0.3),
.scale(to: 1, duration: 0.3)
]),
// Rotate by 360 degrees (pi * 2 in radians)
.rotate(byAngle: .pi * 2, duration: 0.6)
]),
.wait(forDuration: 2)
]))
]))
}
}
}

这将给我们以下结果:

结论

正如您在上面的示例中所看到的,使用SpriteKit进行动画制作使我们能够编写非常容易改变,扩展和试验的声明性动画代码。它绝对不是动画的灵丹妙药,因为我们无法以`UIView`这种方式动画任何形式的动画,但对于自包含的基于场景的动画,我认为它是一个非常好的工具。