原文: How To Make A View Controller Transition Animation Like in the Ping App
作者: Rounak Jain
译者:远的风景,あ夂寒ツ
匿名社交网络App Secret 制造商最近发布了一个新App叫做 Ping ,用户可以收到他们感兴趣内容的消息。
Ping突出的是主导主屏幕和菜单之间转场的动画,如图所示。
每次见到一个完美的动画,我都会思考要是我讲怎么样去在iOS上实现这个动画。
在这个教程中,你将会学习用Swift实现这个很酷的动画。在这个过程中,你将会用到图形图层、遮罩、theUIViewControllerAnimatedTransitioning协议,以及UIPercentDrivenInteractiveTransition类等等。
注意:本教程假定你了解基本的iOS开发和Swift语言。如果你是新手,可以参看我们网站上的 教程 。
在Ping中,动画发生在从一个控制器过渡到另一个控制器。
在iOS中,你可以通过将两个视图控制器放在一个UINavigationController中来自定义视图控制器之间的动画,并使用iOS7中的UIViewControllerAnimatedTransitioning协议来动画转场。
在以下内容中,你可以了解更多详细细节,不过本质上该协议允许你:
指定动画的时间
创建一个可以引用两个控制器的容器视图
实现任何你想到的动画
你将会用UIView动画或Core Animatio动画实现这些。
下面具体讨论怎么实现圆形的过度效果。
描述这个动画效果如下:
屏幕右上角有个圆形按钮可控制视图的出现。
换句话说,圆作为一个Mask显示边框内的东西,隐藏边框外的东西。
你可以在CAlayer上使用遮罩,并用其alpha channel决定要展示图层的哪个属性。
1的alpha值展示下面的图层内容,0的alpha值则隐藏下面的内容,中间部分局部地展示图层的内容,以下用图示解释这个意思:
现在你已经了解了遮罩,下一步要决定使用哪种遮罩。由于动画带有圆形遮罩,所以最自然的选择是CAShapeLayer。想要动画这个圆形,你需要简单增加圆形遮罩的半径。
注意:这个章节是为想从头构建项目准备的,如果你是一个资深的iOS开发者,你可以越过此章节直接从 Custom Animation section 开始。
在Xcode中选择File/New/Project新建工程,接选择 iOS/Application/Single View Application.
给工程命名为CircleTransition,选择开发语言为Swift,设备为iPhone。
打开Main.storyboard,你会发现有一个single view controller,但是过度效果需要几个控制器之间切换。
首先你要把控制器嵌入到导航控制器中,选定好视图控制器,选择 Editor/Embed In/Navigation Controller。
接下来需要隐藏导航栏,打开Xcode右边的工具栏选择第四个tab(Attributes Inspector )取消Shows Navigation Bar的选定框。
现在添加视图控制器到Storyboard(故事板)中,使用导航控制器水平地链接各个视图控制器。
选择一个新的视图控制器,在右边的工具面板中选择第三个tab(Identity Inspector),更改类类型为ViewController,这样就与Xcode创建的ViewController类文件一致了。
接下来,在每个视图控制器右上角添加一个按钮。双击每个按钮,按下退格键,将按钮的标题设置为空字符串。
设置每个按钮的背景为黑色。
现在用AutoLayout设置按钮的位置设置。选择第一个视图控制器的按钮,设置如下:
点击右边和顶部红色括号,设置为10
设置宽度和高度为44
设置Update Frames为Items of New Constraints
点击Add 4 Constraints,按钮的大小和位置就设置好了,重复为每个视图控制器设置按钮。
最后设置按钮的形状,通过设置corner radius把按钮形状设置为圆形。在右边的工具面板中选择第三个tab(Identity Inspector)中用 User Defined Runtime Attributes设置按钮的layer的cornerRadius参数。
运行的时候将会见到按钮形状为圆形,而在IB中按钮形状不是圆形。
现在给每个视图控制器设置不同的背景色。给第一个视图控制器设置绿色背景色,第二个设置黄色背景色。
给两个视图控制器都添加上image views,设置高宽都为300 points, 选择右下角的Align选项,让它们居中展示在父视图中,并选择Horizontal Center in Container和Vertical Center in Container.
通过Resolve Auto Layout Issues将它们的Frame调整到正确值,然后Update Frames,这是canvas看起来是这样的:
下载图片 iPhones 和 iPads 版,接着把它们赋给image view,并设置image view的content model为Aspect Fit。
设置完后canvas如下所示:
恭喜!你已经完成了App的框架。现在可以开始链接按钮的响应事件了。
右击第一个视图控制器的按钮,然后拖动action outlet到第二个视图控制器。
接着显示一个弹出的菜单:选择show。
当按钮被按下时,第二个视图控制器被push进来。
打开Xocde的右边the Attributes Inspector命名segue的identifier为 PushSegue .
编译并运行该应用程序以确保推出第二个视图控制器。
现在你已经将按钮连接在第二个视图控制器上,试试弹出视图控制器,所以你需要些一个在ViewController类中写一个方法:
@IBAction func circleTapped(sender:UIButton) { self.navigationController?.popViewControllerAnimated(true) }
同时添加一个弱引用的属性:
@IBOutlet weak var button: UIButton!
回到故事版中,对两个视图控制器如下操作:
右击按钮
拖曳Touch Up Inside圆形到视图控制器的顶部
右击按钮,同时拖住引用的outlet到每个view controller中,并链接到视图控制器的button属性。
编译并再次运行,现在你已经有了一个功能完善的push和pop动画。
如果你跳过了前面的章节,可以直接下载 starter project ,这样你可以直接使用完成配置的视图控制器和按钮。
自定义push或pop动画需要实现UINavigationControllerDelegate协议的animationControllerForOperation方法。
新建一个文件,命名NavigationControllerDelegate,实现UINavigationControllerDelegate协议。
class NavigationControllerDelegate: NSObject,UINavigationControllerDelegate { }
接着打开Main.storyboard , 把UINavigationControllerDelegate赋给storyboard的UINavigationController的委托。
要实现这一步,可在右边库中搜索object,并拖拽到左侧Navigation Controller Source的下面。
现在点击object,在右边Identity Inspector中,将其类更改为NavigationControllerDelegate.
接下来,右击左面板中的UINavigationController,将object赋给UINavigationController的委托,并将其委托属性拖拽到NavigationControllerDelegate 对象上:
返回NavigationControllerDelegate,并添加如下占位符方法:
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { return nil }
注意方法的主体是空的,某个时候你将会用到它。
该方法接受两个需要转场的视图控制器,这将返回一个实现UIViewControllerAnimatedTransitioning的对象。
所以你需要创建其中一个。想要完成这一步,你需要通过File/New/File创建一个新的Cocoa Touch类,并将其命名为CircleTransitionAnimator.
声明实现the UIViewControllerAnimatedTransitioning协议:
class CircleTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
接下来你要为协议添加所需方法:
添加第一个方法:
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval { return 0.5 }
在该方法中,你需要返回动画的持续时间。如果你希望动画持续0.5s,那可以返回0.5:
下一步,将该属性添加到类:
weak var transitionContext: UIViewControllerContextTransitioning?
你会需要它来储存转场上下文环境。
下一步添加第二个所需方法:
func animateTransition(transitionContext: UIViewControllerContextTransitioning) { //1 self.transitionContext = transitionContext //2 var containerView = transitionContext.containerView() var fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! ViewController var toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! ViewController var button = fromViewController.button //3 containerView.addSubview(toViewController.view) //4 var circleMaskPathInitial = UIBezierPath(ovalInRect: button.frame) var extremePoint = CGPoint(x: button.center.x - 0, y: button.center.y - CGRectGetHeight(toViewController.view.bounds)) var radius = sqrt((extremePoint.x*extremePoint.x) + (extremePoint.y*extremePoint.y)) var circleMaskPathFinal = UIBezierPath(ovalInRect: CGRectInset(button.frame, -radius, -radius)) //5 var maskLayer = CAShapeLayer() maskLayer.path = circleMaskPathFinal.CGPath toViewController.view.layer.mask = maskLayer //6 var maskLayerAnimation = CABasicAnimation(keyPath: "path") maskLayerAnimation.fromValue = circleMaskPathInitial.CGPath maskLayerAnimation.toValue = circleMaskPathFinal.CGPath maskLayerAnimation.duration = self.transitionDuration(transitionContext) maskLayerAnimation.delegate = self maskLayer.addAnimation(maskLayerAnimation, forKey: "path") }
逐步解读代码:
1.在超出该方法范围外保持对transitionContext的引用,以便将来访问。
2.创建从容器视图到视图控制器的引用。容器视图是动画发生的地方,切换的视图控制器是动画的一部分。
3.添加toViewController作为containerView的子视图。
4.创建两个圆形UIBezierPath实例:一个是按钮的尺寸,一个实例的半径范围可覆盖整个屏幕。最终的动画将位于这两个Bezier路径间。
5.创建一个新的CAShapeLayer来展示圆形遮罩。你可以在动画之后使用最终的循环路径指定其路径值,以避免图层在动画完成后回弹。
6.在关键路径上创建一个CABasicAnimation,从circleMaskPathInitial到circleMaskPathFinal.你也要注册一个委托,因为你要在动画完成后做一些清理工作。
接着在同一个类中执行animationDidStop()进行清理:
override func animationDidStop(anim: CAAnimation!, finished flag: Bool) { self.transitionContext?.completeTransition(!self.transitionContext!.transitionWasCancelled()) self.transitionContext?.viewControllerForKey(UITransitionContextFromViewControllerKey)?.view.layer.mask = nil }
第一行是告知iOS动画的完成。由于动画已经完成了,所以你可以移除遮罩。最后一步是实际使用CircleTransitionAnimator.
回到NavigationControllerDelegate.swift,并调整此前你添加的stub方法:
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { return CircleTransitionAnimator() }
简单调整后会返回一个新的CircleTransitionAnimator实例。编译并运行app,最终动画效果如下:
恭喜,现在你已经重制了Ping app中的动画。如果这是你想要的效果,那可以在此打住了,但是如果你想了解如何实现动画的交互,请继续阅读!
动画运行正常后,你可以将关注自定义视图控制器转场的另一个特性:交互手势。由于点击操作已经是很老套的了,所以你可以通过实现这个特性来增加UI的深度。
交互式手势从调用navigationController:interactionControllerForAnimationController:开始。这是一个UINavigationControllerDelegate方法,有望返回一个符合UIViewControllerInteractiveTransitioning的对象。
iOS SDK提供了UIPercentDrivenInteractiveTransition类,该类已经在这个协议中实现,并且为你做了不少交互式手势处理。
打开NavigationControllerDelegate.swift,并添加该属性和新方法:
var interactionController: UIPercentDrivenInteractiveTransition? func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return self.interactionController }
回头思考下这个轻扫返回手势,很明显,你需要一个手势识别器。
你将要为导航控制器添加手势识别器。你需要一个导航控制器的引用,打开NavigationControllerDelegate.swift并添加以下属性:
@IBOutlet weak var navigationController: UINavigationController?
打开Main.storyboard,右击左侧Navigation Controller Delegate object,将属性和导航控制器连接起来,然后从navigationController属性中 拖到storyboard中的导航控制器上。
返回NavigationControllerDelegate.swift并实现awakeFromNib():
override func awakeFromNib() { super.awakeFromNib() var panGesture = UIPanGestureRecognizer(target: self, action: Selector("panned:")) self.navigationController!.view.addGestureRecognizer(panGesture) }
这一步会创建UIPanGestureRecognizer,并将该对象添加到导航控制器的视图上,并得到panned:方法中的手势回调函数。
下一步,实现该方法:
//1 @IBAction func panned(gestureRecognizer: UIPanGestureRecognizer) { switch gestureRecognizer.state { case .Began: self.interactionController = UIPercentDrivenInteractiveTransition() if self.navigationController?.viewControllers.count > 1 { self.navigationController?.popViewControllerAnimated(true) } else { self.navigationController?.topViewController.performSegueWithIdentifier("PushSegue", sender: nil) } //2 case .Changed: var translation = gestureRecognizer.translationInView(self.navigationController!.view) var completionProgress = translation.x/CGRectGetWidth(self.navigationController!.view.bounds) self.interactionController?.updateInteractiveTransition(completionProgress) //3 case .Ended: if (gestureRecognizer.velocityInView(self.navigationController!.view).x > 0) { self.interactionController?.finishInteractiveTransition() } else { self.interactionController?.cancelInteractiveTransition() } self.interactionController = nil //4 default: self.interactionController?.cancelInteractiveTransition() self.interactionController = nil } }
代码分解如下:
.Began: 只要识别了手势,那么它会初始化一个UIPercentDrivenInteractiveTransition对象并将其赋给interactionController属性。
如果你切换到第一个视图控制器,它初始化了一个push,如果是在第二个视图控制器,那么初始化的是pop。Pop非常简单,但是对于push,你需要从此前创建的按钮底部手动完成segue.
反过来,push/pop调用触发了NavigationControllerDelegate方法调用返回self.interactionController.这样属性就有了non-nil值。
.Changed: 这种状态下,你完成了手势的进程并更新了interactionController.插入动画是项艰苦的工作,不过苹果已经做了这部分的工作,你无需做什么事情。
.Ended: 你已经看到了pan手势的速度。如果是正数,转场就完成了;如果不是,就是被取消了。你也可以将interactionController设置为nil,这样她就承担了清理的任务。
default: 如果是其他任何状态,你可以简单取消转场并将interactionController设置为nil.
构建并运行app,从左向右轻扫,你会看到相同的动画,但是是在你的手指控制之下。
这是 该教程的完整项目
希望你能喜欢这篇主要为了实现一个简单但非常酷的转场动画的文章。你可以在自己的app中实现类似Ping中的效果,也可以通过改变背景色和动画速度来更改其外观和整体感觉。
这篇文章中有不少东西,包括使用图形图层、遮罩、UIViewControllerAnimatedTransitioning协议、UIPercentDrivenInteractiveTransition类以及其他等。
如果有任何问题,欢迎交流。
若需转载,请写明来源和译者!