iOS 动画教程-自定义 View Controller 呈现转换
2017-01-11 17:31
573 查看
原文:ios-animation-tutorial-custom-view-controller-presentation-transitions
作者:Marin Todorov
译者:kmyhy
当你呈现相机、通讯录、或者某种自定义模式窗口时,你每次都会调用同一个 UIKit 方法 present(_:animated:completion:)。这个方法将当前屏幕“让给”另一个 view controller。
默认的呈现动画简单地用新视图推开当前视图。下图演示了“新建联系人” view controller 在联系人列表视图上层向上滑出:
在本教程中,你将用自己的自定义呈现动画替换默认动画,并完成本教程中的项目。
第一个 view controller(即 ViewController)包含了 app 的标题和主要介绍以及底部的一个 scroll view,用于显示一个有用的香草列表。
当用户点击了列表中的图片,main view controller 会呈现一个 HerbDetailsViewController;这个 view controller 有一个背景、一个标题、一个描述和几个按钮用于注明图片的所有者。
在 ViewController.swift 和 HerbDetailsViewController.swift 已经有部分代码了,足以维持 app 运行。运行程序,app 是这个样子:
点击某个香草图片,细节页面以标准的弹出动画方式呈现。对于一般的 app 来说这也足够了,但对于你的 app 则需要做得更好!
你的任务是创建自定义呈现动画让你的 app 更加绚烂夺目!你需要将目前内置的动画替换成:用所点击的香草的图片展开至全屏!
撸起手袖,系紧围裙,准备动手开始定制呈现控制器!
每当你呈现一个新的 view controller 时,UIKit 会询问它的委托是否需要使用自定义动画。自定义动画的第一步是这样的:
UIKit 会调用 animationController(forPresented:presenting:source:) 方法,看是否有一个 UIViewControllerAnimatedTransitioning 对象返回。如果这个方法返回空,UIKit 使用默认动画,否则,UIKit 使用返回的对象作为这次转换的动画控制器。
UIKit 首先询问动画控制器(简称为 animator),动画需要几秒钟?然后调用它的animateTransition(using:) 方法。这时你的自定义动画开始生效了。
在 animateTransition(using:) 方法中,你可以同时访问到正在显示的 view controller 和即将呈现的新 view controller。你可以淡入、缩放、旋转并随心所欲地操作已有的视图和新的视图。
你已经大致了解了之定义呈现控制器是如何工作的了,现在,开始来创建我们自己的吧!
打开 Xcode 菜单 File\New\File… 选择模板: iOS\Source\Cocoa Touch Class。
类名设置为 PopAnimator,语言选择 Swift,继承于 NSObject。
打开 PopAnimator.swift 修改类定义,实现 UIViewControllerAnimatedTransitioning 协议:
Xcode 会抱怨没有实现必须的委托方法,等下我们会解决这个。
在类中添加如下方法:
动画时长返回 0 只是临时的;后面会修改这个为真正的时长。
继续新增如下方法:
这个方法将放入动画代码,暂时是空实现,以消除 Xcode 的报错。
有了基本的 animator 类之后,你可以在 view controller 中实现委托方法了。
打开 ViewController.swift 新增如下扩展:
这声明对 transitioning delegate 协议的实现。等会我们再来添加这些方法。
找到 didTapImageView(_:) 方法。在方法底部,你看到了呈现详情 view controller 的代码。herbDetails 是新 view controller 的实例;你需要将它的 transitioning 委托设置为 main controller。
在这个方法最后一行,即调用 present(…) 方法之后加入以下代码:
现在 UIKit 会在每次呈现 details view controller 的时候都索要一个 animator 对象。但你还没有实现任何 UIViewControllerTransitioningDelegate 方法,所以 UIKit 还是会使用默认的动画。
接下来应该实例化一个 animator 对象并在 UIKit 询问的时候返给它。
在 ViewController 中添加一个属性:
这个是一个 PopAnimator 对象,用于驱动你的 view controller 动画。你只需要一个 PopAnimator 对象,因为你可以在每次呈现 view controller 时都使用同一个 animator 对象,因为每次的动画都是同一个。
在 ViewController 的扩展中加入第一个委托方法:
这个方法提供几个参数,你可以根据它们来决定是返回一个自定义的动画还是不。在本文中,你总是返回同一个 PopAnimator 实例,因为你只有一个呈现动画。
你已经添加了一个用于呈现 view controller 的委托方法,那么用于解散的呢?
这是另一个委托方法:
这个方法和前一个方法基本是干同一件事情:判断要解散的是哪个 view controller,觉此来决定是否返回 nil,返回 nil 表示采用默认的解散动画,或者返回一个自定义的 animator。这里你返回的是 nil,因为你还没有实现解散动画。
你已经拥有了一个自定义的 animator 来负责自定义动画,但它是如何工作的呢?
运行程序,点击任何一张香草图片:
什么也没发生。为啥?你有一个用于驱动动画的自定义 animator,但是……等等,在 animator 类中还没有编写代码!你会在下一节完成这个任务。
首先,加入几个属性:
duration 变量会用到几个地方,比如告诉 UIKit 动画时长,以及创建动画时。
我们还定义了一个 presenting 变量,用于告诉 animator 类,当前是在呈现还是解散过程。我们需要记住这个变量,因为我们将以正面顺序执行呈现,而以相反顺序执行解散。
最后,我们用 originFrame 变量保存原来用户所点到的图片的 frame 形状——我们会将图片由这个 frame 以动画方式放大到全屏,反过来则执行相反动作。当你获取当前所选图片并将它的 frame 传递给 animator 实例时,需要注意这个 originFrame。
现在,你可以回到 UIViewControllerAnimatedTransitioning 方法来了。
在 transitionDuration() 方法中,用下句替换:
重用 duration 属性,能让你很容易可以调试 transition 动画。你可以简单修改这个值,使动画变快变慢。
在开始编写代码之前,一个重要的问题就是理解 animation context 的实际上是什么。
当两个 view controller 之间开始转换时,原来的 view 被添加到 transition container 转换容器,新的 view controller 的 view 被创建出来,但仍然不可见,如下图所示:
因此你的任务是在 animateTransition() 方法中将新的 view 添加到转换容器,“以动画方式”显示它,如果有必要的话,将原有的 view 以动画方式移除。
默认,当转换动画完成时,原有 view 从转换容器中移除。
在你能够“烹制”出更多的食品之前,你需要创建一个简单的动画,看看它是如何实现的,然后再实现更酷的、同时也是更复杂的转换。
首先,获取容器 view,你的动画将在这个 view 中发生。然后获取新的 view 并赋给 toView 变量。
转换上下文有两个非常方便的方法,允许你访问动画的参与者
view(forKey:): 你可以访问到 “原有的” and “新的” view,通过指定 key 参数为 UITransitionContextViewKey.from 或 UITransitionContextViewKey.to。
viewController(forKey:): 你可以通过指定 key 参数为 UITransitionContextViewControllerKey.from 或者 UITransitionContextViewControllerKey.to 来访问 “原有的” 和 “新的” view controller。
这里,你要同时用到 container view 和要呈现的 view 。然后你将要呈现的 view 添加为 container view 的 subview 并以某种方式动画。
在 animateTransition() 中添加:
注意,你需要在动画完成块中调用转换上下文的 completeTransition() 方法,这是为了通知 UIKit 你的转换动画已经完成,UIKit 可以完成这次 view controller 转换了。
运行程序,点击某张香草图片,你会看到香草的介绍以淡入的方式出现在主 view controller 中:
这个转换勉强过得去,你已经大概弄清了在 animateTransition 方法中应该干些什么——你将在里面加入一些更好的东西!
containerView 是你的动画即将发生的地方,toView 是需要呈现的新视图。如果你正在呈现,herbView 就是 toView,否则它应该从上下文中获取。对于解散和呈现,herbView 都会是你将执行动画的 view。
当你呈现细节页面时,它会拉伸到整个屏幕大小。解散时,它又会缩小到原始 frame 大小。
在 animateTransition() 添加:
在上述代码中,我们需要根据条件获得原先的 frame 和最终动画结束时的 frame,然后计算两个 view 之间的横纵比例。
现在我们需要关注新 view 的位置,因为它需要显示在所点击的 image 上方,看起来就像是被点的图像拉伸到了全屏大小。
在 animateTransition() 中添加:
当呈现新 view 时,我们设置了它的 scale 和位置以便和原图 frame 的位置大小匹配。
现在在 animateTransition() 中加入最后的代码:
首先将 toView 添加到 container。然后,让 herbView 放在 subview 的最上层,因为你只会对这个 view 进行动画。记住,在解散时,toView 是原始 view,因此在第一个行代码,你会将 toView 加在最上层,这样你的动画会被隐藏在下层看不见,所以你需要将 herbView 放到上层。
然后,开始动画。这里使用了一个 spring 动画,这会带来一种弹簧效果。
在 animations 块中,我们修改 herbView 的 transform 属性和位置。在呈现时,你将底部的小尺寸动画到全屏,因此目标 transform 就是 identity transform。在解散时,你将它的大小缩小到原始图片大小。
这里,我们已经准备好了将新 view 的位置对齐被点到的图片,在原来的 frame 和最终的 frame 之间进行动画,最后调用 completeTransition() 方法将控制转给 UIKit。让我们来看看代码的实际效果!
运行程序,点击第一个香草图片,看看你的动画效果:
是的,它还不是十分完美。但当你修改了这些瑕疵,你的动画就会同你想象的一模一样!
当前你的动画是从左上角开始;因为 originFrame 默认的 origin 是(0,0)——你并没有修改过这个值。
打开 ViewController.swift 在 animationController(forPresented:) 头部加入:
将 transition 的 originFrame 设置为 selectedImage 即你刚刚点击的图片的 frame。然后将 presenting 设置为 true,在动画期间隐藏所选图片。
运行程序,点击列表中的不同香草,你会看到:
打开 ViewController.swift 修改 animationController(forDismissed:) 方法为:
这将告诉 animator 对象,你将解散一个 view controller,这样动画代码会以正确的方式进行。
运行程序,点击一张香草图片然后点击屏幕任何地方解散它:
转换动画看起来没什么问题,但请注意,你选择的香草从 scroll view 中消失了!当你解散细节页面时,你需要让所点击的图片重新显示。
打开 PopAnimator.swift 添加一个新的闭包属性:
这将允许解散动画完成时执行你传入的代码。
然后,找到 animateTransition() 方法在完成块中,在调用 completeTransition() 之前加入:
当解散完成,调用 dismissCompletion —— 这里刚好可以显示原来的图片。
打开 ViewController.swift 在 viewDidLoad() 中加入:
这里当转换动画完成重新显示了原来的图片,以替代详情页面。
运行程序,体验转换动画,包括呈现和解散。现在,香草不会在无缘无故消失了!
你可以将设备方向改变看成是一种呈现,从一个 view controller 转换到它自己,仅仅是 size 不同。
iOS 8 中出现的 viewWillTransition(to size:coordinator:)方法,允许你以一种简单直白的方式处理设备方向的变化。你不再需要为横屏竖屏分别设计不同的布局,相反,你只需改变 view controller 的视图 size。
打开 ViewController.swift ,实现 viewWillTransition(to:with:) 方法:
第一个参数 size 通知你 view controller 当前正在转换到哪个 size。第二个参数 coordinator 是一个 transition coordinator 对象,通过它可以访问该转换的许多属性。
当横屏的时候,你需要做的仅仅是降低 app 背景图片的 alpha 值,提高文字的可读性。
在 viewWillTransitionToSize 加入:
animate(alongsideTransition:) 允许你在旋屏过程中同时执行你指定的动画,也就是在 UIKit 执行默认旋屏动画的同时。
你的动画块会收到一个 transitionging 上下文,这和你在呈现 view controller 时使用的上下文是一样的。这里,你没有 from 和 to 视图控制器了,因为它们是同一个,但你可以获得比如动画时长等属性。
在动画块中,我们判断目标 size 的宽度是否大于高度,如果是,降低背景图的 alpha 值为 0.25。这将使横屏下的背景变淡。如果是竖屏模式,alpha 值设为 0.55。
运行程序,旋转设备(如果是模拟器,按 command+左箭头),查看实际效果。
你将看到当旋转到横屏时背景变暗。这使得长文本更容易阅读。
如果你点击图片,你会注意到动画有点乱。因为屏幕旋转为横屏后,图片仍然是竖向的大小。在原始图片和拉伸至全屏的图像之间的转换并不流畅。
不要担心——你有一个新方法 viewWillTransition(to:with:) 能够解决这个问题。
ViewController 有一个成员方法叫 positionListItems(),它负责香草图片的大小和位置。这个方法在 app 一启动时,被 viewDidLoad()方法所调用。
在 animate(alongsideTransition:) 方法的动画块中,在设置 alpha 值之后加入以下代码:
这将在设备旋转后改变香草图片的 size 和位置。当屏幕完成旋转后,香草图片也会被重新改变大小:
因为这些图片都已经有了一个横屏布局,因此你的转换动画就能正常运行了。试试看!
这里,你可以对这个转换进行大量的改进。例如,这些点子:
在转换期间隐藏被点击的图片,以便它们看起来真的想“长大”到整个屏幕。
让每个香草的描述文本以淡入淡出的方式动画,这样转换动画会更加平滑。
针对横屏对转换进行测试和调整。
如果你想学习更多内容,请参考我们的iOS Animations by Tutorials。这本书已完全迟滞 Swift 3 和 iOS10 。你会学习如何使用 spring 动画、转换、关键帧动画、CALayer 动画、自动布局约束动画、view controller 转换动画等等!
希望你喜欢本教程,如果有任何问题和建议,请在下面留言!
作者:Marin Todorov
译者:kmyhy
当你呈现相机、通讯录、或者某种自定义模式窗口时,你每次都会调用同一个 UIKit 方法 present(_:animated:completion:)。这个方法将当前屏幕“让给”另一个 view controller。
默认的呈现动画简单地用新视图推开当前视图。下图演示了“新建联系人” view controller 在联系人列表视图上层向上滑出:
在本教程中,你将用自己的自定义呈现动画替换默认动画,并完成本教程中的项目。
开始
下载本文的开始项目 Beginner Cook。打开 Main.storyboard :第一个 view controller(即 ViewController)包含了 app 的标题和主要介绍以及底部的一个 scroll view,用于显示一个有用的香草列表。
当用户点击了列表中的图片,main view controller 会呈现一个 HerbDetailsViewController;这个 view controller 有一个背景、一个标题、一个描述和几个按钮用于注明图片的所有者。
在 ViewController.swift 和 HerbDetailsViewController.swift 已经有部分代码了,足以维持 app 运行。运行程序,app 是这个样子:
点击某个香草图片,细节页面以标准的弹出动画方式呈现。对于一般的 app 来说这也足够了,但对于你的 app 则需要做得更好!
你的任务是创建自定义呈现动画让你的 app 更加绚烂夺目!你需要将目前内置的动画替换成:用所点击的香草的图片展开至全屏!
撸起手袖,系紧围裙,准备动手开始定制呈现控制器!
自定义动画的幕后工作
UIKit 允许你通过委托模型定制化 view controller 的呈现过程;你可以让 main view controller(或者可以用另一个类专门来干这个)采用 UIViewControllerTransitioningDelegate 协议。每当你呈现一个新的 view controller 时,UIKit 会询问它的委托是否需要使用自定义动画。自定义动画的第一步是这样的:
UIKit 会调用 animationController(forPresented:presenting:source:) 方法,看是否有一个 UIViewControllerAnimatedTransitioning 对象返回。如果这个方法返回空,UIKit 使用默认动画,否则,UIKit 使用返回的对象作为这次转换的动画控制器。
UIKit 首先询问动画控制器(简称为 animator),动画需要几秒钟?然后调用它的animateTransition(using:) 方法。这时你的自定义动画开始生效了。
在 animateTransition(using:) 方法中,你可以同时访问到正在显示的 view controller 和即将呈现的新 view controller。你可以淡入、缩放、旋转并随心所欲地操作已有的视图和新的视图。
你已经大致了解了之定义呈现控制器是如何工作的了,现在,开始来创建我们自己的吧!
实现 Transition 委托
因为委托的责任是管理动画控制器 animator 对象,而由 animator 来执行真正的动画,因此在编写委托代码之前的第一件事情就是创建一个 animator 类。打开 Xcode 菜单 File\New\File… 选择模板: iOS\Source\Cocoa Touch Class。
类名设置为 PopAnimator,语言选择 Swift,继承于 NSObject。
打开 PopAnimator.swift 修改类定义,实现 UIViewControllerAnimatedTransitioning 协议:
class PopAnimator: NSObject, UIViewControllerAnimatedTransitioning { }
Xcode 会抱怨没有实现必须的委托方法,等下我们会解决这个。
在类中添加如下方法:
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return 0 }
动画时长返回 0 只是临时的;后面会修改这个为真正的时长。
继续新增如下方法:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { }
这个方法将放入动画代码,暂时是空实现,以消除 Xcode 的报错。
有了基本的 animator 类之后,你可以在 view controller 中实现委托方法了。
打开 ViewController.swift 新增如下扩展:
extension ViewController: UIViewControllerTransitioningDelegate { }
这声明对 transitioning delegate 协议的实现。等会我们再来添加这些方法。
找到 didTapImageView(_:) 方法。在方法底部,你看到了呈现详情 view controller 的代码。herbDetails 是新 view controller 的实例;你需要将它的 transitioning 委托设置为 main controller。
在这个方法最后一行,即调用 present(…) 方法之后加入以下代码:
// ... present(herbDetails, animated: true, completion: nil) herbDetails.transitioningDelegate = self // 加入这行
现在 UIKit 会在每次呈现 details view controller 的时候都索要一个 animator 对象。但你还没有实现任何 UIViewControllerTransitioningDelegate 方法,所以 UIKit 还是会使用默认的动画。
接下来应该实例化一个 animator 对象并在 UIKit 询问的时候返给它。
在 ViewController 中添加一个属性:
let transition = PopAnimator()
这个是一个 PopAnimator 对象,用于驱动你的 view controller 动画。你只需要一个 PopAnimator 对象,因为你可以在每次呈现 view controller 时都使用同一个 animator 对象,因为每次的动画都是同一个。
在 ViewController 的扩展中加入第一个委托方法:
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { return transition }
这个方法提供几个参数,你可以根据它们来决定是返回一个自定义的动画还是不。在本文中,你总是返回同一个 PopAnimator 实例,因为你只有一个呈现动画。
你已经添加了一个用于呈现 view controller 的委托方法,那么用于解散的呢?
这是另一个委托方法:
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { return nil }
这个方法和前一个方法基本是干同一件事情:判断要解散的是哪个 view controller,觉此来决定是否返回 nil,返回 nil 表示采用默认的解散动画,或者返回一个自定义的 animator。这里你返回的是 nil,因为你还没有实现解散动画。
你已经拥有了一个自定义的 animator 来负责自定义动画,但它是如何工作的呢?
运行程序,点击任何一张香草图片:
什么也没发生。为啥?你有一个用于驱动动画的自定义 animator,但是……等等,在 animator 类中还没有编写代码!你会在下一节完成这个任务。
创建 Transition Animator
打开 PopAnimator.swift; 这里我们将加入两个 view controller 之间进行转换的代码。首先,加入几个属性:
let duration = 1.0 var presenting = true var originFrame = CGRect.zero
duration 变量会用到几个地方,比如告诉 UIKit 动画时长,以及创建动画时。
我们还定义了一个 presenting 变量,用于告诉 animator 类,当前是在呈现还是解散过程。我们需要记住这个变量,因为我们将以正面顺序执行呈现,而以相反顺序执行解散。
最后,我们用 originFrame 变量保存原来用户所点到的图片的 frame 形状——我们会将图片由这个 frame 以动画方式放大到全屏,反过来则执行相反动作。当你获取当前所选图片并将它的 frame 传递给 animator 实例时,需要注意这个 originFrame。
现在,你可以回到 UIViewControllerAnimatedTransitioning 方法来了。
在 transitionDuration() 方法中,用下句替换:
return duration
重用 duration 属性,能让你很容易可以调试 transition 动画。你可以简单修改这个值,使动画变快变慢。
设置转换上下文
现在为 animateTransition 注入魔力。这个方法有一个 UIViewControllerContextTransitioning 参数,通过它你能访问和转换相关的 view controller 和参数。在开始编写代码之前,一个重要的问题就是理解 animation context 的实际上是什么。
当两个 view controller 之间开始转换时,原来的 view 被添加到 transition container 转换容器,新的 view controller 的 view 被创建出来,但仍然不可见,如下图所示:
因此你的任务是在 animateTransition() 方法中将新的 view 添加到转换容器,“以动画方式”显示它,如果有必要的话,将原有的 view 以动画方式移除。
默认,当转换动画完成时,原有 view 从转换容器中移除。
在你能够“烹制”出更多的食品之前,你需要创建一个简单的动画,看看它是如何实现的,然后再实现更酷的、同时也是更复杂的转换。
添加淡入动画
开始,我们用一个简单的淡出动画来实现自定义动画。在 animateTransition 方法中加入:let containerView = transitionContext.containerView let toView = transitionContext.view(forKey: .to)!
首先,获取容器 view,你的动画将在这个 view 中发生。然后获取新的 view 并赋给 toView 变量。
转换上下文有两个非常方便的方法,允许你访问动画的参与者
view(forKey:): 你可以访问到 “原有的” and “新的” view,通过指定 key 参数为 UITransitionContextViewKey.from 或 UITransitionContextViewKey.to。
viewController(forKey:): 你可以通过指定 key 参数为 UITransitionContextViewControllerKey.from 或者 UITransitionContextViewControllerKey.to 来访问 “原有的” 和 “新的” view controller。
这里,你要同时用到 container view 和要呈现的 view 。然后你将要呈现的 view 添加为 container view 的 subview 并以某种方式动画。
在 animateTransition() 中添加:
containerView.addSubview(toView) toView.alpha = 0.0 UIView.animate(withDuration: duration, animations: { toView.alpha = 1.0 }, completion: { _ in transitionContext.completeTransition(true) } )
注意,你需要在动画完成块中调用转换上下文的 completeTransition() 方法,这是为了通知 UIKit 你的转换动画已经完成,UIKit 可以完成这次 view controller 转换了。
运行程序,点击某张香草图片,你会看到香草的介绍以淡入的方式出现在主 view controller 中:
这个转换勉强过得去,你已经大概弄清了在 animateTransition 方法中应该干些什么——你将在里面加入一些更好的东西!
加入一个 Pop 动画
新的动画需要重新调整一下代码结构,因此将 animateTransition() 中的代码替换为:let containerView = transitionContext.containerView let toView = transitionContext.view(forKey: .to)! let herbView = presenting ? toView : transitionContext.view(forKey: .from)!
containerView 是你的动画即将发生的地方,toView 是需要呈现的新视图。如果你正在呈现,herbView 就是 toView,否则它应该从上下文中获取。对于解散和呈现,herbView 都会是你将执行动画的 view。
当你呈现细节页面时,它会拉伸到整个屏幕大小。解散时,它又会缩小到原始 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 和最终动画结束时的 frame,然后计算两个 view 之间的横纵比例。
现在我们需要关注新 view 的位置,因为它需要显示在所点击的 image 上方,看起来就像是被点的图像拉伸到了全屏大小。
在 animateTransition() 中添加:
let scaleTransform = CGAffineTransform(scaleX: xScaleFactor, y: yScaleFactor) if presenting { herbView.transform = scaleTransform herbView.center = CGPoint( x: initialFrame.midX, y: initialFrame.midY) herbView.clipsToBounds = true }
当呈现新 view 时,我们设置了它的 scale 和位置以便和原图 frame 的位置大小匹配。
现在在 animateTransition() 中加入最后的代码:
containerView.addSubview(toView) containerView.bringSubview(toFront: herbView) UIView.animate(withDuration: duration, delay:0.0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.0, animations: { herbView.transform = self.presenting ? CGAffineTransform.identity : scaleTransform herbView.center = CGPoint(x: finalFrame.midX, y: finalFrame.midY) }, completion:{_ in transitionContext.completeTransition(true) } )
首先将 toView 添加到 container。然后,让 herbView 放在 subview 的最上层,因为你只会对这个 view 进行动画。记住,在解散时,toView 是原始 view,因此在第一个行代码,你会将 toView 加在最上层,这样你的动画会被隐藏在下层看不见,所以你需要将 herbView 放到上层。
然后,开始动画。这里使用了一个 spring 动画,这会带来一种弹簧效果。
在 animations 块中,我们修改 herbView 的 transform 属性和位置。在呈现时,你将底部的小尺寸动画到全屏,因此目标 transform 就是 identity transform。在解散时,你将它的大小缩小到原始图片大小。
这里,我们已经准备好了将新 view 的位置对齐被点到的图片,在原来的 frame 和最终的 frame 之间进行动画,最后调用 completeTransition() 方法将控制转给 UIKit。让我们来看看代码的实际效果!
运行程序,点击第一个香草图片,看看你的动画效果:
是的,它还不是十分完美。但当你修改了这些瑕疵,你的动画就会同你想象的一模一样!
当前你的动画是从左上角开始;因为 originFrame 默认的 origin 是(0,0)——你并没有修改过这个值。
打开 ViewController.swift 在 animationController(forPresented:) 头部加入:
transition.originFrame = selectedImage!.superview!.convert(selectedImage!.frame, to: nil) transition.presenting = true selectedImage!.isHidden = true
将 transition 的 originFrame 设置为 selectedImage 即你刚刚点击的图片的 frame。然后将 presenting 设置为 true,在动画期间隐藏所选图片。
运行程序,点击列表中的不同香草,你会看到:
添加解散动画
剩下来的事情就是解散详情页面。实际上大部分工作都已经在 animator 中做完了——转换动画中的代码中开始、结束 frame 都已经设置正确,你最后的工作就是在呈现和解散时适时地播放动画。开心吧?打开 ViewController.swift 修改 animationController(forDismissed:) 方法为:
transition.presenting = false return transition
这将告诉 animator 对象,你将解散一个 view controller,这样动画代码会以正确的方式进行。
运行程序,点击一张香草图片然后点击屏幕任何地方解散它:
转换动画看起来没什么问题,但请注意,你选择的香草从 scroll view 中消失了!当你解散细节页面时,你需要让所点击的图片重新显示。
打开 PopAnimator.swift 添加一个新的闭包属性:
var dismissCompletion: (()->Void)?
这将允许解散动画完成时执行你传入的代码。
然后,找到 animateTransition() 方法在完成块中,在调用 completeTransition() 之前加入:
if !self.presenting { self.dismissCompletion?() }
当解散完成,调用 dismissCompletion —— 这里刚好可以显示原来的图片。
打开 ViewController.swift 在 viewDidLoad() 中加入:
transition.dismissCompletion = { self.selectedImage!.isHidden = false }
这里当转换动画完成重新显示了原来的图片,以替代详情页面。
运行程序,体验转换动画,包括呈现和解散。现在,香草不会在无缘无故消失了!
设备方向的转换
注意: 这部分内容是可选的。如果你对设备方向改变不感兴趣的话,请跳到挑战部分。你可以将设备方向改变看成是一种呈现,从一个 view controller 转换到它自己,仅仅是 size 不同。
iOS 8 中出现的 viewWillTransition(to size:coordinator:)方法,允许你以一种简单直白的方式处理设备方向的变化。你不再需要为横屏竖屏分别设计不同的布局,相反,你只需改变 view controller 的视图 size。
打开 ViewController.swift ,实现 viewWillTransition(to:with:) 方法:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) }
第一个参数 size 通知你 view controller 当前正在转换到哪个 size。第二个参数 coordinator 是一个 transition coordinator 对象,通过它可以访问该转换的许多属性。
当横屏的时候,你需要做的仅仅是降低 app 背景图片的 alpha 值,提高文字的可读性。
在 viewWillTransitionToSize 加入:
coordinator.animate( alongsideTransition: {context in self.bgImage.alpha = (size.width>size.height) ? 0.25 : 0.55 }, completion: nil )
animate(alongsideTransition:) 允许你在旋屏过程中同时执行你指定的动画,也就是在 UIKit 执行默认旋屏动画的同时。
你的动画块会收到一个 transitionging 上下文,这和你在呈现 view controller 时使用的上下文是一样的。这里,你没有 from 和 to 视图控制器了,因为它们是同一个,但你可以获得比如动画时长等属性。
在动画块中,我们判断目标 size 的宽度是否大于高度,如果是,降低背景图的 alpha 值为 0.25。这将使横屏下的背景变淡。如果是竖屏模式,alpha 值设为 0.55。
运行程序,旋转设备(如果是模拟器,按 command+左箭头),查看实际效果。
你将看到当旋转到横屏时背景变暗。这使得长文本更容易阅读。
如果你点击图片,你会注意到动画有点乱。因为屏幕旋转为横屏后,图片仍然是竖向的大小。在原始图片和拉伸至全屏的图像之间的转换并不流畅。
不要担心——你有一个新方法 viewWillTransition(to:with:) 能够解决这个问题。
ViewController 有一个成员方法叫 positionListItems(),它负责香草图片的大小和位置。这个方法在 app 一启动时,被 viewDidLoad()方法所调用。
在 animate(alongsideTransition:) 方法的动画块中,在设置 alpha 值之后加入以下代码:
self.positionListItems()
这将在设备旋转后改变香草图片的 size 和位置。当屏幕完成旋转后,香草图片也会被重新改变大小:
因为这些图片都已经有了一个横屏布局,因此你的转换动画就能正常运行了。试试看!
结束语
从这里下载最终完成的项目。这里,你可以对这个转换进行大量的改进。例如,这些点子:
在转换期间隐藏被点击的图片,以便它们看起来真的想“长大”到整个屏幕。
让每个香草的描述文本以淡入淡出的方式动画,这样转换动画会更加平滑。
针对横屏对转换进行测试和调整。
如果你想学习更多内容,请参考我们的iOS Animations by Tutorials。这本书已完全迟滞 Swift 3 和 iOS10 。你会学习如何使用 spring 动画、转换、关键帧动画、CALayer 动画、自动布局约束动画、view controller 转换动画等等!
希望你喜欢本教程,如果有任何问题和建议,请在下面留言!
相关文章推荐
- AnimatedTransitionGallery 是 iOS 7 自定义动画转换画廊应用。
- C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(三十六)地图自定义切片与导出
- [iOS]实现了一套自定义动画库
- ios开发学习--动画(Animation)效果源码分享--系列教程
- iOS培训教程——介绍CATransition创建动画
- IOS 自定义presentModalViewController动画
- Word入门动画教程125:保存自定义列表样式
- 【教程】三种方法将GIF动画转换成…
- ios开发学习--动画(Animation)效果源码分享--系列教程2
- ios开发学习--动画(Animation)效果源码分享--系列教程1
- iOS7教程系列:自定义导航转场动画以及更多
- 【iOS-Cocos2d游戏开发之二十一 】自定义精灵类并为你的精灵设置攻击帧(指定开始帧)以及扩展Cocos2d源码的CCAnimation简化动画创建!
- 【iOS-Cocos2d游戏开发之二十一 】自定义精灵类并为你的精灵设置攻击帧(指定开始帧)以及扩展Cocos2d源码的CCAnimation简化动画创建!
- C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(三十六)地图自定义切片与导出
- ios开发学习--动画(Animation)效果源码分享--系列教程2
- C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(三十六)地图自定义切片与导出
- 由ios学到的C++用户自定义转换
- iOS学习之自定义弹出UIPickerView或UIDatePicker(动画效果)
- <iOS>解决自定义相机的打开时动画不一致问题
- iOS7教程系列:自定义导航转场动画以及更多