自定义视图控制器转场动画[译]

iOS动画教学:自定义视图控制器转场动画

Note from Ray:这是从iOS Animations by Tutorials Second Edition摘录的一小节,能从这篇文章中显露出书籍讲述的内容。教程基于iOS 9,Xcode 7和Swift 2。

是否当你想要显示摄像视图控制器,地址栏或者你自定义的场景控制器时,你都是在调用相同的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。在这本书里,你会学习到更多视图控制器的过渡展示,方向改变动画,导航控制器和交互过渡,等等。

我们希望你享受这个教程,如果你有任何问题或者建议,可以在下方留言!

原文地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容