动画示例(十三) —— 一种自定义视图控制器转场动画 (一)

版本记录

版本号 时间
V1.0 2019.06.07 星期五

前言

如果你细看了我前面写的有关动画的部分,就知道前面介绍了CoreAnimation、序列帧以及LOTAnimation等很多动画方式,接下来几篇我们就以动画示例为线索,进行动画的讲解。部分相关代码已经上传至GitHub - 刀客传奇。感兴趣的可以看我写的前面几篇。
1. 动画示例(一) —— 一种外扩的简单动画
2. 动画示例(二) —— 一种抖动的简单动画
3. 动画示例(三) —— 仿头条一种LOTAnimation动画
4. 动画示例(四) —— QuartzCore之CAEmitterLayer下雪❄️动画
5. 动画示例(五) —— QuartzCore之CAEmitterLayer烟花动画
6. 动画示例(六) —— QuartzCore之CAEmitterLayer、CAReplicatorLayer和CAGradientLayer简单动画
7. 动画示例(七) —— 基于CAShapeLayer图像加载过程的简单动画(一)
8. 动画示例(八) —— UIViewController间转场动画的实现 (一)
9. 动画示例(九) —— 一种复杂加载动画的实现 (一)
10. 动画示例(十) —— 一种弹性动画的实现 (一)
11. 动画示例(十一) —— 一种基于UIView的Spring弹性动画的实现 (一)
12. 动画示例(十二) —— 一种不规则形状注入动画的实现 (一)

开始

首先看下写作环境

Swift 5, iOS 12, Xcode 10

无论您是展示相机视图控制器还是自定义设计的模态屏幕,重要的是要了解这些转换是如何发生的。

始终使用相同的UIKit方法调用转换:present(_:animated:completion :)。 此方法使用默认演示动画将当前屏幕“放弃”到另一个视图控制器,以向上滑动新视图以覆盖当前屏幕。

下图显示了一个“New Contact”视图控制器向上滑动联系人列表:

在这个iOS动画教程中,您将创建自己的自定义演示文稿控制器转换,以替换默认的转换,并使本教程的项目更加生动。

打开下载好的入门项目并选择Main.storyboard

第一个视图控制器HomeViewController包含应用程序的配方列表。 只要用户点击列表中的一个图像,HomeViewController就会显示DetailsViewController。 该视图控制器具有图像,标题和描述。

HomeViewController.swiftDetailsViewController.swift中已经有足够的代码来支持基本的应用程序。 构建并运行应用程序以查看应用程序的外观和感觉:

点击其中一个配方图像,然后通过标准垂直封面过渡显示详细信息屏幕。 这可能没问题,但你的食谱值得更好!

您的工作是为您的应用添加一些自定义演示控制器动画,以使其更好! 您将使用将已点击的配方图像扩展为全屏视图的方式替换当前的库存动画,如下所示:

卷起袖子,打开你的开发围裙,为自定义演示控制器的内部工作做好准备!


Behind the Scenes of Custom Transitions

UIKit允许您通过代理模式自定义视图控制器的演示文稿。 您只需使主视图控制器或您专门为此目的创建的另一个类符合UIViewControllerTransitioningDelegate

每次呈现新的视图控制器时,UIKit都会询问其代理是否应该使用自定义转换。 以下是自定义过渡舞蹈的第一步:

UIKit调用animationController(forPresented:presents:source :)来查看它是否返回一个UIViewControllerAnimatedTransitioning对象。 如果该方法返回nil,则UIKit使用内置转换。 如果UIKit接收到UIViewControllerAnimatedTransitioning对象,则UIKit将该对象用作转换的动画控制器。

UIKit可以使用自定义动画控制器之前,舞蹈中还有一些步骤:

UIKit首先要求您的动画控制器 - 简称为animator - 以秒为单位转换持续时间(transition duration),然后调用animateTransition(using:)。这是您的自定义动画成为焦点的时候。

animateTransition(using:)中,您可以访问屏幕上的当前视图控制器以及要显示的新视图控制器。您可以随意淡化,缩放,旋转和操纵现有视图和新视图。

现在您已经了解了自定义演示控制器的工作原理,您可以开始创建自己的演示控制器。


Implementing Transition Delegates

由于代理的任务是管理执行动画的animator对象,因此在编写委托代码之前,首先必须为动画制作者类创建存根。

从Xcode的主菜单中,选择File ▸ New ▸ File…并选择模板iOS ▸ Source ▸ Cocoa Touch Class

将新类命名为PopAnimator,确保选中Swift,并使其成为NSObject的子类。

打开PopAnimator.swift并更新类定义,使其符合UIViewControllerAnimatedTransitioning协议,如下所示:

