Swift图片浏览器
这几天学习swift,做一个swift图片浏览器的demo。
看了网上很多浏览器的写法,感觉封装的最好的是 JXPhotoBrowser 自己也跟着学习了一下,涉及到:
-
自定义转场(present和dismiss)
-
imageView的contentMode
-
手势以及手势冲突
篇幅较长,先马后看
先看效果吧,主要是用collectionView实现 展示3.gif 加入手势(单击、双击、拖拽、捏合) 单击等.gif drag.gif自定义模态转场动画
在界面跳转的时候,指定代理为photoAnimation,我们将转场动画相关代码,全部交给这个类来完成。
photoVc.transitioningDelegate = photoAnimation
首先,我们需要了解以下几个协议:
UIViewControllerTransitioningDelegate协议
通俗来讲,返回一个实现了UIViewControllerAnimatedTransitioning协议的协议方法的对象。
并且在方法中,实现present和dismiss动画
@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协议
一组用于实现自定义视图控制器转换的动画的方法。
划重点:
在animator对象中,实现transitionDuration(使用:)方法来指定转换的持续时间,并实现animateTransition(使用:)方法来创建动画本身。
您可以提供单独的animator对象来呈现和解散视图控制器。(就是自定义present和dismiss动画)
返回动画执行的时间
public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
告诉animator执行转换动画
public func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
PS(交互会用到,这里不用):
要向视图控制器转换中添加用户交互,您必须使用animator对象和交互式animator对象——使用uiviewcontrollerinteractivetransiating协议的自定义对象。
UIViewControllerContextTransitioning协议
一组为视图控制器之间的转换动画提供上下文信息的方法
不要在自己的类中采用此协议,也不要直接创建采用此协议的对象。在转换期间,涉及到转换的animator对象从UIKit接收到一个完整配置的上下文对象。
在定义自定义animator对象时,总是检查isAnimated()方法返回的值,以确定是否应该创建动画。当你创建转换动画时,总是从一个适当的完成块调用completeTransition(_:)方法,让UIKit知道你所有的动画什么时候完成。
很明显,这个协议不需要我们自己实现,只需要在转场动画的时候,获取对应的上下文,其中:
// 充当转换中涉及的视图的父视图,相当于视图转换的容器
var containerView: UIView
//返回涉及转换的控制器(.from/.to)
func viewController(forKey: UITransitionContextViewControllerKey)
//返回涉及转换的视图(.from/.to)
func viewKey: UITransitionContextViewKey)
//通知系统过渡动画已经完成。您必须在动画完成后调用此方法,以通知系统完成转换动画。您通过的参数必须指示动画是否成功完成。这个方法的默认实现调用animator对象的animationEnded(_:)方法,让它有机会执行任何最后一分钟的清理。
func completeTransition(_ didComplete: Bool)方法
PS(交互会用到,这里不用):
当动画开始时,交互式animator对象必须保存一个指向上下文对象的指针。根据用户交互,animator对象然后调用updateInteractiveTransition(_:)、finishInteractiveTransition()或cancelInteractiveTransition()方法来报告完成动画的进度。
动画的实现细节
present具体实现
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)
//有利于后面拖拽时,设置presentView的alpha
transitionContext.containerView.backgroundColor = .black
// transitionContext.containerView.endImageRectForpresent(indexPath)
//执行动画
presentView.alpha = 0
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
tempImageView.frame = presentD.endImageRectForPresent(indexPath: indexPath)
// disView?.alpha = 0 如果直接设置为0,在后面拖拽时,不好设置alpha
}) { _ in
transitionContext.containerView.backgroundColor = .clear
//上报动画执行完毕
transitionContext.completeTransition(true)
tempImageView.removeFromSuperview()
presentView.alpha = 1
}
}
dismiss具体实现
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()
//执行动画
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
在显示图片的时候,我们会遇到长图和短图,所以在显示图片的时候,我们要设置imageView的contentMode。在demo中,最初用了两种mode
1.scaleAspectFill // contents scaled to fill with fixed aspect. some portion of content may be clipped.内容按比例缩放以填充固定的方面。
scaleAspectFill样式.png
2.scaleAspectFit // contents scaled to fit with fixed aspect. remainder is transparent内容按比例缩放以适应固定的方面。剩余部分是透明的
scaleAspectFit样式.png
最后,觉得scaleAspectFill最合适,更具有美感。
手势
//单击
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)
//捏合手势
//CollectionView是UIScorllView的子类,UIScorllView天生支持pinch捏合手势,只需要实现它的代理方法即可
//返回将要缩放的视图
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
/// 需要在缩放的时候调用
open func scrollViewDidZoom(_ scrollView: UIScrollView) {
let imageH = (imageView.image?.size.height)! / (imageView.image?.size.width)! * kScreenWidth
if imageH < kScreenHeight {
imageView.center = centerOfContentSize
}
}
其中,需要设置单击和双击的依赖关系:tap.require(toFail: doubleTap);pan手势需要添加在scrollView中,否则长图下拉时不能退出。
在进行双击图片缩放时,需要用到zoom(to: animated:),对指定frame进行缩放
@objc fileprivate func doubleClick(_ dbTap: UITapGestureRecognizer) {
// 如果当前没有任何缩放,则放大到目标比例
let scale = scrollView.maximumZoomScale
print(scale)
// 否则重置到原比例
if scrollView.zoomScale == 1.0 {
// 以点击的位置为中心,放大
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)
}
}
后来看到一篇文章中介绍这个方法:
- -(void)zoomToRect:(CGRect)rect animated:(BOOL)animate
把从scrollView里截取的矩形区域缩放到整个scrollView当前可视的frame里面。如果截取的区域大于scrollView的frame时,图片缩小,如果截取区域小于frame,会看到图片放大。一般情况下rect需要自己计算出来。即要把用户点击坐标附近的区域内容在scrollViewl里进行缩放。
拖拽手势
最初,向上滑动时,不响应手势;
//MARK: 对pan手势的处理
extension BrowseCollectionViewCell: UIGestureRecognizerDelegate{
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let pan = gestureRecognizer as? UIPanGestureRecognizer else{
return true
}
//在指定视图的坐标系中平移手势的速度。
let velocity = pan.velocity(in: self)
//向上滑动,不响应手势
if velocity.y < 0 {
return false
}
//横向滑动时,不响应Pan手势
if abs(Int(velocity.x)) > Int(velocity.y){
return false
}
//向下滑动,如果图片顶部超出可视范围,不响应
if scrollView.contentOffset.y > 0 {
return false
}
return true
}
}
根据手势的状态,决定图片的状态
@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:
//随着收拾的移动,计算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()
}
}
/// 返回拖拽的结果(包括:image的frame和透明度)
private func panResult(_ pan: UIPanGestureRecognizer) -> (CGRect, CGFloat) {
//表示拖拽点在scrollView中的位置,即拖拽的位置
let currentTouch = pan.location(in: scrollView)
// print(currentTouch)
// 拖动偏移量(距离)
//在指定视图的坐标系中平移手势的转换。
//x和y值表示随时间推移的总平移量。它们不是上次报告转换时的delta值。在首次识别手势时,将转换值应用于视图的状态——不要在每次调用处理程序时将值连接起来。
let translation = pan.translation(in: scrollView)
// print("This is a test\(translation)")
// 由下拉的偏移值决定缩放比例,越往下偏移,缩得越小。scale值区间[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
// 计算x和y。保持手指在图片上的相对位置不变。
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)
}
有啥疑问,一起探讨,先写到这~~~