iOS 自定义转场动画

引言

简单的模态化presentdismiss 的自定义转场。

一、准备工作

  1. 准备一个继承自NSObject并且遵守 UIViewControllerTransitioningDelegateUIViewControllerAnimatedTransitioning协议的一个类 - YSTrafficPopUpsAnimator
  2. 准备一个继承自 UIViewController 的类 - YSTrafficPopUpsViewController,声明并且创建 YSTrafficPopUpsAnimator 的一个属性 - animator
  3. 重写或者自定义初始化方法,将YSTrafficPopUpsViewControllermodalPresentationStyle 属性设置为.custom, 将 animator 赋值给 YSTrafficPopUpsViewControllertransitioningDelegate属性。

二、协议的介绍

  • 1. UIViewControllerTransitioningDelegate – 描述 ViewController 转场的
// 返回一个处理 present 动画过渡的对象
optional func animationController(forPresented presented: UIViewController, 
                                              presenting: UIViewController, 
                                                     source: UIViewController) -> UIViewControllerAnimatedTransitioning?

// 返回一个处理 dismiss动画过渡的对象
optional func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?

// 返回一个处理 present 手势过渡的对象
optional func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?

// 返回一个处理 dismiss手势过渡的对像
optional func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?

// 使用UIModalPresentationStyle.custom呈现样式呈现视图控制器时,
// 系统将调用此方法,并要求管理您的自定义样式的呈现控制器。 
// 如果实现此方法,请使用它来创建并返回要用于管理演示过程的自定义演示控制器对象。
// 如果您未实现此方法,或者此方法的实现返回nil,则系统将使用默认的表示控制器对象。
 // 默认的表示控制器不会向视图层次结构添加任何视图或内容。
optional func presentationController(forPresented presented: UIViewController,
                                                 presenting: UIViewController?, 
                                                     source: UIViewController) -> UIPresentationController?

2. UIViewControllerAnimatedTransitioning – 定义动画内容的

// 返回动画过渡的时间
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval

// 所有的过渡动画事务都在这个方法里面完成
func animateTransition(using transitionContext: UIViewControllerContextTransitioning)

// 当您想使用可中断的动画器对象(例如UIViewPropertyAnimator对象)执行转换时,请实现此方法。 您必须在过渡期间返回相同的动画师对象。
optional func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating

//  过渡完成后,使用此方法执行过渡动画师所需的任何最终清理操作。
optional func animationEnded(_ transitionCompleted: Bool)

3.UIViewControllerContextTransitioning – 表示动画上下文的

// 动画过渡应在其中进行的视图。所有需要执行动画的视图必须在这个视图里进行。
var containerView: UIView { get }

// 获取转场前后视图控制器的方法
func viewController(forKey key: UITransitionContextViewControllerKey) -> UIViewController?

// 获取转场前后视图的方法
func view(forKey key: UITransitionContextViewKey) -> UIView?

// 通知系统过渡动画已完成。在动画之行完成之后,一定要调用此方法。
func completeTransition(_ didComplete: Bool)

注:这些是协议常用的属性和方法,其他的暂不研究。

三、ViewController 和Animator 的关系以及上述三个协议之间的关系流程

1. ViewController不处理任何有关转场动画的操作,跟平常使用的UIViewController 一样。Animator 负责presentdismiss 时的转场动画。Animator对象是 赋值给viewControllertransitioningDelegate的属性,用于自定义转场。
2. 先执行UIViewControllerTransitioningDelegate 中的方法,再执行 UIViewControllerAnimatedTransitioning中的方法,因此,在animateTransition方法中可以知道什么时候是present,什么时候是dismiss
3. UIViewControllerContextTransitioninganimateTransition 方法的参数,通过它,可以获取containerViewtoViewfromViewtoViewControllerfromViewController等一系列需要的控件和方法。
4. 在animateTransition 方法中就可以自定义 presentdismiss的转场动画。

四、代码的实现

需求:查看一组图像,可以左划右划查看,可以点击按钮可以跳到下一张或者上一张。
1.控制器代码:

import UIKit

private let imageCellId = "imageCellId"

class YSTrafficPopUpsViewController: UIViewController {
    // MARK: - 属性
    private var maskView = UIView()
    private var collectionView: UICollectionView?
    private var pageControl: UIPageControl?
    private var nextButton: UIButton?
    private var previousButton: UIButton?
    
    private var imageURLStrings: [String]?
    private var animator: YSTrafficPopUpsAnimator?
    
    // MARK: - 初始化
    init(imageURLStrings: [String]?) {
        super.init(nibName: nil, bundle: nil)
        
        self.imageURLStrings = imageURLStrings
        animator = YSTrafficPopUpsAnimator()
        
        modalPresentationStyle = .custom
        transitioningDelegate = animator
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        configuraAnimator()
    }
    // 注意:我在这个地方配置了 animator对象需要的东西。
    func configuraAnimator() {
        animator?.maskView = maskView
        animator?.nextButton = nextButton
        animator?.previousButton = previousButton
        animator?.pageControl = pageControl
        animator?.collectionView = collectionView
    }
}