class PopAnimator: NSObject, UIViewControllerAnimatedTransitioning {

}

您将看到来自Xcode的一些警告,因为您尚未实现所需的委托方法。 您可以使用Xcode提供的快速修复程序来生成缺少的存根方法,也可以自己编写它们。

将以下方法添加到类中:

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) 
    -> TimeInterval {
  return 0
}

上面的0值只是一个占位符值。 在您完成项目时,您将在以后用实际值替换它。

现在,将以下方法存根添加到类中:

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

}

上面的存根将保存你的动画代码,添加它应该清除Xcode中的剩余错误。

现在您已经拥有了基本的animator类,您可以继续在视图控制器端实现委托方法。


Wiring up the Delegates

打开HomeViewController.swift并将以下扩展名添加到文件末尾:

// MARK: - UIViewControllerTransitioningDelegate

extension HomeViewController: UIViewControllerTransitioningDelegate {

}

此代码表示视图控制器符合转换委托协议,您稍后将在此处添加。

首先,在HomeViewController.swift中找到prepare(for:sender :)。 在该方法的底部附近,您将看到设置details view controller的代码。 detailsViewController是新视图控制器的实例,您需要将HomeViewController设置为其转换代理。

在设置配方之前添加以下行:

detailsViewController.transitioningDelegate = self

现在,每次在屏幕上显示详细信息视图控制器时,UIKit都会向HomeViewController询问animator对象。 但是,您仍然没有实现任何UIViewControllerTransitioningDelegate方法,因此UIKit仍将使用默认转换。

下一步是实际创建动画对象并在请求时将其返回给UIKit。


Using the Animator

HomeViewController顶部添加以下新属性:

let transition = PopAnimator()

这是PopAnimator的实例,它将驱动您的动画视图控制器转换。 您只需要一个PopAnimator实例,因为每次呈现视图控制器时都可以继续使用相同的对象,因为每次转换都是相同的。

现在,将第一个委托方法添加到HomeViewController中的UIViewControllerTransitioningDelegate扩展:

func animationController(
  forPresented presented: UIViewController, 
  presenting: UIViewController, source: UIViewController) 
    -> UIViewControllerAnimatedTransitioning? {
  return transition
}

此方法采用一些参数,使您可以决定是否要返回自定义动画。 在本教程中,您将始终返回PopAnimator单例,因为您只有一种展示的转换。

您已经添加了用于呈现视图控制器的委托方法,但是您将如何处理消失视图控制器?

添加以下委托方法来处理此问题:

func animationController(forDismissed dismissed: UIViewController)
    -> UIViewControllerAnimatedTransitioning? {
  return nil
}

上面的方法与前一个方法基本相同:您检查哪个视图控制器被解除并决定是否返回nil并使用默认动画或返回自定义过渡动画并使用它。 目前,你返回nil,因为你不会在以后实施消失动画。

你最终有一个自定义动画师来处理你的自定义过渡。 但它有效吗?

构建并运行您的应用程序并点击其中一个配方图像:

什么都没发生。 为什么? 你有一个自定义animator来推动过渡,但是......哦,等等,你没有向动画师类添加任何代码! 你将在下一节中处理这个问题。


Creating your Transition Animator

打开PopAnimator.swift。 您可以在此处添加代码以在两个视图控制器之间进行转换。

首先,将以下属性添加到此类:

let duration = 0.8
var presenting = true
var originFrame = CGRect.zero

您将在多个位置使用duration,例如当您告诉UIKit过渡将花费多长时间以及何时创建组成动画时。

您还可以定义presenting,告诉animator类您是在演示还是消失视图控制器。 你想要跟踪这一点,因为通常情况下,你会将动画向前运行呈现,反向运行以消失。

最后,您将使用originFrame存储用户点击的图像的原始originFrame - 您将需要它从original frame动画到全屏图像,反之亦然。 稍后当您获取当前选定的图像并将其frame传递给animator实例时,请密切关注originFrame

现在,您可以继续使用UIViewControllerAnimatedTransitioning方法。

transitionDuration(using :)中的代码替换为以下代码:

return duration

重复使用duration属性可以轻松地尝试过渡动画。 您可以简单地修改属性的值,以使转换运行更快或更慢。

1. Setting your Transition’s Context

是时候给animateTransition(using:)添加一些魔法了。 此方法有一个类型为UIViewControllerContextTransitioning的参数,通过该参数可以访问转换的参数和视图控制器。

在开始处理代码本身之前,了解动画上下文实际上是什么很重要。

当两个视图控制器之间的转换开始时,现有视图将添加到转换容器视图(transition container view)中,并且新视图控制器的视图已创建但尚未可见,如下所示:

