引言
簡(jiǎn)單的模態(tài)化present
和 dismiss
的自定義轉(zhuǎn)場(chǎng)。
一荣堰、準(zhǔn)備工作
- 準(zhǔn)備一個(gè)繼承自
NSObject
并且遵守UIViewControllerTransitioningDelegate
和UIViewControllerAnimatedTransitioning
協(xié)議的一個(gè)類 -YSTrafficPopUpsAnimator
褐桌。 - 準(zhǔn)備一個(gè)繼承自
UIViewController
的類 -YSTrafficPopUpsViewController
,聲明并且創(chuàng)建YSTrafficPopUpsAnimator
的一個(gè)屬性 -animator
。 - 重寫(xiě)或者自定義初始化方法览露,將
YSTrafficPopUpsViewController
的modalPresentationStyle
屬性設(shè)置為.custom
, 將animator
賦值給YSTrafficPopUpsViewController
的transitioningDelegate
屬性所灸。
二丽惶、協(xié)議的介紹
- 1. UIViewControllerTransitioningDelegate – 描述 ViewController 轉(zhuǎn)場(chǎng)的
// 返回一個(gè)處理 present 動(dòng)畫(huà)過(guò)渡的對(duì)象
optional func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController) -> UIViewControllerAnimatedTransitioning?
// 返回一個(gè)處理 dismiss動(dòng)畫(huà)過(guò)渡的對(duì)象
optional func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
// 返回一個(gè)處理 present 手勢(shì)過(guò)渡的對(duì)象
optional func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?
// 返回一個(gè)處理 dismiss手勢(shì)過(guò)渡的對(duì)像
optional func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?
// 使用UIModalPresentationStyle.custom呈現(xiàn)樣式呈現(xiàn)視圖控制器時(shí),
// 系統(tǒng)將調(diào)用此方法爬立,并要求管理您的自定義樣式的呈現(xiàn)控制器钾唬。
// 如果實(shí)現(xiàn)此方法,請(qǐng)使用它來(lái)創(chuàng)建并返回要用于管理演示過(guò)程的自定義演示控制器對(duì)象懦尝。
// 如果您未實(shí)現(xiàn)此方法知纷,或者此方法的實(shí)現(xiàn)返回nil,則系統(tǒng)將使用默認(rèn)的表示控制器對(duì)象陵霉。
// 默認(rèn)的表示控制器不會(huì)向視圖層次結(jié)構(gòu)添加任何視圖或內(nèi)容琅轧。
optional func presentationController(forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController) -> UIPresentationController?
2. UIViewControllerAnimatedTransitioning – 定義動(dòng)畫(huà)內(nèi)容的
// 返回動(dòng)畫(huà)過(guò)渡的時(shí)間
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
// 所有的過(guò)渡動(dòng)畫(huà)事務(wù)都在這個(gè)方法里面完成
func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
// 當(dāng)您想使用可中斷的動(dòng)畫(huà)器對(duì)象(例如UIViewPropertyAnimator對(duì)象)執(zhí)行轉(zhuǎn)換時(shí),請(qǐng)實(shí)現(xiàn)此方法踊挠。 您必須在過(guò)渡期間返回相同的動(dòng)畫(huà)師對(duì)象乍桂。
optional func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating
// 過(guò)渡完成后冲杀,使用此方法執(zhí)行過(guò)渡動(dòng)畫(huà)師所需的任何最終清理操作。
optional func animationEnded(_ transitionCompleted: Bool)
3.UIViewControllerContextTransitioning – 表示動(dòng)畫(huà)上下文的
// 動(dòng)畫(huà)過(guò)渡應(yīng)在其中進(jìn)行的視圖睹酌。所有需要執(zhí)行動(dòng)畫(huà)的視圖必須在這個(gè)視圖里進(jìn)行权谁。
var containerView: UIView { get }
// 獲取轉(zhuǎn)場(chǎng)前后視圖控制器的方法
func viewController(forKey key: UITransitionContextViewControllerKey) -> UIViewController?
// 獲取轉(zhuǎn)場(chǎng)前后視圖的方法
func view(forKey key: UITransitionContextViewKey) -> UIView?
// 通知系統(tǒng)過(guò)渡動(dòng)畫(huà)已完成。在動(dòng)畫(huà)之行完成之后憋沿,一定要調(diào)用此方法旺芽。
func completeTransition(_ didComplete: Bool)
注:這些是協(xié)議常用的屬性和方法,其他的暫不研究辐啄。
三采章、ViewController 和Animator 的關(guān)系以及上述三個(gè)協(xié)議之間的關(guān)系流程
1. ViewController
不處理任何有關(guān)轉(zhuǎn)場(chǎng)動(dòng)畫(huà)的操作,跟平常使用的UIViewController
一樣壶辜。Animator
負(fù)責(zé)present
和dismiss
時(shí)的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)悯舟。Animator
對(duì)象是 賦值給viewController
的transitioningDelegate
的屬性,用于自定義轉(zhuǎn)場(chǎng)砸民。
2. 先執(zhí)行UIViewControllerTransitioningDelegate
中的方法抵怎,再執(zhí)行 UIViewControllerAnimatedTransitioning
中的方法,因此岭参,在animateTransition
方法中可以知道什么時(shí)候是present
反惕,什么時(shí)候是dismiss
。
3. UIViewControllerContextTransitioning
是 animateTransition
方法的參數(shù)冗荸,通過(guò)它承璃,可以獲取containerView
, toView
蚌本,fromView
盔粹, toViewController
,fromViewController
等一系列需要的控件和方法程癌。
4. 在animateTransition
方法中就可以自定義 present
和 dismiss
的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)舷嗡。
四、代碼的實(shí)現(xiàn)
需求:查看一組圖像嵌莉,可以左劃右劃查看进萄,可以點(diǎn)擊按鈕可以跳到下一張或者上一張。
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()
}
// 注意:我在這個(gè)地方配置了 animator對(duì)象需要的東西锐峭。
func configuraAnimator() {
animator?.maskView = maskView
animator?.nextButton = nextButton
animator?.previousButton = previousButton
animator?.pageControl = pageControl
animator?.collectionView = collectionView
}
}
// MARK: - 監(jiān)聽(tīng)方法
@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: - 設(shè)置 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 修飾的屬性都是待會(huì)兒自定義轉(zhuǎn)場(chǎng)動(dòng)畫(huà)時(shí)需要用到的控件中鼠,用 weak 修飾是為了避免產(chǎn)生循環(huán)引用。*/
weak var maskView: UIView?
weak var collectionView: UICollectionView?
weak var pageControl: UIPageControl?
weak var nextButton: UIButton?
weak var previousButton: UIButton?
var isPresenting: Bool = false
// 實(shí)現(xiàn) UIViewControllerTransitioningDelegate 的方法
// 注意:實(shí)現(xiàn)這兩個(gè)方法就可以知道什么時(shí)候做 present 操作沿癞,什么時(shí)候做 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
}
}
// 遵守并實(shí)現(xiàn) UIViewControllerAnimatedTransitioning 協(xié)議
extension YSTrafficPopUpsAnimator: UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
0.5
}
// 在這個(gè)方法進(jìn)行轉(zhuǎn)場(chǎng)動(dòng)畫(huà)
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)
六椎扬、總結(jié)
其實(shí)主要理解了第三點(diǎn)ViewController 和Animator 的關(guān)系以及上述三個(gè)協(xié)議之間的關(guān)系流程之后
惫搏,就可以知道自定義轉(zhuǎn)場(chǎng)動(dòng)畫(huà)的大概實(shí)現(xiàn)思路具温。