// MARK: - 监听方法
@objc private extension YSTrafficPopUpsViewController {
    
    func closeViewController() {
        dismiss(animated: true, completion: nil)
    }
    
    func nextClick() {
        let currentPage = pageControl?.currentPage ?? 0
        let index = currentPage + 1
        
        if index >= imageURLStrings?.count ?? 0 {
            return
        }
        
        collectionView?.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: true)
    }
    
    func previousClick() {
        let currentPage = pageControl?.currentPage ?? 0
        let index = currentPage - 1
        
        if index < 0 {
            return
        }
        
        collectionView?.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: true)
    }
}

// MARK: - 设置 UI 界面
private extension YSTrafficPopUpsViewController {
    func setupUI() {
        setupMaskView()
        setupCollectionView()
        setupPageControl()
        setupButtons()
    }
    
    func setupMaskView() {
        let tapGes = UITapGestureRecognizer(target: self, action: #selector(closeViewController))
        maskView.addGestureRecognizer(tapGes)
        
        view.addSubview(maskView)
        maskView.frame = view.bounds
    }
    
    func setupButtons() {
        if imageURLStrings?.count ?? 0 <= 1 {
            return
        }
        
        nextButton = UIButton(target: self,
                              action: #selector(nextClick),
                              backImageName: "traffic_next_icon",
                              backSelectedImageName: "traffic_next_icon")
        previousButton = UIButton(target: self,
                                  action: #selector(previousClick),
                                  backImageName: "traffic_previous_icon",
                                  backSelectedImageName: "traffic_previous_icon")
        
        view.addSubview(nextButton!)
        view.addSubview(previousButton!)
        
        nextButton?.snp.makeConstraints({ (make) in
            make.centerY.equalTo(collectionView!)
            make.right.equalTo(collectionView!).offset(180)
        })
        
        previousButton?.snp.makeConstraints({ (make) in
            make.centerY.equalTo(collectionView!)
            make.left.equalTo(collectionView!).offset(-180)
        })
        
    }
    
    func setupPageControl() {
        if imageURLStrings?.count ?? 0 <= 1 {
            return
        }
        
        pageControl = UIPageControl()
        pageControl?.numberOfPages = imageURLStrings?.count ?? 0
        pageControl?.currentPage = 0
        pageControl?.currentPageIndicatorTintColor = UIColor(r: 238, g: 216, b: 171)
        pageControl?.pageIndicatorTintColor = .gray
        
        view.addSubview(pageControl!)
        pageControl?.snp.makeConstraints({ (make) in
            make.centerX.equalTo(view.snp.centerX)
            make.top.equalTo(collectionView!.snp.bottom).offset(-30)
        })
    }
    
    func setupCollectionView() {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView?.showsVerticalScrollIndicator = false
        collectionView?.showsHorizontalScrollIndicator = false
        collectionView?.isPagingEnabled = true
        collectionView?.scrollsToTop = false
        collectionView?.bounces = false
        collectionView?.delegate = self
        collectionView?.dataSource = self
        collectionView?.backgroundColor = .clear
        collectionView?.layer.cornerRadius = 15.5
        collectionView?.layer.masksToBounds = true
        collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: imageCellId)
        
        var imageSize: CGSize = .zero
        
        if imageURLStrings?.count != 0 {
            if let tempImage = UIImage(contentsOfFile: imageURLStrings![0]) {
                imageSize = tempImage.size
            }
        }
        
        var targerSize = CGSize.zero
        
        if imageSize != .zero {
            let ratio = imageSize.width / imageSize.height
            targerSize.height = view.bounds.height - kTabBarHeight * 2 - kTabBarHeight / 2
            targerSize.width = ratio * targerSize.height
        }
        
        view.addSubview(collectionView!)
        collectionView?.snp.makeConstraints({ (make) in
            make.centerX.equalTo(view)
            make.centerY.equalTo(view).offset((view.bounds.height + targerSize.height) / 2)
            make.size.equalTo(targerSize)
        })
    }
}

// MARK: - UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout
extension YSTrafficPopUpsViewController: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        imageURLStrings?.count ?? 0
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: imageCellId, for: indexPath)
        
        var imageView = cell.contentView.subviews.last as? UIImageView
        
        if imageView == nil {
            imageView = UIImageView()
            cell.contentView.addSubview(imageView!)
            
            imageView?.snp.makeConstraints({ (make) in
                make.edges.equalTo(cell.contentView)
            })
        }
        
        imageView?.sd_setImage(with: URL(fileURLWithPath: imageURLStrings![indexPath.item]), placeholderImage: UIImage(named: "traffic_placeholder_icon"))
        
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        collectionView.bounds.size
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        0
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        0
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let width = scrollView.bounds.width
        let offsetX = scrollView.contentOffset.x
        
        let index = (offsetX + (width * 0.5)) / width
        pageControl?.currentPage = Int(index)
    }
}