因此,您的任务是将新视图添加到animateTransition(using:)中的转换容器,“animate in”其显示,并在需要时“animate out”旧视图。

默认情况下,转换动画完成后,旧视图将从转换容器中删除。

在这个厨房里有太多的厨师之前,你会创建一个简单的过渡动画,看看它是如何工作的,然后再实现一个更酷,虽然更复杂的过渡。

2. Adding an Expand Transition

您将从简单的扩展过渡开始,以了解自定义过渡。 将以下代码添加到animateTransition(using:)。 不要担心弹出的两个初始化警告;你将在一分钟内使用这些变量:

let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!

首先,获取将在其中进行动画的containerView,然后获取新视图并将其存储在toView中。

转换上下文对象有两个非常方便的方法,可以让您访问转换播放器:

  • view(forKey :):这使您可以分别通过参数UITransitionContextViewKey.fromUITransitionContextViewKey.to访问“旧”和“新”视图控制器的视图。
  • viewController(forKey :):这使您可以分别通过参数UITransitionContextViewControllerKey.fromUITransitionContextViewControllerKey.to访问“旧”和“新”视图控制器。

此时,您同时拥有容器视图和要显示的视图。 接下来,您需要将要作为子项呈现的视图添加到容器视图中,并以某种方式为其设置动画。

将以下内容添加到animateTransition(using:)

containerView.addSubview(toView)
toView.transform = CGAffineTransform(scaleX: 0.0, y: 0.0)
UIView.animate(
  withDuration: duration,
  animations: {
    toView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
  },
  completion: { _ in
    transitionContext.completeTransition(true)
  }
)

请注意,您在动画完成块中的转换上下文上调用completeTransition(_ :)。 这告诉UIKit您的过渡动画已经完成,并且UIKit可以自由地结束视图控制器转换。

构建并运行您的应用程序并点击列表中的一个配方,您将看到配方概述在主视图控制器上展开:

过渡是可以接受的,你已经看到了在animateTransition(using:)中做了什么 - 但是你要添加更好的东西!

3. Adding a Pop Transition

您将以稍微不同的方式构造新转换的代码,因此使用以下代码替换animateTransition(using:)中的所有代码:

let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
let recipeView = presenting ? toView : transitionContext.view(forKey: .from)!

containerView是您的动画所在的位置,而toView是要呈现的新视图。 如果您正在演示,recipeView只是toView,否则您从上下文中获取它,因为它现在是“from”视图。 对于呈现和消失,您将始终为recipeView制作动画。 当您显示详细信息控制器视图时,它将逐渐占用整个屏幕。 当被消失时,它将缩小到图像的原始frame

将以下内容添加到animateTransition(using:)

let initialFrame = presenting ? originFrame : recipeView.frame
let finalFrame = presenting ? recipeView.frame : originFrame

let xScaleFactor = presenting ?
  initialFrame.width / finalFrame.width :
  finalFrame.width / initialFrame.width

let yScaleFactor = presenting ?
  initialFrame.height / finalFrame.height :
  finalFrame.height / initialFrame.height

在上面的代码中,您可以检测初始和最终动画frames,然后计算在每个视图之间设置动画时需要在每个轴上应用的比例因子。

现在,您需要仔细定位新视图,使其显示在点击图像的正上方。 这将使得看起来像点击的图像扩展以填充屏幕。


Scaling the View

将以下内容添加到animateTransition(using:)

let scaleTransform = CGAffineTransform(scaleX: xScaleFactor, y: yScaleFactor)

if presenting {
  recipeView.transform = scaleTransform
  recipeView.center = CGPoint(
    x: initialFrame.midX,
    y: initialFrame.midY)
  recipeView.clipsToBounds = true
}

recipeView.layer.cornerRadius = presenting ? 20.0 : 0.0
recipeView.layer.masksToBounds = true

在显示新视图时,您可以设置其比例和位置,使其与初始帧的大小和位置完全匹配。 您还可以设置正确的拐角半径。

现在,将最后的代码添加到animateTransition(using:)

containerView.addSubview(toView)
containerView.bringSubviewToFront(recipeView)

UIView.animate(
  withDuration: duration,
  delay:0.0,
  usingSpringWithDamping: 0.5,
  initialSpringVelocity: 0.2,
  animations: {
    recipeView.transform = self.presenting ? .identity : scaleTransform
    recipeView.center = CGPoint(x: finalFrame.midX, y: finalFrame.midY)
    recipeView.layer.cornerRadius = !self.presenting ? 20.0 : 0.0
  }, completion: { _ in
    transitionContext.completeTransition(true)
})

