Skip to content

View Controller Transition PartIII

seedante edited this page Jul 4, 2016 · 3 revisions

本文为全篇第三部分。

全篇目录

第一部分:PartI Link(以下目录无法跳转,请点击该链接查看内容)

第二部分:PartII Link(以下目录无法跳转,请点击该链接查看内容)

本部分的目录:

插曲:UICollectionViewController 布局转场

与三大主流转场不同,布局转场只针对 CollectionViewController 搭配 NavigationController 的组合,且是作用于布局,而非视图。采用这种布局转场时,NavigationController 将会用布局变化的动画来替代 push 和 pop 的默认动画。苹果自家的照片应用中的「照片」Tab 页面使用了这个技术:在「年度-精选-时刻」几个时间模式间切换时,CollectionViewController 在 push 或 pop 时尽力维持在同一个元素的位置同时进行布局转换。

布局转场的实现比三大主流转场要简单得多,只需要满足四个条件:NavigationController + CollectionViewController, 且要求后者都拥有相同数据源, 并且开启useLayoutToLayoutNavigationTransitions属性为真。

let cvc0 = UICollectionViewController(collectionViewLayout: layout0)
//作为 root VC 的 cvc0 的该属性必须为 false,该属性默认为 false。
cvc0.useLayoutToLayoutNavigationTransitions = false
let nav = UINavigationController(rootViewController: cvc0)
//cvc0, cvc1, cvc2 必须具有相同的数据,如果在某个时刻修改了其中的一个数据源,其他的数据源必须同步,不然会出错。
let cvc1 = UICollectionViewController(collectionViewLayout: layout1)
cvc1.useLayoutToLayoutNavigationTransitions = true
nav.pushViewController(cvc1, animated: true)

let cvc2 = UICollectionViewController(collectionViewLayout: layout2)
cvc2.useLayoutToLayoutNavigationTransitions = true
nav.pushViewController(cvc2, animated: true)

nav.popViewControllerAnimated(true)
nav.popViewControllerAnimated(true)

Push 进入控制器栈后,不能更改useLayoutToLayoutNavigationTransitions的值,否则应用会崩溃。当 CollectionView 的数据源(section 和 cell 的数量)不完全一致时,push 和 pop 时依然会有布局转场动画,但是当 pop 回到 rootVC 时,应用会崩溃。可否共享数据源保持同步来克服这个缺点?测试表明,这样做可能会造成画面上的残缺,以及不稳定,建议不要这么做。

此外,iOS 7 支持 UICollectionView 布局的交互转换(Layout Interactive Transition),过程与控制器的交互转场(ViewController Interactive Transition)类似,这个功能和布局转场(CollectionViewController Layout Transition)容易混淆,前者是在自身布局转换的基础上实现了交互控制,后者是 CollectionViewController 与 NavigationController 结合后在转场的同时进行布局转换。感兴趣的话可以看这个功能的文档

布局转场不支持交互控制。Demo 地址:CollectionViewControllerLayoutTransition

有人告诉我使用这个布局转场无法达到官方应用里「年度-精选-时刻」里转场的效果,我发现这是苹果又把好东西自己偷着用而只公开一个阉割版本的 API。到底怎么回事?「照片」应用在这个转场里使用了弹簧动画效果,实际上这是对动画的时间曲线进行了设置,但是没有公开的接口,我们使用的这个布局转场的时间曲线是线性的,呈现出来的效果却差了不少。要实现官方的原生效果,还是要在 NavigationController 里自定义转场来控制时间曲线,也就是要使用弹簧动画。

UICollectionView 使用下面的方法对布局的切换添加动画:

func setCollectionViewLayout(_ layout: UICollectionViewLayout, animated animated: Bool)

非常幸运的是,这个方法能够在 UIView Animation 下使用,不然,你要重写整个布局动画。接下来的问题是怎么利用普通的 NavigationController 转场来重现布局转场。大体思路是:toCollectionViewController 以 fromCollectionViewController 的布局初始化,并且调整其下 toCollectionView 可视区域与 fromCollectionView 一致,在转场里执行布局切换的动画。还有一个问题是,怎么调整可视区域一致?这个其实不复杂,调整contentOffset属性即可,这是 UIScrollView 的特性。

