美文网首页特效动画
Swift图片浏览器

Swift图片浏览器

作者: Pei丶Code | 来源:发表于2018-08-28 15:29 被阅读186次

    这几天学习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)
        }
    

    有啥疑问,一起探讨,先写到这~~~

    DEMO地址(希望star)

    相关文章

      网友评论

        本文标题:Swift图片浏览器

        本文链接:https://www.haomeiwen.com/subject/rxmpiftx.html