iOS实现Pinterest的转场动画

我们先来看看效果

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的转场动画)