你有没想过,乔治·卢卡斯的太空史诗歌剧,其实,可以在 iOS 上实现?在银河系中黑暗与光明双方之间的永恒战役中,我们得到启发,并创建具有星球大战人物特色的梦幻般的 UI 动画。
创作我们自己的星球大战续集,对于我们最强的两种原力:设计和开发,都是个挑战。
设计星球大战动画
by Kostya Trundayev
在我开始制作动画的时候,我想对标准开关控件创作一种独一无二的交互逻辑,并扩展影响到整个屏幕。我想到了一个有趣的想法,让用户通过点击该开关,进行两种不同的外观的切换。但我该使用什么界面来让两种设定显得有意义呢?
我记得星球大战,尤其是星球大战系列游戏,用户可以加入光明或黑暗的一方。天行者与黑武士。光明与黑暗。没错!这个矛盾人格的想法是多么出色,实在无法用言语形容。
我已经花了几个小时研究界面,我为每个设定档创建了两种图片拼贴,并绘制自己的界面。我使用 Photoshop 创建设定档,然后在 Adobe After Effects 中添加动画。
在两种屏幕都准备好了后,我觉得若发布在 Dribbble 它们看起太朴素了。我瞥了一眼屏幕中的黑武士,我终于想到了!画面中黑武士试图用他的手触碰十字星座。我想,“要是屏幕不只是接近,而是碎成小块,就像被原力击中一样?”这听起来很酷!我研究了在 After Effects 中如何实现,并做出了正如我想象的效果。我使用了粉碎效果,它能让我通过运用许多不同的设置实现摇摇欲坠的效果。
所以,剩下来唯一要做的事情是循环动画,因为该组件在动画结束的后,没有可供用户着陆的屏幕,并且从那里可以通过设定档的设置回到屏幕。这就是为什么我用星球大战标志创建第三个屏幕的原因,一个“设置你的个人资料”的按钮和群星璀璨的天空。我在 CC 球状动作特效的帮助下制作了该星空。
我将视频放入设备模型中,并为 Dribbble 做了个短片。但故事还没有结束。你还是可以学到更多内容的,因为伟大的设计加上天才的开发才是真正的原力觉醒。
使用 Swift 开发星球大战动画
by Artem Sydorenko
有两件事情在星球大战动画中似乎很难实现(在 Github 上浏览):在主屏幕中满天飞的星星和碎成片片的视图。我在 CAEmitterLayer 的帮助下解决了第一个挑战,它是一个提供粒子发射系统的类。然后,我有一段相当艰苦的时间,就是试图将视图破碎的 4000 个碎片以不同的方向降落。
如何将星球大战击成碎片
为了将 UIView 打破成碎片,我用了下面的方法:snapshotViewAfterScreenUpdates() 和 resizableSnapshotViewFromRect (afterScreenUpdates:withCapInsets:)。我也使用了中间快照,它能让我更快创建创建小碎片(从快照创建快照比从原来视图制作非常小的快照要快得多):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
let fromViewSnapshot = fromView.snapshotViewAfterScreenUpdates(false) for x in CGFloat(0).stride(through: size.width, by: width) { for y in CGFloat(0).stride(through: size.height, by: height) { let snapshotRegion = CGRect(x: x, y: y, width: width, height: height) let snapshot = fromViewSnapshot.resizableSnapshotViewFromRect(snapshotRegion, afterScreenUpdates: false, withCapInsets: UIEdgeInsetsZero) containerView.addSubview(snapshot) snapshot.frame = snapshotRegion snapshots.append(snapshot) } } |
有 3 种方式可以实现向星球大战那样的动画:通过使用 UIDynamics、Core Animation (UIKit) 或 OpenGL。我们用了全部这些方法来制作了动画,让我们来看看结果。
扩展阅读:如何在 iOS 和 Android 上创建铡刀菜单动画。
用 UIDymanics 制作动画
UIDynamics 是我们第一只小白鼠。我认为这个工具可以帮我实现物体自然下落的效果。它是这样子的:
1 2 3 4 5 6 7 8 9 |
animator = UIDynamicAnimator(referenceView: containerView) for snapshot in snapshots { let push = UIPushBehavior(items: [snapshot], mode: .Instantaneous) push.pushDirection = CGVector(dx: randomFloatBetween(-0.15 , and: 0.15), dy: randomFloatBetween(-0.15 , and: 0)) push.active = true animator.addBehavior(push) } let gravity = UIGravityBehavior(items: snapshots) animator.addBehavior(gravity) |
但是事实证明物体物理运动计算是很耗 CPU 的(我们有大量的物体)。我们不得不同时移动 4000 个对象,UIDynamics 很难维持这样的负荷。
下面是我们的计算结果:
使用 UIKit (Core Animation) 制作动画
由于使用 UIDynamics 制作星球大战动画是低效的,我决定使用 UIView 将动画重写一遍:
1 2 3 4 5 6 7 |
UIView.animateWithDuration(duration, animations: { for view in snapshots { view.frame = view.frame.offsetBy(dx: randomFloatBetween(-200 , and: 200), dy: randomFloatBetween(fromView.frame.height, and: fromView.frame.height * 1.3) ) } }) |
在这种情况下,只有当我们将动画切割成碎片 CPU 才受到影响,这样会在动画开始时有个小延迟。但是,由于在 GPU 负荷,FPS(帧每秒)不能同时显示超过 1500 个视图。FPS 会被降到 26,但不能使动画达到我想要的 60 FPS 流畅度。
由于我们仍不能得到满意的结果,我们决定使用 OpenGL 制作星球大战动画。
使用 OpenGL 制作动画
多数 iOS 教程都是用 Objective-C 设计的,所以我决定用它来开始,如果成功实现,我会使用 Swift 再重写一遍动画(我做到了)。
结果是惊人的,我可以在 iPhone 6 上以 60 帧每秒播放视图碎片动画,而且没有任何中断或延迟。
我想:如果我把动画切碎成 40,000 块而不是 4,000 块呢?OpenGL 能应对这种负荷吗?通过实验该数字,我得到了 4 FPS。为了跟踪代码表现不佳的部分,我用 time profiler 来运行动画。这能让我看到负载并显示花费最长时间实现的方法。
在 time profiler 下,动画运行在 23 FPS,但这很容易解释。Time profiler 执行的是发布配置,这意味着它可以优化正在编译的代码。
我重写了关键代码(我将 BlowSprite 的类改成结构体并替换了映射的周期)后,我可以在发布版本配置上达到 60 FPS。我让动画快了两倍有多!(在没有编译优化的调试配置上, 我得到 11 FPS)。
扩展阅读:如何开发 Android 上的 Instagram 视频
在实现了 3 种类型的动画后,我们可以从下面的统计图看到 UIDynamics、UIKit (Core Animation) 和 OpenGL 的实验结果。只有 OpenGL 动画可以应付负载需求,所以选择他来实现我们的组件。
我们对 CPU 的实验结果如下所示。
在帧每秒的表现上:
如何在你的项目中添加星球大战?
单实现 UIViewControllerTransitioningDelegate 类,可以返回我们的动画构成方法 animationControllerForDismissedController,然后赋值给 viewController 你想排除的 transitioningDelegate。
1 2 3 4 5 6 7 8 |
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { let destination = segue.destinationViewController destination.transitioningDelegate = self } func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { return StarWarsGLAnimator() } |
如何自定义组件
在星球大战动画中有两件事情你可以自定义:持续时间和碎片尺寸。让我们来看看你可以怎么做。
持续时间的部分代码:
1 2 3 4 5 |
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { let animator = StarWarsGLAnimator() animator.duration = 2 return animator } |
碎片尺寸,改变各部分点的尺寸:
1 2 3 4 5 |
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { let animator = StarWarsGLAnimator() animator.spriteWidth = 8 return animator } |
你可以在这些地方查阅我们的星球大战组件:
愿原力与你同在!