2.Animator的代码

import UIKit

class YSTrafficPopUpsAnimator: NSObject, UIViewControllerTransitioningDelegate {
    /**weak 修饰的属性都是待会儿自定义转场动画时需要用到的控件,用 weak 修饰是为了避免产生循环引用。*/
    weak var maskView: UIView?
    weak var collectionView: UICollectionView?
    weak var pageControl: UIPageControl?
    weak var nextButton: UIButton?
    weak var previousButton: UIButton?
    
    var isPresenting: Bool = false
    
    // 实现 UIViewControllerTransitioningDelegate 的方法
    // 注意:实现这两个方法就可以知道什么时候做 present 操作,什么时候做 dismiss 操作。
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        isPresenting = true
        
        return self
    }
    
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        isPresenting = false
        
        return self
    }
}
// 遵守并实现 UIViewControllerAnimatedTransitioning 协议
extension YSTrafficPopUpsAnimator: UIViewControllerAnimatedTransitioning {
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        0.5
    }
    
    // 在这个方法进行转场动画
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        isPresenting ? presentTransition(transitionContext) : dismissTransition(transitionContext)
    }
    
    func presentTransition(_ transitionContext: UIViewControllerContextTransitioning) {
        let containerView = transitionContext.containerView
        let toView = transitionContext.view(forKey: .to)
        
        containerView.addSubview(toView!)
        
        maskView?.backgroundColor = UIColor(white: 0, alpha: 0)
        
        let duration = transitionDuration(using: transitionContext)
        
        UIView.animate(withDuration: duration, animations: {
            self.maskView?.backgroundColor = UIColor(white: 0, alpha: 0.5)
            
        }, completion: nil)
        
        toView?.layoutIfNeeded()
        self.nextButton?.alpha = 0
        self.previousButton?.alpha = 0
        
        self.collectionView?.snp.updateConstraints({ (make) in
            make.centerY.equalTo(toView!)
        })
        
        UIView.animate(withDuration: duration,
                       delay: 0,
                       usingSpringWithDamping: 0.7,
                       initialSpringVelocity: 0,
                       options: .curveEaseOut,
                       animations: {
                        toView?.layoutIfNeeded()
        }) { (_) in
            
            self.nextButton?.snp.updateConstraints({ (make) in
                make.right.equalTo(self.collectionView!)
            })
            
            self.previousButton?.snp.updateConstraints({ (make) in
                make.left.equalTo(self.collectionView!)
            })
            
            UIView.animate(withDuration: duration,
                           delay: 0,
                           usingSpringWithDamping: 0.7,
                           initialSpringVelocity: 0,
                           options: .curveEaseOut,
                           animations: {
                            
                            self.nextButton?.alpha = 1
                            self.previousButton?.alpha = 1
                            toView?.layoutIfNeeded()
            }) { (_) in
                transitionContext.completeTransition(true)
            }
        }
    }
    
    func dismissTransition(_ transitionContext: UIViewControllerContextTransitioning) {
        let fromView = transitionContext.view(forKey: .from)
        let duration = transitionDuration(using: transitionContext)
        
        self.nextButton?.snp.updateConstraints({ (make) in
            make.right.equalTo(self.collectionView!).offset(180)
        })
        self.previousButton?.snp.updateConstraints({ (make) in
            make.left.equalTo(self.collectionView!).offset(-180)
        })
        
        UIView.animate(withDuration: duration,
                       delay: 0,
                       usingSpringWithDamping: 0.7,
                       initialSpringVelocity: 0,
                       options: .curveEaseOut,
                       animations: {
                        
                        self.nextButton?.alpha = 0
                        self.previousButton?.alpha = 0
                        fromView?.layoutIfNeeded()
        }) { (_) in
            
            let offset = (fromView!.bounds.height + self.collectionView!.bounds.height) / 2
            self.collectionView?.snp.updateConstraints({ (make) in
                 make.centerY.equalTo(fromView!).offset(offset)
            })
            
            UIView.animate(withDuration: duration,
                           delay: 0,
                           usingSpringWithDamping: 0.7,
                           initialSpringVelocity: 0,
                           options: .curveEaseOut,
                           animations: {
                            fromView?.layoutIfNeeded()
            }) { (_) in
                transitionContext.completeTransition(true)
            }
            
            UIView.animate(withDuration: duration, animations: {
                self.maskView?.backgroundColor = UIColor(white: 0, alpha: 0)
            }, completion: nil)
        }
    }
    
}

五、使用

let vc = YSTrafficPopUpsViewController(imageURLStrings: imageURLStrings)
present(vc, animated: true, completion: nil)

六、总结

其实主要理解了第三点ViewController 和Animator 的关系以及上述三个协议之间的关系流程之后,就可以知道自定义转场动画的大概实现思路。

你可能感兴趣的:(iOS 自定义转场动画)