我们在左一些app的时候经常会用到详情页,评价页 , 总之就是点一个按钮 就展示一些信息,在做一些简单的展示或者小逻辑。一般都会presentViewController。默认的动画是从下往上,但是我们想要自己主宰它的动画方式怎么弄呢?
本小节将会以一个点击获取英雄详情的demo来介绍一个自定义presentViewController动画(视图控制器切换动画)
本文源码:https://github.com/smalldu/IOS-Animations
中的AnimationDemo11
简单的效果
进阶效果
我们在左一些app的时候经常会用到详情页,评价页 , 总之就是点一个按钮 就展示一些信息,在做一些简单的展示或者小逻辑。一般都会presentViewController
大家可以下载我的代码,看看一些跟过渡动画没有关系的设置,比如文字,和半透明背景 ,下面UIScrollView等等 , 因为他们不是本节要介绍的重点,本节要介绍的重点是自定义过渡动画。
首先,创建一个Single View Application,然后在Main.storyboard中定义好搞两个界面,定义好约束 。 不懂的可以下载我源码。看源码上,也可以不搞这么复杂,随便搞两个页面 练习过渡动画就行。
我的页面结构
页面所有元素都是基于AutoLayout约束的
然后就是创建了一盒Hero.swift 用于存放英雄的基本信息 , 然后在ViewController中将这些英雄的图像加到UIScrollView上,计算好他们的位置。
每个图像都添加 imageView.userInteractionEnabled = true
属性,可交互,然后添加点击的手势
imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: Selector("didTapImageView:")))
然后就是在点击的时候展示详情页 , 详情页。就会传一个Hero对象的参数。没有什么非常特别的。
func didTapImageView(tap: UITapGestureRecognizer) {
selectedImage = tap.view as? UIImageView
let index = tap.view!.tag
let selectedHerb = he[index-1]
//present details view controller
let details = storyboard?.instantiateViewControllerWithIdentifier("detailViewController") as! DetailViewController
details.he = selectedHerb
presentViewController(details, animated: true, completion: nil)
}
这里我把第二个控制器的设置如图
这时候执行还是默认的效果
如果你的控制器要设置自己的动画需要实现UIViewControllerTransitioningDelegate协议。每次你present一个新的ViewController的时候,UIKit就会看这个delegate是否使用自定义过渡。
UIKit通过调用
animationControllerForPresentedController(:_presentingController:sourceController:);方法,如果这个方法返回nil ,就会调用默认的present 。如果返回的是一个非空对象,就会使用这个对象的控制过渡 ,这个对象必须是实现UIViewControllerAnimatedTransitioning协议的对象。
UIViewControllerAnimatedTransitioning这个有两个必须的方法
transitionDuration 这个方法需要提供返回一个时间,动画持续时间
animateTransition 这个是动画的主体方法
我们新建一个PopAnimation.swift的类让它继承NSObject ,然后实现UIViewControllerAnimatedTransitioning协议 实现了协议就自然要实现那两个方法,第一个方法简单返回一个时间就行了,这里暂且返回1。
第二个方法有传一个参数 transitionContext: UIViewControllerContextTransitioning
UIViewControllerContextTransitioning是个什么东西呢 ?
当两个ViewController之间过渡的时候,刚开始新的控制器已经被创建只是还不可见,因此你的任务就是在animateTransition()方法中把新的控制器添加到transition的容器中,把它以动画的方式添加进来 , 把旧控制器以动画方式移除去
transitionContext提供两个非常便捷的方法让你获得transition对象:
- viewForKey() :这个可以通过UITransitionContextFromViewKey 和
UITransitionContextToViewKey 获得新旧视图] - viewControllerForKey(): UITransitionContextFromViewControllerKey 和
UITransitionContextToViewControllerKey 获得新旧试图控制器
所以 我们这里先上一个最简单的动画
import UIKit
class PopAnimator: NSObject,UIViewControllerAnimatedTransitioning {
let duration = 1.0
//动画持续时间
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return duration
}
//动画执行的方法
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView()
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
containerView!.addSubview(toView!)
toView!.alpha = 0.0
UIView.animateWithDuration(duration,
animations: {
toView!.alpha = 1.0
}, completion: { _ in
transitionContext.completeTransition(true)
})
}
}
将它的不透明度由0变为1 ,然后在完成的时候调用动画完成方法
我们首先在ViewController中声明let transition = PopAnimator()
我们前面说了,我们的ViewController还需要实现UIViewControllerTransitioningDelegate协议
为了代码整洁,我们在ViewController最下面添加
extension ViewController:UIViewControllerTransitioningDelegate{
//Present的时候 使用自定义的动画
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return transition
}
//使用默认的动画
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return nil
}
}
这两个代理方法,第一个是Presentpresent返回我们自定义的,第二个暂且返回默认的。
最后别忘了加代理
func didTapImageView(tap: UITapGestureRecognizer) {
selectedImage = tap.view as? UIImageView
let index = tap.view!.tag
let selectedHerb = he[index-1]
//present details view controller
let details = storyboard?.instantiateViewControllerWithIdentifier("detailViewController") as! DetailViewController
print(details)
details.he = selectedHerb
details.transitioningDelegate = self //设置过渡代理
presentViewController(details, animated: true, completion: nil)
}
效果
这样一个简单的效果就实现了。
要实现复杂的效果,我们需要一些计算。首先在PopAnimation中新增两个变量
var presenting = true //是否在presenting
var originFrame = CGRect.zero
presenting主要用来区分是present还是dismiss
然后在animateTransition 中
let containerView = transitionContext.containerView()
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
let detailView = presenting ? toView :
transitionContext.viewForKey(UITransitionContextFromViewKey)!
前两个没变,后面一个变了,如果当前是present , detail就是toView如果不是detail就是from 。
然后加上下面三句
let initialFrame = presenting ? originFrame : detailView!.frame
let finalFrame = presenting ? detailView!.frame : originFrame
let xScaleFactor = presenting ?
initialFrame.width / finalFrame.width :
finalFrame.width / initialFrame.width
let yScaleFactor = presenting ?
initialFrame.height / finalFrame.height :
finalFrame.height / initialFrame.height
如果是present初始就是originFrame原如果不是初始就是detail的frame 。final同理
最后那个算出现在缩放比例
然后添加代码
let scaleTransform = CGAffineTransformMakeScale(xScaleFactor,
yScaleFactor)
if presenting {
detailView!.transform = scaleTransform
detailView!.center = CGPoint(
x: CGRectGetMidX(initialFrame),
y: CGRectGetMidY(initialFrame))
detailView!.clipsToBounds = true
}
定义一个变换,如果present的话 先把detail先缩放(按照山上面计算的比例缩放),然后设置center。为了定位到当前点击的小图的位置。
最后一段
containerView!.addSubview(toView!)
containerView!.bringSubviewToFront(detailView!)
UIView.animateWithDuration(duration, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0, options: UIViewAnimationOptions.AllowAnimatedContent, animations: {
detailView!.transform = self.presenting ?
CGAffineTransformIdentity : scaleTransform
detailView!.center = CGPoint(x: CGRectGetMidX(finalFrame),
y: CGRectGetMidY(finalFrame))
}) { (_) -> Void in
transitionContext.completeTransition(true)
}
第一句无可厚非,为啥要加第二句呢?containerView!.bringSubviewToFront(detailView!)
因为如果是present的话本来就应该放在最前面 ,如果是dismiss的话,不放在最前面开不到变小的效果。
最后动画如果是present就动画还原detail , 如果是dismiss 就把detail缩放,设置center 。
这时候你运行代码 , 并没有从图哪里扩大,而使从0,0点 。
因为这里还有一件事情要做,转换坐标。
在ViewController中
//Present的时候 使用自定义的动画
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.originFrame =
selectedImage!.superview!.convertRect(selectedImage!.frame,
toView: nil)
transition.presenting = true
selectedImage!.hidden = true
return transition
}
//使用默认的动画
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
selectedImage!.hidden = false
transition.presenting = false
return transition
}
第一行是把选择的试图的坐标转换成父试图的坐标,然后transition.presenting 设置了状态,还隐藏了选择的图,这个是为了。dismiss的时候,下面没有图,dismis完成的时候这个图才能显示 。
这个也很简单,在动画完成的时候判断下,如果是dismiss就执行一段代码就行了,可以用代理我这里直接用了闭包
声明一个闭包在PopAnimation
var hideImage:(()->())?
然后动画完成
if !self.presenting{
self.hidIt()
}
func hidIt(){
hideImage?()
}
最后viewc中dsmiss的时候加上那段就行了
//使用默认的动画
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.hideImage={
self.selectedImage!.hidden = false
}
transition.presenting = false
return transition
}
效果
到这里 基本就实现了,主要是后面的算,其实自定义过渡没啥。就那几步。希望大家从中能学到一些东西,这个动画还能更完善就是dismiss之后的圆角。看官们自己搞搞吧