这将首先将toView添加到容器中。接下来,您需要确保recipeView位于顶部,因为这是您正在制作动画的唯一视图。请记住,在消失时,toView是原始视图,因此,在第一行中,您将在其他所有内容之上添加它,除非您将recipeView带到前面,否则您的动画将被隐藏起来。

然后,您可以启动动画。在这里使用弹簧动画会给它一些反弹。

animations表达式中,您可以更改recipeView的变换,位置和角半径。在呈现时,您将从配方图像的小尺寸变为全屏,因此目标变换只是identity变换。在消失时,您可以对其进行动画处理以缩小以匹配原始图像大小。

此时,您已经通过将新视图控制器放置在点击图像上来设置舞台,您在初始帧和最终frames之间进行了动画处理,最后,您调用了completeTransition(using:)将事物交还给UIKit 。现在是时候看到你的代码了!

构建并运行您的应用程序。点击第一个配方图像以查看视图控制器转换的实际效果。

嗯,它并不完美,但是一旦你处理了一些粗糙的边缘,你的动画将正是你想要的!


Adding Some Polish

目前,您的动画从左上角开始。 这是因为originFrame的默认值的原点为(0,0),并且您从未将其设置为任何其他值。

打开HomeViewController.swift并在代码return转换transition之前将以下代码添加到animationController(forPresented:presents:source :)的顶部:

guard 
  let selectedIndexPathCell = tableView.indexPathForSelectedRow,
  let selectedCell = tableView.cellForRow(at: selectedIndexPathCell) 
    as? RecipeTableViewCell,
  let selectedCellSuperview = selectedCell.superview
  else {
    return nil
}

transition.originFrame = selectedCellSuperview.convert(selectedCell.frame, to: nil)
transition.originFrame = CGRect(
  x: transition.originFrame.origin.x + 20,
  y: transition.originFrame.origin.y + 20,
  width: transition.originFrame.size.width - 40,
  height: transition.originFrame.size.height - 40
)

transition.presenting = true
selectedCell.shadowView.isHidden = true

这将获取所选单元格,将转换的originFrame设置为selectedCellSuperviewframe,这是您最后一次点击的单元格。 然后,将present设置为true并在动画期间隐藏点击的单元格。

再次构建并运行应用程序,并在列表中点击不同的配方,以查看转换的每个配置。

1. Adding a Dismiss Transition

剩下要做的就是消失details controller。 你实际上已经完成了animator的大部分工作 - 转换动画代码完成逻辑以设置正确的初始和最终frames,所以你大部分都是向前和向后播放动画的方式。 甜!

打开HomeViewController.swift并用以下内容替换animationController(forDismissed :)的主体:

transition.presenting = false
return transition

这告诉您的animator对象您正在关闭视图控制器,以便动画代码以正确的方向运行。

构建并运行应用程序以查看结果。 点击食谱,然后点击屏幕左上角的X按钮将其关闭。

过渡动画看起来很棒,但请注意您选择的食谱已从table view中消失! 当您关闭详细信息屏幕时,您需要确保重新显示已点击的图像。

打开PopAnimator.swift并向该类添加一个新的闭包属性:

var dismissCompletion: (() -> Void)?

这将允许您传递一些代码,以便在消失转换完成时运行。

接下来,在调用completeTransition()之前,找到animateTransition(using:)并将以下代码添加到对animate(...)的调用中的完成处理程序中:

if !self.presenting {
  self.dismissCompletion?()
}

一旦消失动画完成,此代码将执行dismissCompletion,这是显示原始图像的最佳位置。

打开HomeViewController.swift并将以下代码添加到文件开头的主类中:

override func viewDidLoad() {
  super.viewDidLoad()

  transition.dismissCompletion = { [weak self] in
    guard 
      let selectedIndexPathCell = self?.tableView.indexPathForSelectedRow,
      let selectedCell = self?.tableView.cellForRow(at: selectedIndexPathCell) 
        as? RecipeTableViewCell
      else {
        return
    }

    selectedCell.shadowView.isHidden = false
  }
}

转换动画完成后,此代码显示所选单元格的原始图像以替换配方详细信息视图控制器。

构建并运行您的应用程序以享受过渡动画两种方式!


Device Orientation Transition

您可以将设备方向更改视为从视图控制器到其自身的演示过渡,只是大小不同。

由于应用程序是使用自动布局(Auto Layout)构建的,因此您无需进行更改。 只需旋转设备并享受过渡(如果在iPhone模拟器中进行测试,请按下Command-left arrow)!

后记

本篇主要讲述了一种自定义视图控制器转场动画的实现,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容