美文网首页iOS
iOS实现Pinterest的转场动画

iOS实现Pinterest的转场动画

作者: 大泡沫 | 来源:发表于2018-08-09 22:27 被阅读17次

    我们先来看看效果

    screenshot.gif

    实现原理

    你可以先在github上下载本文的demo

    我们要实现自定义转场动画,最通用的方法就是自定义实现转场动画的类,并使这个类遵守UIViewControllerAnimatedTransitioning协议。

    Pinterest的这个转场动画主要是通过UINavigationController的push和pop实现的,所以我们的目的就是自定义push和pop的转场动画,demo中的HXPinterestTransition类就是具体实现转场动画的类,它遵守了UIViewControllerAnimatedTransitioning协议。

    实现步骤

    1、定义一个协议HXPinterestTransitionView

    这个协议的主要目的就是让需要转场动画的ViewController实现它,并管理需要做动画的View。协议很简单:

    
    // MARK: -  要实现转场动画的ViewController必须遵守此协议
    protocol HXPinterestTransitionView {
    
        func fromTransitionView() -> UIView?
    
        func toTransitionView() -> UIView?
    
    }
    
    

    2、定义HXPinterestTransition,并遵守UIViewControllerAnimatedTransitioning协议

    这是实现转场动画的核心类,本类中实现了Pinterest转场的push和pop方法。

    在效果图中,Pinterest控制器的瀑布流中点击到的cell上的imageView会放大并平移到Detail控制器的imageView的位置上,实现完美重合,在此期间,Pinterest控制器的collectionView也会跟随着放大,就好像是collectionView放大且平移着带动点击中的cell移动到Detail控制器的imageView上,整个过程非常平滑。

    所以push方法的代码是这样的:

        /// push
        private func pushAnimateTransition(using transitionContext: UIViewControllerContextTransitioning) {
            /// 首先对参数进行校验
            guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
                let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
                let fromTargetView = (fromVC as? HXPinterestTransitionView)?.fromTransitionView(),
                let toTargetView = (toVC as? HXPinterestTransitionView)?.toTransitionView() else {
                    transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
                    return
            }
            let containerView = transitionContext.containerView
            /// 计算动画view的初始frame和结束frame
            let fromFrame = fromTargetView.convert(fromTargetView.bounds, to: UIApplication.shared.keyWindow)
            let toFrame = toTargetView.convert(toTargetView.bounds, to: UIApplication.shared.keyWindow)
            let animationScale = toFrame.width / fromFrame.width
            let toScale = 1 / animationScale
            /// 定义一个UIImageView来做动画
            let snapImageView = UIImageView(image: fromTargetView.getScreenImage())
            snapImageView.frame = fromFrame
            /// 设置动画的初始状态
            toVC.view.alpha = 0
            toVC.view.transform = CGAffineTransform(scaleX: toScale, y: toScale)
            toVC.view.frame.origin = CGPoint(x: -toFrame.origin.x * toScale + fromFrame.origin.x, y: -toFrame.origin.y * toScale + fromFrame.origin.y)
            /// 添加一个白背景
            let bgView = UIView(frame: UIScreen.main.bounds)
            bgView.backgroundColor = .white
            /// 添加相应的view
            containerView.addSubview(bgView)
            containerView.addSubview(toVC.view)
            containerView.addSubview(fromVC.view)
            containerView.addSubview(snapImageView)
            
             UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: .curveEaseOut, animations: {
                /// 1. 放大snapImageView,并使snapImageView的frame.origin处于一个正确的位置
                snapImageView.transform = CGAffineTransform(scaleX: animationScale, y: animationScale)
                snapImageView.frame.origin = toFrame.origin
                /// 2. 同时放大fromVC.view,并使fromVC.view的frame.origin处于一个正确的位置, 并改变透明度
                fromVC.view.alpha = 0
                fromVC.view.transform = CGAffineTransform(scaleX: animationScale, y: animationScale)
                fromVC.view.frame.origin = CGPoint(x: -fromFrame.origin.x * animationScale + toFrame.origin.x, y: -fromFrame.origin.y * animationScale + toFrame.origin.y)
                /// 3. 还原toVC.view的状态
                toVC.view.alpha = 1
                toVC.view.transform = CGAffineTransform.identity
                toVC.view.frame = UIScreen.main.bounds
            }) { (_) in
                /// 动画结束,移除多余的view
                bgView.removeFromSuperview()
                snapImageView.removeFromSuperview()
                /// 还原fromVC.view的状态
                fromVC.view.alpha = 1
                fromVC.view.transform = CGAffineTransform.identity
                fromVC.view.frame = UIScreen.main.bounds
                /// 结束动画
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            }
            
        }
    

    代码中首先对参数进行校验,fromVC和toVC必须是要遵守了HXPinterestTransitionView且实现了相应方法的类。

    接下来就是动画的具体实现了,别看代码很长,其实结构很清晰:计算动画view的初始frame和结束frame,定义来做动画的snapImageView,并设置toVC的初始状态,并将需要在动画中展示的view添加到containerView上。在 UIView.animate方法中,主要做了三步,在代码中已经注释过了,这里就不多啰嗦了。

    然后就是pop动画了,在效果图中,pop动画的效果就好像是push动画反过来了一样,所以,实现的代码跟push的代码差别不大:

         /// pop
        private func popAnimateTransition(using transitionContext: UIViewControllerContextTransitioning) {
            guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
                let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
                let fromTargetView = (fromVC as? HXPinterestTransitionView)?.toTransitionView(),
                let toTargetView = (toVC as? HXPinterestTransitionView)?.fromTransitionView() else {
                    transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
                    return
            }
            let containerView = transitionContext.containerView
            
            let fromFrame = fromTargetView.convert(fromTargetView.bounds, to: UIApplication.shared.keyWindow)
            let toFrame = toTargetView.convert(toTargetView.bounds, to: UIApplication.shared.keyWindow)
            let animationScale = fromFrame.width / toFrame.width
            
            let snapImageView = UIImageView(image: toTargetView.getScreenImage())
            snapImageView.frame = toFrame
            snapImageView.transform = CGAffineTransform(scaleX: animationScale, y: animationScale)
            snapImageView.frame.origin = fromFrame.origin
            
            toVC.view.transform = CGAffineTransform(scaleX: animationScale, y: animationScale)
            toVC.view.frame.origin = CGPoint(x: -toFrame.origin.x * animationScale + fromFrame.origin.x, y: -toFrame.origin.y * animationScale + fromFrame.origin.y)
            
            let bgView = UIView(frame: UIScreen.main.bounds)
            bgView.backgroundColor = .white
            
            containerView.addSubview(toVC.view)
            containerView.addSubview(bgView)
            containerView.addSubview(snapImageView)
            
            UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: .curveEaseOut, animations: {
                snapImageView.transform = CGAffineTransform.identity
                snapImageView.frame.origin = toFrame.origin
                toVC.view.transform = CGAffineTransform.identity
                toVC.view.frame = UIScreen.main.bounds
                bgView.alpha = 0
            }) { (_) in
                snapImageView.removeFromSuperview()
                bgView.removeFromSuperview()
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            }
        }
    

    需要注意的是fromTargetView是fromVC.toTransitionView(),toTargetView是toVC.fromTransitionView()。

    3、定义HXPinterestTransitionManager

    为了更加易于使用,笔者还定义了一个HXPinterestTransitionManager类来专门管理是否需要执行转场动画,只需要将navigationController的delegate设置为HXPinterestTransitionManager的实例就可以了。
    代码是这样的:

    // MARK: -  为转场类定制的manager
    class HXPinterestTransitionManager: NSObject, UINavigationControllerDelegate {
        
        public func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            /// 如果fromVC和toVC都遵守HXPinterestTransitionView协议,就使用Pinterest转场动画,否则使用系统转场动画
            guard let _ = fromVC as? HXPinterestTransitionView, let _ = toVC as? HXPinterestTransitionView else { return nil }
            switch operation {
            case .push:
                return HXPinterestTransition(.push)
            case .pop:
                return HXPinterestTransition(.pop)
            case .none:
                return nil
            }
        }
        
    }
    

    4、应用场景实例

    这里有两个ViewController,分别是PinterestViewController和DetailViewController。
    在PinterestViewController中设置navigationController的代理为HXPinterestTransitionManager的实例,最好是将其设置为属性,这样可以保证在navigationController的生命周期中一直有效。

      
      private let pinterestTransitionManager = HXPinterestTransitionManager()
    
        // MARK: -  Life Cycle
        override func viewDidLoad() {
            super.viewDidLoad()
            /// 设置导航控制器代理为pinterestTransitionManager
            navigationController?.delegate = pinterestTransitionManager
            /// 其他代码
            ......
        }
    

    分别在PinterestViewController和DetailViewController中遵守HXPinterestTransitionView协议。

    // MARK: -  HXPinterestTransitionView
    extension PinterestViewController: HXPinterestTransitionView {
        
        func fromTransitionView() -> UIView? {
            ///这里取collectionView中被选中的cell
            guard let selectedItem = collectionView.indexPathsForSelectedItems?.first,
                let cell = collectionView.cellForItem(at: selectedItem) as? PintersetCell else { return nil }
            return cell.imageView
        }
        
        func toTransitionView() -> UIView? {
            return nil
        }
        
    }
    
    // MARK: -  HXPinterestTransitionView
    extension DetailViewController: HXPinterestTransitionView {
        
        func fromTransitionView() -> UIView? {
            return nil
        }
        
        func toTransitionView() -> UIView? {
            return imageView
        }
        
    }
    

    具体的实现就是这么简单,只需要设置navigationController的代码,然后在需要做动画的viewController中分别遵守HXPinterestTransitionView协议就行了。

    总结

    自定义转场动画是iOS开发中比较常见的需求,这里给各位看官提供一种思路,如果有什么错误的地方,请指正。
    github的代码在这里,如果觉得不错,请不要吝啬star,谢谢。

    相关文章

      网友评论

        本文标题:iOS实现Pinterest的转场动画

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