//确保 toCollectionVC 的布局和数据源和 fromColletionVC 一致。
let toCollectionVC = ...  
//设置 contentOffset 使得两者的可视区域一致。      
toCollectionVC.collectionView?.contentOffset = (fromCollectionVC.collectionView?.contentOffset)!
//必须有这行代码,它的作用是强制 toCollectionView 刷新内容,使得上面的代码生效。
toCollectionVC.collectionView?.layoutIfNeeded()
//一切准备妥当,Push!
fromCollectionVC.navigationController?.pushViewController(toCollectionVC, animated: true)

在动画控制器里:

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    let containerView = transitionContext.containerView()
    let toCollectionVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! UICollectionViewController
    
    containerView?.addSubview(toCollectionVC.view)
    //这个弹簧动画其实做不到官方的效果,因为这是个残缺的 API,恩,官方放出的阉割版。
    //iOS 9 里才推出了完整版的弹簧动画的 API,CASpringAnimation,而且是个 Core Animation API,连文档都没有这个类的描述。
    //即使使用了 CASpringAnimation 这个完整版的 API,参数的设置也是很麻烦的事情,你得先去温习高中物理知识。
    UIView.animateWithDuration(1, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 10, options: UIViewAnimationOptions.CurveEaseIn, animations: {
        //这里只示范了 Push 操作。
        if self.operation == .Push{
            ...
            toCollectionVC.collectionView?.setCollectionViewLayout(finalLayout, animated: true)
        }
        
        }, completion: {_ in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
    })
}

进阶

尽管我在前面做出了解释,你是否依然嫌弃本文中示范的动画效果太过简单并且对我的解释表示怀疑?再次表示,「在动画控制器里,参与转场的视图只有 fromView 和 toView 之分,与转场方式无关。」所有的转场动画里转场的部分都相差无几,能不能写出炫酷的转场动画就看你能不能写出那样炫酷的动画了。因此,学习了前面的内容后并不能帮助你立马就能够实现 Github 上那些热门的转场动画,它们成为热门的原因在于动画本身,与转场本身关系不大,但它们与转场结合后就有了神奇的力量。那学习了作为进阶的本章能立马实现那些热门的转场效果吗?有可能,有些效果其实很简单,一点就透,还有一些效果涉及的技术属于本文主题之外的内容,我会给出相关的提示就不深入了。

本章的进阶分为两个部分:

  1. 案例分析:动画的方式非常多,有些并不常见,有些只是简单到令人惊讶的组合,只是你不曾了解过所以不知道如何实现,一旦了解了就不再是难事。尽管这些动画本身并不属于转场技术这个主题,但与转场结合后往往有着惊艳的视觉效果,这部分将提供一些实现此类转场动画的思路,技巧和工具来扩展视野。有很多动画类型我也没有尝试过,可能的话我会继续更新一些有意思的案例。
  2. 自定义容器转场:官方支持四种方式的转场,而且这些也足以应付绝大多数需求了,但依然有些地方无法顾及。本文一直通过探索转场的边界的方式来总结使用方法以及陷阱,在本文的压轴部分,我们将挣脱系统的束缚来实现自定义容器控制器的转场效果。

案例分析

动画的持续时间一般不超过0.5秒,稍纵即逝,有时候看到一个复杂的转场动画也不容易知道实现的方式,我一般是通过逐帧解析的手法来分析实现的手段:开源的就运行一下,使用系统自带的 QuickPlayer 对 iOS 设备进行录屏,再用 QuickPlayer 打开视频,按下 cmd+T 打开剪辑功能,这时候就能查看每一帧了;Gif 等格式的原型动画的动图就直接使用系统自带的 Preview 打开看中间帧。

子元素动画

当转场动画涉及视图中的子视图时,往往无法依赖第三方的动画库来实现,你必须为这种效果单独定制,神奇移动就是一个典型的例子。神奇移动是 Keynote 中的一个动画效果,如果某个元素在连续的两页 Keynote 同时存在,在页面切换时,该元素从上一页的位置移动到下一页的位置,非常神奇。在转场中怎么实现这个效果呢?最简单的方法是截图配合移动动画:伪造那个元素的视图添加到 containerView 中,从 fromView 中的位置移动到 toView 中的位置,这期间 fromView 和 toView 中的该元素视图隐藏,等到移动结束恢复 toView 中该元素的显示,并将伪造的元素视图从 containerView 中移除。

