引言
简单的模态化present
和 dismiss
的自定义转场。
一、准备工作
- 准备一个继承自
NSObject
并且遵守UIViewControllerTransitioningDelegate
和UIViewControllerAnimatedTransitioning
协议的一个类 -YSTrafficPopUpsAnimator
。 - 准备一个继承自
UIViewController
的类 -YSTrafficPopUpsViewController
,声明并且创建YSTrafficPopUpsAnimator
的一个属性 -animator
。 - 重写或者自定义初始化方法,将
YSTrafficPopUpsViewController
的modalPresentationStyle
属性设置为.custom
, 将animator
赋值给YSTrafficPopUpsViewController
的transitioningDelegate
属性。
二、协议的介绍
- 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
负责present
和dismiss
时的转场动画。Animator
对象是 赋值给viewController
的transitioningDelegate
的属性,用于自定义转场。
2. 先执行UIViewControllerTransitioningDelegate
中的方法,再执行 UIViewControllerAnimatedTransitioning
中的方法,因此,在animateTransition
方法中可以知道什么时候是present
,什么时候是dismiss
。
3. UIViewControllerContextTransitioning
是 animateTransition
方法的参数,通过它,可以获取containerView
, toView
,fromView
, toViewController
,fromViewController
等一系列需要的控件和方法。
4. 在animateTransition
方法中就可以自定义 present
和 dismiss
的转场动画。
四、代码的实现
需求:查看一组图像,可以左划右划查看,可以点击按钮可以跳到下一张或者上一张。
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 的关系以及上述三个协议之间的关系流程之后
,就可以知道自定义转场动画的大概实现思路。