我们先来看看效果
实现原理
你可以先在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,谢谢。