UIView 有几个convert方法用于在不同的视图之间转换坐标:

func convertPoint(_ point: CGPoint, toView view: UIView?) -> CGPoint
func convertPoint(_ point: CGPoint, fromView view: UIView?) -> CGPoint
func convertPoint(_ point: CGPoint, fromView view: UIView?) -> CGPoint
func convertPoint(_ point: CGPoint, fromView view: UIView?) -> CGPoint

对截图这个需求,iOS 7 提供了趁手的工具,UIView Snapshot API:

func snapshotViewAfterScreenUpdates(_ afterUpdates: Bool) -> UIView
//获取视图的部分内容
func resizableSnapshotViewFromRect(_ rect: CGRect, afterScreenUpdates afterUpdates: Bool, withCapInsets capInsets: UIEdgeInsets) -> UIView

afterScreenUpdates参数值为true时,这两个方法能够强制视图立刻更新内容,同时返回更新后的视图内容。在 push 或 presentation 中,如果 toVC 是 CollectionViewController 并且需要对 visibleCells 进行动画,此时动画控制器里是无法获取到的,因为此时 collectionView 还未向数据源询问内容,执行此方法后能够达成目的。UIView 的layoutIfNeeded()也能要求立即刷新布局达到同样的效果。

Mask 动画

MaskAnimtion

左边的动画教程:How To Make A View Controller Transition Animation Like in the Ping App;右边动画的开源地址:BubbleTransition

Mask 动画往往在视觉上令人印象深刻,这种动画通过使用一种特定形状的图形作为 mask 截取当前视图内容,使得当前视图只表现出 mask 图形部分的内容,在 PS 界俗称「遮罩」。UIView 有个属性maskView可以用来遮挡部分内容,但这里的效果并不是对maskView的利用;CALayer 有个对应的属性mask,而 CAShapeLayer 这个子类搭配 UIBezierPath 类可以实现各种不规则图形。这种动画一般就是 mask + CAShapeLayer + UIBezierPath 的组合拳搞定的,实际上实现这种圆形的形变是很简单的,只要发挥你的想象力,可以实现任何形状的形变动画。

这类转场动画在转场过程中对 toView 使用 mask 动画,不过,右边的这个动画实际上并不是上面的组合来完成的,它的真相是这样:

Truth behind BubbleTransition

这个开发者实在是太天才了,这个手法本身就是对 mask 概念的应用,效果卓越,但方法却简单到难以置信。关于使用 mask + CAShapeLayer + UIBezierPath 这种方法实现 mask 动画的方法请看我的这篇文章

高性能动画框架

有些动画使用 UIView 的动画 API 难以实现,或者难以达到较好的性能,又或者两者皆有,幸好我们还有其他选择。StartWar 使用更底层的 OpenGL 框架来解决性能问题以及 Objc.io 在探讨转场这个话题时使用 GPUImage 定制动画都是这类的典范。在交互控制器章节中提到过,官方只能对 UIView 动画 API 实现的转场动画实施完美的交互控制,这也不是绝对的,接下来我们就来挑战这个难题。

自定义容器控制器转场

压轴环节我们将实现这样一个效果:

ButtonTransition ContainerVC Interacitve Transition

Demo 地址:CustomContainerVCTransition

分析一下思路,这个控制器和 UITabBarController 在行为上比较相似,只是 TabBar 由下面跑到了上面。我们可以使用 UITabBarController 子类,然后打造一个伪 TabBar 放在顶部,原来的 TabBar 则隐藏,行为上完全一致,使用 UITabBarController 子类的好处是可以减轻实现转场的负担,不过,有时候这样的子类不是你想要的,UIViewController 子类能够提供更多的自由度,好吧,一个完全模仿 UITabBarController 行为的 UIViewController 子类,实际上我没有想到非得这样做的原因,但我想肯定有需要定制自己的容器控制器的场景,这正是本节要探讨的。Objc.io 也讨论过这个话题,文章的末尾把实现交互控制当做作业留了下来。珠玉在前,我就站在大牛的肩上继续这个话题吧。Objc.io 的这篇文章写得较早使用了 Objective-C 语言,如果要读者先去读这篇文章再继续读本节的内容,难免割裂,所以本节还是从头讨论这个话题吧,最终效果如上面所示,在自定义的容器控制器中实现交互控制切换子视图,也可以通过填充了 UIButton 的 ButtonTabBar 来实现 TabBar 一样行为的 Tab 切换,在通过手势切换页面时 ButtonTabBar 会实现渐变色动画。ButtonTabBar 有很大扩展性,改造或是替换为其他视图还是有很多应用场景的。

