iOS动画教学:自定义视图控制器转场动画
是否当你想要显示摄像视图控制器,地址栏或者你自定义的场景控制器时,你都是在调用相同的UIKit方法:presentViewController(_:animated:completion:)。这个方法“丢掉”当前的视图控制器从而转向其他的控制器。
默认的转场动画简单地从当前的视图滑入一个新的视图,下面这个图展示了如何从当前联系人列表滑入展示一个“新的联系人”视图控制器。
在这个教程里,你将会创建自定义转场控制器用以替代默认的动画,使得教程的初始项目变得更加有生气。
开始
你可以从初始项目这里下载,解压zip文件和打开Beginner Cook项目,选择Main.Stroyboard开始教程:
第一个视图控制器(ViewController)包含了app的标题,主要的描述还有一个scroll View在底部用于展示可用的药草列表。
当点击列表当中的任意一张图片时,HerbDetailSViewController作为主要的控制器展示。这个视图控制器图片作为背景,有标题,文字描述和一些按钮用作指示图片的拥有者。
项目里ViewController.swift和HerbDetailsViewController.swift有足够的代码支撑这个小项目。构建和运行项目你会看到如下的效果图:
随意点击其中一张药草的图片,详情页面会通过标准的转从动画(从底部滑动)展示出来。这说明你的app是完好的,但是你的药草能够展示得更好!
你的工作是为你的app添加自定义的转场使其绽放!你将会更换当前的动画,使其变成一点击药草图片即会伸展铺满整个屏幕如下图这样:
卷起的衣袖,戴上你开发者的围裙,准备为你的app做一个自定义转场控制器吧!
在自定义场景过渡的背后
UIKit允许你自定义你的视图控制器展示方式通过协议的模式;你需要使你主控制器(或者其他你为了展示而创建的类)适应UIViewControllerTransitionDelegate.
每一次你展示一个新的视图控制器,UIKit会向协议请求是否应该使用一个自定义的转换。这是你自定义转场动画的第一步,如下图:
UIKit调用animationControllerForPresentedController(:_ presentingController:sourceController:); 如果这个方法返回nil,UIKit会使用内建的转场方法。如果UIKit在这个方法接收到一个对象,UIKit会使用这个对象用作转场。
在UIKit能够使用自定义动画控制器之前还有一些步骤:
UIKit在转场时间内会首先请求你的动画控制器(你可以理解为动画设计者)然后会调用他的animateTransition(),这里就是进行你自定义动画的关键步骤。
在animateTransition(),你会使用当前的视图控制器和将要显示的视图控制器,你可以渐隐,缩放,旋转和任意操控存在和即将显示的视图。
现在你已经学会了一些关于自定义转场控制器的工作原理,你可以开始做一个你自己的转场控制器了。
实现过渡协议
由于代理的任务是用于管理展示动画设计者对象,所以在你编写代理的代码之前你需要先创建一个动画设计者的类。
从Xcode的主菜单选择 File\New\File...然后选择模版 iOS\Source\Cocoa Touch Class
设置新建类为NSObject的子类,名称为PopAnimator。
打开PopAnimator.swift然后使其遵循UIViewControllerAnimatedTransitioning protocol如下:
class PopAnimator: NSObject, UIViewControllerAnimatedTransitioning {
}
然后你会看到Xcode会向你抱怨,因为你还没有实现代理的必须方法,所以接下来把它们实现。
添加以下方法到类内:
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?)-> NSTimeInterval {
return 0
}
返回0仅仅是作为一个转场时间占位值,在后面你可以自行替换这个值。
继续添加一下方法到类内:
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
}
上面的函数指示你的动画代码;添加完之后Xcode不会再向你提示错误。现在你有了动画设计者的类,接下来该接续在视图控制器实现另外一个协议方法。
打开ViewController.swift添加下列扩展:
extension ViewController: UIViewControllerTransitioningDelegate {
}
这段代码指示出你的视图控制器是遵循该协议的,待会你会添加一些方法的实现。
在类内找到didTapImage()这个方法,你会看到展现细节视图控制器的代码。herbDetails是这个新的视图控制器的实例;你需要设置他的转场协议代理给主视图控制器。
在刚才的方法的最后一行添加这段代码然后再presentViewController:
herbDetails.transitioningDelegate = self
现在每当ViewController展示细节视图控制器时,UIKit都会向它请求一个动画设计者对象。但是,你现在还没有实现UIViewControllerTransitioningDelegate的方法,所以目前为止还是使用默认的过渡方法。
下一步就是创建一个动画设计者对象,并当UIKit请求时返回给它。
为ViewController添加一个新的属性:
let transition = PopAnimator()
这是一个用于驱动你的视图控制器动画过渡的PopAnimator实例。只要你希望的过渡的动画是一样的,你便能一直使用这个实例来过渡新的视图控制器。
接下来实现扩展里面的代理方法:
func animationControllerForPresentedController(
presented: UIViewController,
presentingController presenting: UIViewController,
sourceController source: UIViewController) ->
UIViewControllerAnimatedTransitioning? {
return transition
}
这个方法的参数能够提供给你判断是否使用特别的过渡动画。在这个教程里你只会返回唯一的这个PopAnimator实例。
你已经实现了过渡到其他控制器的方法,但是你是否也想要处理控制器消失的那一个方法?
添加下面这个方法能解决上面的问题:
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return nil
}
这个方法和之前那个方法是类似的,你能够通过检查参数来获知是哪个控制器将要消失掉,以及是否需要使用自定义的过渡动画。暂时来说我们先返回nil,在往后再实现这个小时的动画。
你已经实现了一个自定义的动画设计者去管理你的控制器过渡,但是已经能工作了吗?
构建和运行你的项目然后轻触其中一张药草的图片:
创建你自己的过渡动画设计者
打开PopAnimator.swift;我们将要在这里添加视图控制器之间的过渡代码。
首先,添加以下的属性到类内:
let duration = 1.0
var presenting = true
var originFrame = CGRect.zero
duration会使用在好几个地方,例如:告知UIKit过渡持续的时间和当我们创建动画的时候也需要用到。
persenting会指示你的动画设计者是正在展现或者取消一个视图控制器。你需要时刻更新这个属性,因为是需要它指示动画是向前发展或者调转向后。
最后,你会使用originFrame去储存被点击图像的原始frame-你会从这个frame扩展到整个屏幕的尺寸,反之亦然。当你选择了图像,你需要获得该图像的frame然后传递给动画设计者这个实例。
现在你该继续实现UIViewControllerAnimatedTransitioning方法。
替代刚才写好的transitionDuration()实现:
return duration
使用duration属性使你能轻易试验和改变动画的过渡时间。
设置过渡的上下文
是时候添加一下酷炫的东西到animateTransition。这个方法有一个UIViewControllerContextTransitioning类型的参数用于你获得需要的参数和控制器之间的过渡。
在你开始写代码之前,你首先要明白什么是动画的上下文。
当两个控制器之间开始过渡的时候,当前存在的视图会被添加到一个过渡容器视图里,然后新的视图控制器会被创建,但是仍然未可见,就如下图所示:
因此你的任务是在animateTransition()里向过渡容器天价一个新的视图。使其“动画地进入”,然后有需要的话将旧的视图“动画地退出”。
默认来说,当动画完成时,旧的视图会被移出过渡容器。
添加一个酷炫的过渡
创建一个自己的过渡动画需要配合过渡上下文的“容器”视图来工作。这是一个临时的视图(更像是一个暂存器),它只会在过渡发生的时候才会添加的屏幕上。你能够在这个视图上创建你所有的动画。
把下面的代码插入到animateTransition():
let containerView = transitionContext.containerView()!
let toView =
transitionContext.viewForKey(UITransitionContextToViewKey)!
let herbView = presenting ? toView : transitionContext.viewForKey(UITransitionContextFromViewKey)!
你的动画会生存在containerView里,toView即是将要展示的新的视图。如果你是在将要显示的状态,herbView就是toView;能从上下文获得。不过在这个例子无论是展现或者消失,herbView总是你进行动画的对象。
当你展示细节视图控制器,它会扩张铺满整个屏幕,当它消息,它会缩少为原来图像的frame。
添加下面的代码到animateTransition():
let initialFrame = presenting ? originFrame : herbView.frame
let finalFrame = presenting ? herbView.frame : originFrame
let xScaleFactor = presenting ?
initialFrame.width / finalFrame.width :
finalFrame.width / initialFrame.width
let yScaleFactor = presenting ?
initialFrame.height / finalFrame.height :
finalFrame.height / initialFrame.height
上面这段代码,你检测得出初始和最后的动画frame然后计算出缩放的系数,用作待会的动画。
现在你应该认真地把心得视图摆放在被点击的图像上面;这会看起来像被点击的图片扩展到整个屏幕。
添加下面的代码到animateTransition():
let scaleTransform = CGAffineTransformMakeScale(xScaleFactor, yScaleFactor)
if presenting {
herbView.transform = scaleTransform
herbView.center = CGPoint(
x: CGRectGetMidX(initialFrame),
y: CGRectGetMidY(initialFrame))
herbView.clipsToBounds = true
}
当展示一个新的视图,你设置它的缩放比例和位置,使其尽量合适初始图片的frame。
现在添加最后一点代码到animateTransition():
containerView.addSubview(toView)
containerView.bringSubviewToFront(herbView)
UIView.animateWithDuration(duration, delay:0.0,
usingSpringWithDamping: 0.4,
initialSpringVelocity: 0.0,
options: [],
animations: {
herbView.transform = self.presenting ?
CGAffineTransformIdentity : scaleTransform
herbView.center = CGPoint(x: CGRectGetMidX(finalFrame),
y: CGRectGetMidY(finalFrame))
}, completion:{_ in
transitionContext.completeTransition(true)
})
首先我们会添加toView到container里面。接着,你需要确保herbView是在视图层的顶层,因为这是你想要。注意当消失视图控制器时,看第一行代码,toView是origin view(就是那个主视图),你的动画会因此被挡住,你需要把hertView的视图层往上提。
然后,你就能开始你的动画了-使用spring效果的动画会更好看!
在动画的展示过程中,你改变了herbView的大小比例和位置。当展现的时候,你从一个在底部的小尺寸扩展成整个屏幕的大小,因此变换是之前标记的变换。当消失时,你的动画需要缩小成原始图像的尺寸。
这里的重点:你设置新的控制器在被点击的图像上面;你使用动画变换着初始和结束的frame;最后,你调用completeTransition()方法提交这些逻辑给UIKit。现在是时候让你的代码动起来了!
构建和运行你的项目;点击第一个药草的图片你会看到你的视图控制器像你如期一样过渡。
现在你的动画开始在左下角,这是因为默认的originFrame的值是(0,0)-你还没有设置它。
打开ViewController.swift然后添加下面的代码到animationControllerForPresentedController()的顶部:
transition.originFrame =
selectedImage!.superview!.convertRect(selectedImage!.frame, toView: nil)
transition.presenting = true
这段代码会传递被你选中图像的frame给originFrame。然后设置presenting标志为true,然后在动画里被点击的图像会被隐藏。
再次构建和运行你的项目;试着点击列表里不同的药草图片,观察它们的过渡动画:
添加消失的过渡
剩下要做的工作就是消失细节控制器。你已经为动画设计者做了足够多的工作-过渡动画代码的逻辑适应了两种情况下的初始和消失的frame,因此你能非常轻易地完成剩下的工作。
打开ViewController.swift然后代替animationControllerForDismissedController() 这个方法的实现:
transition.presenting = false
return transition
这段代码时在告诉动画设计者对象:你正在消失一个视图控制器,该使用对应的动画代码去过渡。
构建和运行项目能够看到结构,点击一个药草图片然后再点击屏幕任意位置取消当前视图!
最后那一下点击的过渡有一点太过平整-你能动画地改变一下四角圆角半径使其平整地回到一开始的位置。
添加下面的代码到animateTransition:
let round = CABasicAnimation(keyPath: "cornerRadius")
round.fromValue = presenting ? 20.0/xScaleFactor : 0.0
round.toValue = presenting ? 0.0 : 20.0/xScaleFactor
round.duration = duration / 2
herbView.layer.addAnimation(round, forKey: nil)
herbView.layer.cornerRadius = presenting ? 0.0 : 20.0/xScaleFactor
当你展现细节视图控制器时,圆角半径从20.0到0.0,反之从0.0到20.0。一个友好的点击结束本节教程!
接下来做什么?
你可以从这里下载完成的项目。
之后,你可以继续提高这个项目。这里有一些主意:
在过渡的时候隐藏被点击的图片使其看起来更像生长到整个屏幕。
渐入和渐出药草的描述文字看来会更平整一点。
测试使过渡适应横屏。
更多处理过渡场景动画的章节在iOS Animations by Tutorials。在这本书里,你会学习到更多视图控制器的过渡展示,方向改变动画,导航控制器和交互过渡,等等。
我们希望你享受这个教程,如果你有任何问题或者建议,可以在下方留言!