iOS实现Pinterest的转场动画
我们先来看看效果
实现原理
你可以先在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
,分别是PinterestViewControlle
r和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,谢谢。