这章剩下的内容绝大多数人用不上,而且很占篇幅,我放到另外一个页面去了,如有兴趣,请来这里阅读:自定义容器控制器转场

尾声:转场动画的设计

虽然我不是设计师,但还是想在结束之前聊一聊我对转场动画设计的看法。动画的使用无疑能够提升应用的体验,但仅限于使用了合适的动画。

除了一些加载动画可以炫酷华丽极尽炫技之能事,绝大部分的日常操作并不适合使用过于炫酷或复杂的动画,比如 VCTransitionsLibrary 这个库里的大部分效果。该库提供了多达10种转场效果,从技术上讲,大部分效果都是针对 transform 进行动画,如果你对这些感兴趣或是恰好有这方面的使用需求,可以学习这些效果的实现,从代码角度看,封装技巧也很值得学习,这个库是学习转场动画的极佳范例;不过从使用效果上看,这个库提供的效果像 PPT 里提供的动画效果一样,绝大部分都应该避免在日常操作中使用。不过作为开发者,我们应该知道技术实现的手段,即使这些效果并不适合在绝大部分场景中使用。

场景转换的目的是过渡到下一个场景,在操作频繁的日常场景中使用复杂的过场动画容易造成视觉疲劳,这种情景下使用简单的动画即可,实现起来非常简单,更多的工作往往是怎么把它们与其他特性更好地结合起来,正如 FDFullscreenPopGesture 做的那样。除了日常操作,也会遇到一些特殊的场景需要定制复杂的转场动画,这种复杂除了动画效果本身的复杂,这需要掌握相应的动画手段,也可能涉及转场过程的配合,这需要对转场机制比较熟悉。比如 StarWars,这个转场动画在视觉上极其惊艳,一出场便获得上千星星的青睐,它有贴合星战内涵的创意设计和惊艳的视觉表现,以及优秀的性能优化,如果要评选年度转场动画甚至是史上最佳,我会投票给它;而我在本文里实现的范例,从动画效果来讲,都是很简单的,可以预见本文无法吸引大众的转发,压轴环节里的自定义容器控制器转场也是如此,但是后者需要熟知转场机制才能实现。从这点来看,转场动画在实际使用中走向两个极端:日常场景中的转场动画十分简单,实现难度很低;特定场景的转场动画可能非常复杂,不过实现难度并不能一概而论,正如我在案例分析一节里指出的几个案例那样。

希望本文能帮助你。

第一部分:转场机制、非交互转场

第二部分:交互转场以及 iOS 10 的新特性

版权申明以及其他

版权申明:我已将本文在微信公众平台的发表权「独家代理」给 iOS 开发(iOSDevTips)微信公共帐号。扫码关注「iOS 开发」:

二维码

关于 ViewController Transition 这个主题,我其实不愿意使用转场这个翻译,不过使用字面翻译控制器转换也不方便,而且由于 objccn.io 的影响,大家都在用这个翻译,为了统一我还是使用了这个翻译,还有 context,每次遇到这个词,大家都是翻译成上下文,我个人不喜欢这个翻译。老实说,我觉得大家交流技术时对于专业词汇还是尽量用英文表达,不容易出错。另外关于这个话题的延伸阅读,我还是推荐前言里提到的官方文档:View Controller Programming Guide for iOS

如心血来潮,请随意赞赏^_^。但是要在支付宝里加我好友,这让我很困扰,支付宝好友还是算了。希望你是出于文章写得好或者帮到你的原因给我打赏,而非其他,因为打赏要加我好友还是算了。有问题,在我的简书的本文章页面下留言就好,这是最有效的交流方式。

AlipayDonate