美文网首页iOS开发SwiftiOS记录篇项目开发技巧
自定义 push 和 pop 实现相册翻开效果(上)

自定义 push 和 pop 实现相册翻开效果(上)

作者: seedante | 来源:发表于2015-07-19 23:04 被阅读4158次

    效果预览:

    AlbumTransition.gif

    前言

    苹果自家应用 Photos 里点击相册后的动画是非常精妙的,而且是可交互的。我有类似的动画需求,上面是我自己的设计效果。本指南分上下两篇,分别探讨非交互和交互动画的实现。

    本文是将三个月前的 Demo 重构后重新写的,重构后,这个效果可以方便地在你的工程中使用,仅需添加几行代码和几个简单的设置。效果适用场景:两个UICollectionViewController类之间的 push 和 pop 操作。Demo 是个小型的相册浏览器, 这完全是基于我的需求来做的,因此在初期并没有考虑做成一个手把手教你实现这个效果的教程,不过前面说了,仅需添加几行代码就可在你的工程里使用,花上几分钟搭建一个场景照着做下来也是没问题的。另外,部分细节比较繁琐,都放进文章里就太长了,想了解的话看源代码,遇到这部分我会提示的。

    Demo 地址:SDECollectionViewAlbumTransition

    我把 iOS 里的动画分为两种:趣味动画和逻辑动画,前者比如一些加载场景的动画,用来消磨时间,怎么炫酷都可以,后者是符合场景变化的动画,符合逻辑最重要,如果还能很有趣那就更好了。我实现的效果算得上符合逻辑,离有趣或者酷还有点距离。

    如上所示,我希望呈现出打开相簿后照片飞出来的效果,这个设计是行为上的拟物,最好翻开封面时还能发出金光,NO,NO,太浮夸了,简直跟中华小当家或者国产奇幻剧开宝箱似的。当然,主要是我不知道怎么做,会做的话我就会做出来给大家看的,不过,我是不会把这种效果放在正常的产品里的,在游戏界这种效果比较常见,比如炉石里新卡牌点开时就带这种圣光效果。

    从技术上讲,以 push 为例:图片像一本相册的封面一样翻开,这是一个可用 transform 实现的 翻转动画;下一层级的视图也就是相册里的照片在封面后出现,这个效果需要缩小照片并按一定规则排列好;封面继续往左翻动,而照片则移动到预定位置并在这个过程中恢复到原大小。这个动画本质上就是个 View Controller Transition 加上多个元素协作进行动画的过程。总的来说,动画分为两个部分,首先是自定义 push 和 pop,其次是各种元素的协作。现在先攻克第一个难点,下面进入科普时间。

    View Controller Transition 视图控制器转换

    对于这个话题,我推荐:1. WWDC13 上的 Custom Transitions Using View Controllers,2.Custom Transitions on iOS,3. Objc.io 的自定义 ViewController 容器转场。以及一个自定义 transition 效果的库:VCTransitionsLibrary,可以读读代码看看这些效果怎么实现的。

    自定义 transition 类型

    View Controller Transition 是什么?其实平时你就一直能看到,在切换或是添加新的视图控制器来显示视图的时候发生的过程就是 ViewController Transition,比如 push 或 pop 一个 View Controller,在 TabBarController 中切换到其他 View Controller,以模态方式显示另外一个 View Controller。只不过,在 iOS 7 之前我们无法干涉这个过程,从 iOS 7 开始支持自定义 View Controller Transition,目前仅支持以下四种自定义类型:


    iOS 支持的的自定义视图转换类型 from WWDC13 #218

    除了最后一个是布局转换,前三种基本囊括了 iOS 中显示切换视图的全部方式:
    1.Modal 视图的显示和消失;
    2.TabBar Controller 在子视图中切换;
    3.Navigation Controller 推入和推出视图。

    其中 presentations and dismissals 只支持 UIModalPresentationFullScreen 和 UIModalPresentationCustom 这两种 Modal 视图的显示和消失。

    文章开头的效果是第三种,需要实现自定义 push 和 pop。

    Transition Protocol

    iOS 提供了几套 protocol 来满足自定义 transition 的需求。

    WWDC13#218-Custom Transition 的构成

    对以上 protocol 的解释节选自 Objc.io 的自定义 ViewController 容器转场

    iOS 7 自定义视图控制器转场的 API 基本上都是以协议的方式提供的,这也使其可以非常灵活的使用,因为你可以很简单地将它们插入到你的类中。最主要的五个组件如下:
    1.动画控制器 (Animation Controllers) 遵从UIViewControllerAnimatedTransitioning协议,并且负责实际执行动画。
    2.交互控制器 (Interaction Controllers) 通过遵从UIViewControllerInteractiveTransitioning协议来控制可交互式的转场。
    3.转场代理 (Transitioning Delegates) 根据不同的转场类型方便的提供需要的动画控制器和交互控制器。
    4.转场上下文 (Transitioning Contexts) 定义了转场时需要的元数据,比如在转场过程中所参与的视图控制器和视图的相关属性。 转场上下文对象遵从UIViewControllerContextTransitioning协议,并且这是由系统负责生成和提供的。
    5.转场协调器(Transition Coordinators) 可以在运行转场动画时,并行的运行其他动画。 转场协调器遵从UIViewControllerTransitionCoordinator协议。

    看晕了?没关系。这五个组件并不是全部都需要你提供,实现一个最简单的非交互的自定义 transition,只需要实现1和3即可,其实还会用到4,不过大部分情况下这个组件由系统提供给我们,我们只需要实现组件1和3就可以了。

    实战

    准备工作

    这篇不涉及交互过程,因此我单独做了个分支:No-Interaction-Transition,是本篇内容的最终版本;或者你还是想自己动手,使用纯色块的 Cell 就好了,几分钟就能搞定,又或者不怕再麻烦一点,提取这个分支里面 Example 文件夹里的文件替换到你的工程好了。到这里还是很简单的,如果觉得不简单,那就看看好了,把本文加入待读列表过一个月后再来学习。

    Demo 里有三个分支,默认分支是能够自动添加 pinch 手势支持 pop 操作,还是就是这篇文章的分支 No-Interaction-Transition,还有一种就是同时支持 push 和 pop 操作的 pinch 手势的分支 Pinch-Push-Pop-Transition

    下面需要你配置这样的一个场景,在此基础上逐步改造成最终的效果:在 storyboard 里放置一个UINavigationController和两个UICollectionViewController,如果你不用 storyboard,相信你也能自己搞定设置。

    使用场景

    下面使用 fromVC 和 toVC 分别代表 push 和 pop 过程涉及的源和目标UICollectionViewController,animationController 代表动画控制器,它执行真正的动画。实现一个最基本的非自定义 push,在你的 fromVC 里实现以下代理方法:

    override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
        if let toVC = self.storyboard?.instantiateViewControllerWithIdentifier("XXX") {
            /*对 toVC 做一些设置,然后 push*/
            ......
            self.navigationController?.pushViewController(toVC, animated: true)
        }
    }
    

    现在,一个最简单的场景就搭建完成了。此时,push 和 pop 都是系统替我们完成,运行程序,动画效果是 Slide。接下来,我们就把这个动画换成我设计的。

    如果你是在 storyboard 里通过拉 segue 来完成跳转,那需要你去- prepareForSegue:sender:里做一些调整了,但先别这么干,按照我的节奏来。

    接手系统 transition

    第一步,为UINavigationController提供遵守UINavigationControllerDelegate协议的对象(组件3)作为代理 delegate,在 push 和 pop 时系统会要求这个 delegate 来提供动画控制器和交互控制器;没有提供这个代理时,比如上面的情况里,系统将会使用默认的 Slide 动画。该协议的方法名很直白,其中前者必须实现,用于提供组件1来执行实际的动画,后者提供组件2实现交互动画,是可选的。
    - navigationController:animationControllerForOperation:fromViewController:toViewController:
    - navigationController:interactionControllerForAnimationController:

    新建SDENavigationControllerDelegate类作为代理,声明如下:

    在 storyboard 里拖一个 NSObject 下面图中这一块区域,然后将其类设置为SDENavigationControllerDelegate。你没看错,就是拖一个 NSObject,在你经常拖控件的地方输入 object 就能看到。如果你还不知道,恭喜,现在你又学到新知识了。

    在 storyboard 里为 navigation controller 设置 delegate

    小坑预警:如果你想在代码里设置UINavigationController的 delegate,那么viewDidLoad()并不是一个合适的地方,因为此时 ViewController 尚未被推入UINavigationControllerviewControllers栈里,通过UIViewController.navigationController得到的只是 nil。哪儿合适,在viewDidAppear()后调用的方法都可以,这么说这有点......作为一个UICollectionViewController,push 时在 didSelectCell 那个方法里最合适了。

    本文将只实现非交互的动画,可交互的动画在系列下篇讨论。在SDENavigationControllerDelegate类里实现以下方法提供动画控制器:

    func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        //需要通过是 push 还是 pop 操作来执行不同的动画,因此自定义了一个需要用操作类型来初始化的动画控制器
        let animationController = SDEPushAndPopAnimationController(operation: operation)控制器
        return animationController
    }
    

    第二步,实现上面提供的动画控制器类SDEPushAndPopAnimationController,该类遵守 UIViewControllerAnimatedTransitioning协议,需要实现以下方法:

    - transitionDuration: //提供 transition animation 的持续时间
    - animateTransition:  //执行动画的地方,最重要的方法
    - animationEnded:     //可选方法,动画完毕后调用,大部分时候用不上
    

    SDEPushAndPopAnimationController类的实现:

    class SDEPushAndPopAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
    //通过变量来保存操作类型
    private var operation: UINavigationControllerOperation
    
    init(operation: UINavigationControllerOperation){
        self.operation = operation
        super.init()
    }
    
    //返回动画执行时间,实际上 navigationBar 的动画时间也由该方法返回的时间决定。
    //所有自定义的 navigationbar transition 的动画效果都是 cross fade。
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
        return 1.0
    }
    //执行动画的地方
    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        switch operation{
        case .Push:
        /*do some thing*/
        case .Pop:
        /*do some thing also*/
        default: break
        }
    }
    

    WT...恩,暂时先这么处理吧。接下来,再次进入科普时间。

    来看看 WWDC13 Session 218 中对 ViewController Transition 的解释:


    ViewController Transition 图解

    NavigationController 维持的 ViewController 的结构和我们想象的一样,是个栈,但其对应的 View 的结构却不是这样。在 transition 结束时,fromView 被从 containerView 中被移除,如果我们没有这么做,系统会替我们完成的。这么看来,containerView 里只保留栈顶 ViewController 的视图,也就是屏幕上我们看到的那个视图。

    图中的两个状态之间的变化就发生在动画控制器的- animateTransition:方法里,不过动画的执行不限于这里,viewWillXXX, viewDidXXX等这些方法里都可以执行你想要的动画。不过,所有动画放在这里执行还有一个最最最最最重要的目的,先放结论:你想纳入交互化控制过程的动画必须在- animateTransition:里执行,而且,必须使用 UIView Animation 来实现,不要使用 Core Animation,在系列下篇里实现交互动画时会详细讨论有关细节。科普结束,返回实现过程。

    定制动画

    animateTransition:方法的原型为:

    func animateTransition(_ transitionContext: UIViewControllerContextTransitioning)
    

    该函数的参数也就是组件4,由系统提供给我们,它提供了 transition 过程中我们需要的绝大部分信息,包括参与 transition 过程的控制器以及 transition 过程的状态,最后还要将 transition 的执行结果通知给系统。

    animateTransition:方法中要做的事情主要是这样:

    func animateTransition(transitionContext:UIViewControllerContextTransitioning) {
        //由系统提供的 transitionContext 能提供大部分需要的信息,下面的,应该很好理解吧。
        let containerView = transitionContext.containerView()
        let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as? UICollectionViewController
        let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as? UICollectionViewController
        let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)
        let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
        let duration = transitionDuration(transitionContext)//这是要求实现的另外一个方法,往回看
            
        //containerView 在 transition 过程中担任 fromView 和 toView的父视图;将 toView 添加到 containerView 中,toView 才能显示在屏幕上
        containerView?.addSubview(toView!)
        UIView.animateWithDuration(duration, animations: {
            /*添加动画*/
        }, completion: { _ in
                //结束 transition 过程
                let isCancelled = transitionContext.transitionWasCancelled()
                transitionContext.completeTransition(!isCancelled)
        })
    }
    

    在很多文章里,会给你演示一些简单的动画,实际上,我们可以对当前视图 fromView 和下一屏视图 toView 做任何动画,仅限于你的想象力以及实现能力。

    VCTransitionsLibrary 这个库包含了十种效果,都是针对视图整体实现的动画,而当 transition 涉及视图中的子视图时,这个库就不适用了。比如神奇移动,就是将 fromView 上的子视图移动到 toView 上,实现思路有两种:一是,toView 出现时,将目标元素移动到源元素的位置进行遮挡,然后移动到预定位置,比较简单;二是将 fromView 和 toView 中相同子视图都隐藏,对该子视图截图并加入 toView 中作为伪装,然后将伪装的子视图移动到 toView 上的指定位置,最后移除伪装的子视图然后将隐藏的子视图恢复显示。这两个方法中很重要的一点就是无论是伪装的还是真正的子视图在开始和结束移动时的位置和大小都要吻合,不然就露馅了。

    回到这个动画,前面提到,实现交互动画,一定要使用 UIView Animation 而不是 Core Animation。而且这里的动画还涉及多个元素的配合,不同元素的动画的开始时间与持续时间都不一样,使用 UIView Animation 是没法满足这个要求的,因为常规的延迟执行手段在交互动画里没有作用,只有一个解决办法:UIView key frame animation,这里 push 和 pop 过程中的动画都是采用这种方式实现的。

    UIView.animateKeyframesWithDuration(duration, delay: 0.0, options: options, animations: {
        //添加多步动画
        self.addkeyFrameAnimationForBackgroundColorInPush(fromVC!, toVC: toVC!)
        self.addKeyFrameAnimationInPushForFakeCoverView(self.fakeCoverView)
        self.addKeyFrameAnimationOnVisibleCellsInPushToVC(toVC!)
    }, completion: { finished in
        let isCancelled = transitionContext.transitionWasCancelled()
        transitionContext.completeTransition(!isCancelled)
    })
    

    开头的效果拆分成三个动画完成:

    1.翻开封面的动画。由于 toView 里并没有封面这个元素,需要使用伪装的封面,push 时隐藏原封面的同时在 toView 上添加和原封面内容一样的视图来欺骗我们的眼睛,pop 时则将这个伪装封面翻回去,然后恢复源封面的显示。封面的第二个问题,如何保证封面在 toView 上依然保持在视觉正确的位置。这个也好解决,无论当前 collectionView 怎么移动,封面相对于 fromView.superView 和封面相对于 toView.superView 的位置是一样的,因为这两个位置都是相对于当前屏幕的位置。UIView 有一套"convertXXX"的方法用于属于同一个 UIWindow 的视图之间进行坐标的转换:

    //配合好封面上翻转和消失动画的时间
    func addKeyFrameAnimationInPushForFakeCoverView(coverView: UIView?){
        //封面是最早执行动画的元素,并且在整体动画的中途完成。
        UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0.5, animations: {
            var flipLeftTransform = CATransform3DIdentity
            flipLeftTransform.m34 = -1.0 / 500.0
            flipLeftTransform = CATransform3DRotate(flipLeftTransform, CGFloat(-M_PI), 0.0, 1.0, 0.0)
            coverView?.layer.transform = flipLeftTransform
        })
    }
    

    2.调整 visibleCells 的动画,这在 pop 时不是问题,但是在 push 时,你会发现在- animateTransition:里通过 toVC.collectionView?.visibleCells()返回的是空数组,没法获取 visibleCells 意味着我们没法对即将出现的 visibleCells 进行调整,怎么办?这个问题在三个月前将我折磨死了,可以从这篇记录里看到当时的历程,由于无法获取 visibleCells 而苦苦寻求其他办法最终却失败。解决办法的关键是从这篇教程 How to Create an iOS Book Open Animation 里得知的,使用toVC.view.snapshotViewAfterScreenUpdates(true)能够强制视图立即进行刷新,此时可以获取 visibleCells,事实上可以还有方法也可以:- layoutIfNeeded。具体对于这些 visibleCells 根据自身的 indexPath 来设置大小和位置是一件比较繁琐的事情,这部分代码放在setupVisibleCellsBeforePushToVC:里了,这里不详细讨论。

    func addKeyFrameAnimationOnVisibleCellsInPushToVC(toVC: UICollectionViewController){
        let collectionView = toVC.collectionView!
        for cell in collectionView.visibleCells(){
            //不同位置的 cell 的动画的开始时间和持续时间有些许差别,让离得中心越远的元素越早到达位置,最后的效果非常赏心悦目。这个是从上面那个库里学来的,但目前还有点瑕疵。
            let relativeStartTime = ......
            var relativeDuration =  ......
            //以渐显的方式出现在封面后,但这个效果一般
            UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0.7, animations: {
                cell.alpha = 1
            })
            //在封面完全翻开后才开始照片的动画,开始时间各有差异。
            UIView.addKeyframeWithRelativeStartTime(0.5 + relativeStartTime, relativeDuration: relativeDuration, animations: {
                cell.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1, 1)
            })
            UIView.addKeyframeWithRelativeStartTime(0.5 + relativeStartTime, relativeDuration: relativeDuration, animations: {
                cell.center = layoutAttributes!.center
            })
        }
    }
    

    3.调整视图背景色。这是个很不起眼的小地方,但可能会让你栽个大跟头。如果你设置了 toVC 的视图的背景色,动画开始时屏幕就会呈现该背景,这时候 fromView 就立刻不可见了,动画效果是非常糟糕的;这时候你或许会在 storyboard 里将 toVC 的 collectionView 的背景色调整为透明色来解决这个问题,可惜在动画结束后,背景色突然变黑,这是因为动画结束后,fromView 被移除出去了, toView 没有了背景空无一物,屏幕背景自然就变成黑色了。解决办法是,在 storyboard 里将 toVC 的 collectionView 的背景色设置为透明色,然后在 transition 过程中使用动画来进行过渡到你需要的背景色。

    func addkeyFrameAnimationForBackgroundColorInPush(fromVC: UICollectionViewController, toVC: UICollectionViewController){
        UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 1.0, animations: {
            let toCollectionViewBackgroundColor = fromVC.collectionView?.backgroundColor
            toVC.collectionView?.backgroundColor = toCollectionViewBackgroundColor
        })
    }
    

    animateTransition:执行动画之前,还有一个问题,pop 结束后要恢复被隐藏的封面,需要在 push 前保留这个被点击的封面的 indexpath 以便在 pop 结束时能够将之恢复。但又不想在UICollectionViewController添加属性,因为你让别人在自己的工程中为这个类添加这个属性还是挺麻烦的,有办法:extensition + associated object,这个技巧是从这个库学来的。为UICollectionViewController添加一个 extension,为所有的UICollectionViewController类添加下面两个属性:

    private var selectedIndexPathAssociationKey: UInt8 = 0
    private var coverRectInSuperviewKey: UInt8 = 1
    
    extension UICollectionViewController {
        //保存被选中的封面的索引
        var selectedIndexPath: NSIndexPath! {
            get {
                return objc_getAssociatedObject(self, &selectedIndexPathAssociationKey) as? NSIndexPath
            }
            set(newValue) {
                objc_setAssociatedObject(self, &selectedIndexPathAssociationKey, newValue, .OBJC_ASSOCIATION_RETAIN)
            }
        }
        //记录被选中的封面相对于屏幕的位置,这个会被传递给 toVC,以便于在 toVC 里调整 visibleCells 的位置和大小使之能够隐藏在封面后面
        var coverRectInSuperview: CGRect! {
            get {
                let value = objc_getAssociatedObject(self, &coverRectInSuperviewKey) as? NSValue
                return value?.CGRectValue()
            }
            set(newValue){
                let value = NSValue(CGRect: newValue)
                objc_setAssociatedObject(self, &coverRectInSuperviewKey, value, .OBJC_ASSOCIATION_RETAIN)
            }
        }
    } 
    

    然后要在之前的代理方法里添加一行代码:

    override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath:NSIndexPath) {
        if let toVC = self.storyboard?.instantiateViewControllerWithIdentifier("XXX") {
            self.selectedIndexPath = indexPath//记录封面索引位置
            ...
            self.navigationController?.pushViewController(toVC, animated: true)
        }
    }
    
    实现 Push

    一切准备就绪,回到动画控制器,补充剩下的部分:

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        ...
        switch operation{
        case .Push:
            //隐藏被选中的封面,同时添加伪装的封面到 toView 里
            let selectedCell = fromVC?.collectionView?.cellForItemAtIndexPath(fromVC!.selectedIndexPath)
            selectedCell?.hidden = true
            //计算伪装的位置,这个位置对于后面添加伪装的封面和调整 visibleCells 至关重要。
            let layoutAttributes = fromVC!.collectionView?.layoutAttributesForItemAtIndexPath(fromVC!.selectedIndexPath)
            let areaRect = fromVC!.collectionView?.convertRect(layoutAttributes!.frame, toView: fromVC!.collectionView?.superview)
            toVC!.coverRectInSuperview = areaRect!
            let fakeCoverView = createAndSetupFakeCoverView(fromVC!, toVC: toVC!)
    
            //强制刷新 toView,以便能够在 toVC 的collectionView 被显示之前能够获取 visibleCells。
            toVC?.view.layoutIfNeeded()
            //针对 visibleCells 调整大小和位置,以便能够隐藏在封面后面,此处比较繁琐,想知道具体实现的话可以看源码
            setupVisibleCellsBeforePushToVC(toVC!)
            //添加 toView, toView 将会出现在屏幕上
            containerView?.addSubview(toView!)
    
            UIView.setAnimationCurve(UIViewAnimationCurve.EaseOut)
            let options: UIViewKeyframeAnimationOptions = [.BeginFromCurrentState, .OverrideInheritedDuration, .CalculationModeCubic, .CalculationModeLinear]
            //key frame animation 里添加的动画的时间都是针对 duration 进行比例计算的,开始时间和持续时间的值都在0和1之间。
            UIView.animateKeyframesWithDuration(duration, delay: 0.0, options: options, animations: {
                //将上面实现的多步动画添加到这里
                self.addkeyFrameAnimationForBackgroundColorInPush(fromVC!, toVC: toVC!)
                self.addKeyFrameAnimationInPushForFakeCoverView(self.fakeCoverView)
                self.addKeyFrameAnimationOnVisibleCellsInPushToVC(toVC!)
                }, completion: { finished in
                    let isCancelled = transitionContext.transitionWasCancelled()
                    //如果 push 被取消,则将一切恢复原样,恢复原装封面的显示
                    if isCancelled{
                        selectedCell?.hidden = false
                    }
                    transitionContext.completeTransition(!isCancelled)
            })
        ...
        }
    }
    
    实现 Pop

    Pop 过程中的动画基本上是对 push 过程的逆向,唯一需要注意的地方是由于用户可能会滑动 collectionView,那么 pop 时的 visibleCells 可能和 push 时的不一样,这时候要注意调整有关计算相对位置的算法,具体可以看代码。这里有个问题,用户在滑动还没有结束时点击返回,此时的 pop 动画就露馅了,因为位置是相对于返回的那一刻在计算的,而界面依然在滑动,封面下面的照片会超出封面的范围。

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        ...
        case .Push:
            ...
        case .Pop:
             //fromVC 和 fromView 都是指代当前显示的视图控制器和视图,与操作类型是 push 还是 pop 无关。
            //需要注意的是,此时不能再简单地使用addSubview:,不然 fromView 会被挡住不可见
            containerView?.insertSubview(toView!, belowSubview: fromView!)
            //根据 tag 来获取伪装的封面
            let coverView = fromView?.viewWithTag(1000)
            UIView.setAnimationCurve(UIViewAnimationCurve.EaseInOut)
            UIView.animateKeyframesWithDuration(duration, delay: 1.0, options: UIViewKeyframeAnimationOptions(), animations: {
                //pop 过程的动画基本上是对 push 过程中动画的逆向。唯一需要注意的是,push 和 pop 时的 visibleCells 可能会不同,需要做出调整,具体看代码
                self.addkeyFrameAnimationForBackgroundColorInPop(fromVC!)
                self.addKeyFrameAnimationInPopForFakeCoverView(coverView)
                self.addKeyFrameAnimationOnVisibleCellsInPopFromVC(fromVC!)
                }, completion: { finished in
                    let isCancelled = transitionContext.transitionWasCancelled()
                    //只有 pop 过程完成了,才能恢复源封面的显示
                    if !isCancelled{
                        let selectedCell = toVC?.collectionView?.cellForItemAtIndexPath(toVC!.selectedIndexPath)
                        selectedCell?.hidden = false
                    }
                    transitionContext.completeTransition(!isCancelled)
            })
    }
    

    这样就完成了非交互动画,接下来在这里讨论下如何使用 pinch 手势来控制 push 和 pop 过程。

    说点什么

    这么一口气看下来,对刚开始接触的人来说有点困难,对有过类似经验的人来说,应该也能找到点新的东西。如果你还没有试过将这个过程交互化,那么这篇内容已经规避了大部分交互动画的陷阱,正如那些加粗显示的内容提示的那样,也正因为如此在下篇里才会显得如此轻松。三个月前的 Demo 也做了和如今大部分都相同的东西,但现在的 Demo 有着更好的解耦性,更方便使用,这也是个进步。

    参考资料:
    1. WWDC13 Session 218: Custom Transitions Using View Controllers
    2.《自定义 ViewController 容器转场》
    3.《Custom Transitions on iOS》,此文是我见过关于 ViewController Custom Transition 的最好文章,强烈推荐。

    相关文章

      网友评论

      本文标题:自定义 push 和 pop 实现相册翻开效果(上)

      本文链接:https://www.haomeiwen.com/subject/cacuqttx.html