這幾天學(xué)習(xí)swift狭归,做一個(gè)swift圖片瀏覽器的demo铝穷。
看了網(wǎng)上很多瀏覽器的寫法,感覺(jué)封裝的最好的是 JXPhotoBrowser 自己也跟著學(xué)習(xí)了一下伴鳖,涉及到:
自定義轉(zhuǎn)場(chǎng)(present和dismiss)
imageView的contentMode
手勢(shì)以及手勢(shì)沖突
篇幅較長(zhǎng)鸿市,先馬后看
先看效果吧锯梁,主要是用collectionView實(shí)現(xiàn)自定義模態(tài)轉(zhuǎn)場(chǎng)動(dòng)畫
在界面跳轉(zhuǎn)的時(shí)候,指定代理為photoAnimation内舟,我們將轉(zhuǎn)場(chǎng)動(dòng)畫相關(guān)代碼合敦,全部交給這個(gè)類來(lái)完成。
photoVc.transitioningDelegate = photoAnimation
首先验游,我們需要了解以下幾個(gè)協(xié)議:
UIViewControllerTransitioningDelegate協(xié)議
通俗來(lái)講充岛,返回一個(gè)實(shí)現(xiàn)了UIViewControllerAnimatedTransitioning協(xié)議的協(xié)議方法的對(duì)象。
并且在方法中耕蝉,實(shí)現(xiàn)present和dismiss動(dòng)畫
@available(iOS 2.0, *)
optional public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?
@available(iOS 2.0, *)
optional public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
UIViewControllerAnimatedTransitioning協(xié)議
一組用于實(shí)現(xiàn)自定義視圖控制器轉(zhuǎn)換的動(dòng)畫的方法崔梗。
劃重點(diǎn):
在animator對(duì)象中,實(shí)現(xiàn)transitionDuration(使用:)方法來(lái)指定轉(zhuǎn)換的持續(xù)時(shí)間垒在,并實(shí)現(xiàn)animateTransition(使用:)方法來(lái)創(chuàng)建動(dòng)畫本身蒜魄。
您可以提供單獨(dú)的animator對(duì)象來(lái)呈現(xiàn)和解散視圖控制器。(就是自定義present和dismiss動(dòng)畫)
返回動(dòng)畫執(zhí)行的時(shí)間
public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
告訴animator執(zhí)行轉(zhuǎn)換動(dòng)畫
public func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
PS(交互會(huì)用到场躯,這里不用):
要向視圖控制器轉(zhuǎn)換中添加用戶交互权悟,您必須使用animator對(duì)象和交互式animator對(duì)象——使用uiviewcontrollerinteractivetransiating協(xié)議的自定義對(duì)象。
UIViewControllerContextTransitioning協(xié)議
一組為視圖控制器之間的轉(zhuǎn)換動(dòng)畫提供上下文信息的方法
不要在自己的類中采用此協(xié)議推盛,也不要直接創(chuàng)建采用此協(xié)議的對(duì)象。在轉(zhuǎn)換期間谦铃,涉及到轉(zhuǎn)換的animator對(duì)象從UIKit接收到一個(gè)完整配置的上下文對(duì)象耘成。
在定義自定義animator對(duì)象時(shí),總是檢查isAnimated()方法返回的值驹闰,以確定是否應(yīng)該創(chuàng)建動(dòng)畫瘪菌。當(dāng)你創(chuàng)建轉(zhuǎn)換動(dòng)畫時(shí),總是從一個(gè)適當(dāng)?shù)耐瓿蓧K調(diào)用completeTransition(_:)方法嘹朗,讓UIKit知道你所有的動(dòng)畫什么時(shí)候完成师妙。
很明顯,這個(gè)協(xié)議不需要我們自己實(shí)現(xiàn)屹培,只需要在轉(zhuǎn)場(chǎng)動(dòng)畫的時(shí)候默穴,獲取對(duì)應(yīng)的上下文怔檩,其中:
// 充當(dāng)轉(zhuǎn)換中涉及的視圖的父視圖,相當(dāng)于視圖轉(zhuǎn)換的容器
var containerView: UIView
//返回涉及轉(zhuǎn)換的控制器(.from/.to)
func viewController(forKey: UITransitionContextViewControllerKey)
//返回涉及轉(zhuǎn)換的視圖(.from/.to)
func viewKey: UITransitionContextViewKey)
//通知系統(tǒng)過(guò)渡動(dòng)畫已經(jīng)完成蓄诽。您必須在動(dòng)畫完成后調(diào)用此方法薛训,以通知系統(tǒng)完成轉(zhuǎn)換動(dòng)畫。您通過(guò)的參數(shù)必須指示動(dòng)畫是否成功完成仑氛。這個(gè)方法的默認(rèn)實(shí)現(xiàn)調(diào)用animator對(duì)象的animationEnded(_:)方法乙埃,讓它有機(jī)會(huì)執(zhí)行任何最后一分鐘的清理。
func completeTransition(_ didComplete: Bool)方法
PS(交互會(huì)用到锯岖,這里不用):
當(dāng)動(dòng)畫開(kāi)始時(shí)介袜,交互式animator對(duì)象必須保存一個(gè)指向上下文對(duì)象的指針。根據(jù)用戶交互出吹,animator對(duì)象然后調(diào)用updateInteractiveTransition(_:)遇伞、finishInteractiveTransition()或cancelInteractiveTransition()方法來(lái)報(bào)告完成動(dòng)畫的進(jìn)度。
動(dòng)畫的實(shí)現(xiàn)細(xì)節(jié)
present具體實(shí)現(xiàn)
fileprivate func presentAnimation(_ transitionContext: UIViewControllerContextTransitioning) {
guard let presentD = presentDelegate, let indexPath = indexPath else {
return
}
//1.取出彈出的View
guard let presentView = transitionContext.view(forKey: .to) else{ return
}
//2.加入到containerView中
transitionContext.containerView.addSubview(presentView)
//3.獲取彈出的imageView
let tempImageView = presentD.imageForPresent(indexPath: indexPath)
tempImageView.frame = presentD.startImageRectForPresent(indexPath: indexPath)
transitionContext.containerView.addSubview(tempImageView)
//有利于后面拖拽時(shí)趋箩,設(shè)置presentView的alpha
transitionContext.containerView.backgroundColor = .black
// transitionContext.containerView.endImageRectForpresent(indexPath)
//執(zhí)行動(dòng)畫
presentView.alpha = 0
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
tempImageView.frame = presentD.endImageRectForPresent(indexPath: indexPath)
// disView?.alpha = 0 如果直接設(shè)置為0赃额,在后面拖拽時(shí),不好設(shè)置alpha
}) { _ in
transitionContext.containerView.backgroundColor = .clear
//上報(bào)動(dòng)畫執(zhí)行完畢
transitionContext.completeTransition(true)
tempImageView.removeFromSuperview()
presentView.alpha = 1
}
}
dismiss具體實(shí)現(xiàn)
fileprivate func dismissAnimation(_ transitionContext: UIViewControllerContextTransitioning) {
guard let dismissD = dismissDelegate , let presentD = presentDelegate else {
return
}
//取出消失的View
guard let dismissView = transitionContext.view(forKey: .from) else {
return
}
guard let presentVC = transitionContext.viewController(forKey: .to) else {
print("predent ! error")
return
}
let presentView = presentVC.view
presentView?.alpha = 0.35
dismissView.alpha = 0
//獲取要退出的imageView
let tempImageV = dismissD.imageForDismiss()
transitionContext.containerView.addSubview(tempImageV)
//獲取將要退出的indexPath
let indexPath = dismissD.indexPathForDissmiss()
//執(zhí)行動(dòng)畫
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
tempImageV.frame = presentD.startImageRectForPresent(indexPath: indexPath)
dismissView.alpha = 0
presentView?.alpha = 1
}) {(_) in
tempImageV.removeFromSuperview()
dismissView.removeFromSuperview()
transitionContext.completeTransition(true)
}
}
ImageView的contentMode
在顯示圖片的時(shí)候叫确,我們會(huì)遇到長(zhǎng)圖和短圖跳芳,所以在顯示圖片的時(shí)候,我們要設(shè)置imageView的contentMode竹勉。在demo中飞盆,最初用了兩種mode
1.scaleAspectFill // contents scaled to fill with fixed aspect. some portion of content may be clipped.內(nèi)容按比例縮放以填充固定的方面。
2.scaleAspectFit // contents scaled to fit with fixed aspect. remainder is transparent內(nèi)容按比例縮放以適應(yīng)固定的方面次乓。剩余部分是透明的
最后吓歇,覺(jué)得scaleAspectFill最合適,更具有美感票腰。
手勢(shì)
//單擊
let tap = UITapGestureRecognizer(target: self, action: #selector(closePhototBrowser))
contentView.addGestureRecognizer(tap)
//雙擊
let doubleTap = UITapGestureRecognizer(target: self, action: #selector(doubleClick(_:)))
doubleTap.numberOfTapsRequired = 2
tap.require(toFail: doubleTap)
contentView.addGestureRecognizer(doubleTap)
//拖拽
let pan = UIPanGestureRecognizer(target: self, action: #selector(panPhotoBrowser(_:)))
pan.delegate = self as UIGestureRecognizerDelegate
scrollView.addGestureRecognizer(pan)
//捏合手勢(shì)
//CollectionView是UIScorllView的子類城看,UIScorllView天生支持pinch捏合手勢(shì),只需要實(shí)現(xiàn)它的代理方法即可
//返回將要縮放的視圖
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
/// 需要在縮放的時(shí)候調(diào)用
open func scrollViewDidZoom(_ scrollView: UIScrollView) {
let imageH = (imageView.image?.size.height)! / (imageView.image?.size.width)! * kScreenWidth
if imageH < kScreenHeight {
imageView.center = centerOfContentSize
}
}
其中杏慰,需要設(shè)置單擊和雙擊的依賴關(guān)系:tap.require(toFail: doubleTap)测柠;pan手勢(shì)需要添加在scrollView中,否則長(zhǎng)圖下拉時(shí)不能退出缘滥。
在進(jìn)行雙擊圖片縮放時(shí)轰胁,需要用到zoom(to: animated:),對(duì)指定frame進(jìn)行縮放
@objc fileprivate func doubleClick(_ dbTap: UITapGestureRecognizer) {
// 如果當(dāng)前沒(méi)有任何縮放朝扼,則放大到目標(biāo)比例
let scale = scrollView.maximumZoomScale
print(scale)
// 否則重置到原比例
if scrollView.zoomScale == 1.0 {
// 以點(diǎn)擊的位置為中心赃阀,放大
let pointInView = dbTap.location(in: imageView)
let w = scrollView.bounds.size.width / scale
let h = scrollView.bounds.size.height / scale
let x = pointInView.x - (w / 2.0)
let y = pointInView.y - (h / 2.0)
let rect = CGRect(x: x, y: y, width: w, height: h)
print(rect)
scrollView.zoom(to: CGRect(x: x, y: y, width: w, height: h), animated: true)
} else {
scrollView.setZoomScale(1.0, animated: true)
}
}
后來(lái)看到一篇文章中介紹這個(gè)方法:
- -(void)zoomToRect:(CGRect)rect animated:(BOOL)animate
把從scrollView里截取的矩形區(qū)域縮放到整個(gè)scrollView當(dāng)前可視的frame里面。如果截取的區(qū)域大于scrollView的frame時(shí)擎颖,圖片縮小榛斯,如果截取區(qū)域小于frame观游,會(huì)看到圖片放大。一般情況下rect需要自己計(jì)算出來(lái)肖抱。即要把用戶點(diǎn)擊坐標(biāo)附近的區(qū)域內(nèi)容在scrollViewl里進(jìn)行縮放备典。
拖拽手勢(shì)
最初,向上滑動(dòng)時(shí)意述,不響應(yīng)手勢(shì)提佣;
//MARK: 對(duì)pan手勢(shì)的處理
extension BrowseCollectionViewCell: UIGestureRecognizerDelegate{
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let pan = gestureRecognizer as? UIPanGestureRecognizer else{
return true
}
//在指定視圖的坐標(biāo)系中平移手勢(shì)的速度。
let velocity = pan.velocity(in: self)
//向上滑動(dòng)荤崇,不響應(yīng)手勢(shì)
if velocity.y < 0 {
return false
}
//橫向滑動(dòng)時(shí)拌屏,不響應(yīng)Pan手勢(shì)
if abs(Int(velocity.x)) > Int(velocity.y){
return false
}
//向下滑動(dòng),如果圖片頂部超出可視范圍术荤,不響應(yīng)
if scrollView.contentOffset.y > 0 {
return false
}
return true
}
}
根據(jù)手勢(shì)的狀態(tài)倚喂,決定圖片的狀態(tài)
@objc fileprivate func panPhotoBrowser(_ pan:UIPanGestureRecognizer){
guard imageView.image != nil else {
return
}
switch pan.state {
case .began:
beganFrame = imageView.frame
beganTouch = pan.location(in: scrollView)
case .changed:
//隨著收拾的移動(dòng),計(jì)算imageView和背景的alpha
//返回圖片的frame和scale
let result = panResult(pan)
imageView.frame = result.0
let alphaz: CGFloat = result.1 * result.1
self.superview?.alpha = alphaz
case .ended, .cancelled:
imageView.frame = panResult(pan).0
if pan.velocity(in: self).y > 0 {
delegate?.photoBrowserCellImageClick()
} else {
// 取消dismiss
endPan()
}
default:
endPan()
}
}
/// 返回拖拽的結(jié)果(包括:image的frame和透明度)
private func panResult(_ pan: UIPanGestureRecognizer) -> (CGRect, CGFloat) {
//表示拖拽點(diǎn)在scrollView中的位置瓣戚,即拖拽的位置
let currentTouch = pan.location(in: scrollView)
// print(currentTouch)
// 拖動(dòng)偏移量(距離)
//在指定視圖的坐標(biāo)系中平移手勢(shì)的轉(zhuǎn)換端圈。
//x和y值表示隨時(shí)間推移的總平移量。它們不是上次報(bào)告轉(zhuǎn)換時(shí)的delta值子库。在首次識(shí)別手勢(shì)時(shí)舱权,將轉(zhuǎn)換值應(yīng)用于視圖的狀態(tài)——不要在每次調(diào)用處理程序時(shí)將值連接起來(lái)。
let translation = pan.translation(in: scrollView)
// print("This is a test\(translation)")
// 由下拉的偏移值決定縮放比例仑嗅,越往下偏移宴倍,縮得越小。scale值區(qū)間[0.3, 1.0]
let scale = min(1.0, max(0.3, 1 - translation.y / bounds.height))
let width = beganFrame.size.width * scale
let height = beganFrame.size.height * scale
// 計(jì)算x和y仓技。保持手指在圖片上的相對(duì)位置不變鸵贬。
let xRate = (beganTouch.x - beganFrame.origin.x) / beganFrame.size.width
let currentTouchDeltaX = xRate * width
let x = currentTouch.x - currentTouchDeltaX
let yRate = (beganTouch.y - beganFrame.origin.y) / beganFrame.size.height
let currentTouchDeltaY = yRate * height
let y = currentTouch.y - currentTouchDeltaY
return (CGRect(x: x.isNaN ? 0 : x, y: y.isNaN ? 0 : y, width: width, height: height), scale)
}
有啥疑問(wèn),一起探討脖捻,先寫到這~~~