版本记录
版本号 | 时间 |
---|---|
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.swift
和DetailsViewController.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.from
或UITransitionContextViewKey.to
访问“旧”和“新”视图控制器的视图。 -
viewController(forKey :):这使您可以分别通过参数
UITransitionContextViewControllerKey.from
或UITransitionContextViewControllerKey.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
设置为selectedCellSuperview
的frame
,这是您最后一次点击的单元格。 然后,将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
)!
后记
本篇主要讲述了一种自定义视图控制器转场动画的实现,感兴趣的给个赞或者关注~~~