Swift3.0 — 从九宫格图片到大图浏览

最近经常看见从九宫格图片到大图浏览时,有个过渡动画,即从九宫格图片的位置放大到整个屏幕,,分析一下,其实主要技术点就是自定义过渡动画,并且在controller切换时获取点击图片位置。
先看下demo效果:


录屏效果.gif

效果分析

无论是九宫格页面还是大图浏览页面,都是多张图片,并且有点击效果,所以用UICollectionView来做最简单直接。无论点击九宫格图片还是大图图片,都需要记录图片当前位置,并且对应到另一页面的item,这样才能找到图片的始末frame,来进行动画。

实现页面布局

1, UICollectionViewCell,因为democell都是只展示一张图片,所以我只自能定义了ImageCollectionViewCell共用,并且设置图片为layercontents,(需要交互、图片不沾满整个cell、或者需要添加其他控件的话可以使用UIImageView):
class ImageCollectionViewCell: UICollectionViewCel {

  lazy var imgView: UIImageView = {
      let imgView: UIImageView = UIImageView(frame: self.contentView.bounds)
      imgView.isUserInteractionEnabled = true
      imgView.contentMode = .scaleAspectFit
      return imgView
  }()

  // 需要交互时采用imageview添加图片
  func showImage(_ image: UIImage) {
      self.contentView.addSubview(imgView)
      imgView.image = image
  }

  /**
   *  如果只是一张图片做背景,直接设置layer
   *  @parm: image:UIImage        目标图片
   *  @parm: resizeStyle:String    图片缩放方式
   */
  func filterWithImage(_ image:UIImage, resizeStyle:String) {
      self.contentView.layer.contents = image.cgImage
      self.contentView.layer.masksToBounds = true
      self.contentView.layer.contentsGravity = resizeStyle
  }
}
2,九宫格页面我直接使用 singleView项目默认的ViewController了:
import UIKit

class ViewController: UIViewController,UICollectionViewDelegate,UICollectionViewDelegateFlowLayout,UICollectionViewDataSource {

var collectionView: UICollectionView?
fileprivate lazy var modalDelegate: ModalAnimationDelegate = ModalAnimationDelegate()
    // 这里默认30个图片了
    lazy var imgArray: Array<UIImage> = {
        var arr:Array<UIImage> = Array()
        for index in 0...30 {
            let imageStr = "images/10_" + String(abs(index - 17)) + ".jpg"
            let image = UIImage(named: imageStr)
            arr.append(image!)
        }
        return arr
    }()


    override func viewDidLoad() {
        super.viewDidLoad()
    
        initCollectionView()
    }

    func  initCollectionView() {
        let flowLayout = UICollectionViewFlowLayout()
        flowLayout.scrollDirection = .vertical
        flowLayout.itemSize = CGSize(width: (screenWidth - 25) / 4, height: (screenWidth - 25) / 4)
        flowLayout.minimumInteritemSpacing = 5
        flowLayout.minimumLineSpacing = 5
        flowLayout.sectionInset = UIEdgeInsetsMake(20, 5, 0, 5)
        collectionView = UICollectionView(frame: UIScreen.main.bounds, collectionViewLayout: flowLayout)
        collectionView?.delegate = self
        collectionView?.dataSource = self
        view.addSubview(collectionView!)
    
        collectionView?.register(ImageCollectionViewCell.self, forCellWithReuseIdentifier: "ImageCollectionViewCell")
   }
    //MARK: collectionview DataSource
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return imgArray.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell:ImageCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCollectionViewCell", for: indexPath) as! ImageCollectionViewCell
        let image: UIImage = imgArray[indexPath.item]
        cell.filterWithImage(image, resizeStyle: kCAGravityResizeAspectFill)
    
        return cell
        }
}
3,浏览大图的页面是 UICollectionViewController的子类,因为从九宫格页面跳转时需要记录点击的cell,所以设置一个currentIndexPath属性:
import UIKit

private let reuseIdentifier = "ImageCollectionViewCell"
private let cellMargin: CGFloat = 15.0
class BlowseBigImageViewController: UICollectionViewController {
    var currentIndexPath: IndexPath?
    var imageArray:Array<UIImage>?

    override init(collectionViewLayout layout: UICollectionViewLayout) {
        super.init(collectionViewLayout: layout)
    }

    convenience init() {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: UIScreen.main.bounds.width , height: UIScreen.main.bounds.height)
        layout.minimumLineSpacing = 15.0
        layout.minimumInteritemSpacing = 0
        layout.scrollDirection = .horizontal
        self.init(collectionViewLayout: layout)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
   }

    override func viewDidLoad() {
        super.viewDidLoad()
        self.collectionView!.register(ImageCollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
        collectionView?.frame.size.width = UIScreen.main.bounds.size.width + cellMargin  //重设宽度,翻页令图片处在正中
    self.collectionView?.isPagingEnabled = true
        guard currentIndexPath != nil else {
            return
        }
        // 设置显示时的位置(默认是从[0,0]开始的)
        self.collectionView?.scrollToItem(at: currentIndexPath! , at: .left, animated: false)
    }


    // MARK: UICollectionViewDataSource
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
   
        return imageArray?.count ?? 0
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ImageCollectionViewCell
    
        guard  let image:UIImage = imageArray?[indexPath.item] else {
            cell.filterWithImage(#imageLiteral(resourceName: "empty_picture"),resizeStyle: kCAGravityResizeAspect)
            return cell
        }
        cell.filterWithImage(image, resizeStyle: kCAGravityResizeAspect)
   
        return cell
    }

  // 点击cell 返回
    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        dismiss(animated: true, completion: nil)
    }
}

上面看到如下代码:
fileprivate lazy var modalDelegate: ModalAnimationDelegate = ModalAnimationDelegate()
实例化了一个ModalAnimationDelegate类,这个类就是自定义模态视图跳转动画的类。

4,自定义模态视图跳转动画

无论是present/dismiss,还是push/pop,都是遵循了一个跳转协议,present/dismiss遵循的是UIViewControllerTransitioningDelegate的协议,具体的使用我在代码上进行了注释。

import UIKit
// 创建代理类,继承`NSObject`,遵守`UIViewControllerTransitioningDelegate `协议
class ModalAnimationDelegate: NSObject, UIViewControllerTransitioningDelegate {
    // 用来区分present 和 dismiss
    fileprivate var isPresent: Bool = true
    fileprivate var duration: TimeInterval = 1
}
  //遵守  `UIViewControllerAnimatedTransitioning `协议,实现present和dismiss的两个代理方法
extension ModalAnimationDelegate: UIViewControllerAnimatedTransitioning {
    // present方法
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        isPresent = true
        return self
    }

    // dismiss方法
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        isPresent = false
        return self
    }
}

  //实现动画时间和动画效果的两个代理方法
extension ModalAnimationDelegate {
    // MARK: UIViewControllerAnimatedTransitioning 必须实现的的两个代理方法
    // 动画时间
    public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return duration
    }
    // 动画效果
    public func animateTransition(using transitionContext: UIViewControllerContextTransitioning){
        isPresent ? customPresentAnimation(transitionContext) : customDismissAnimation(transitionContext)
    }
    
    // 下面就是自定义的动画,
    // 自定义present动画
     private func customPresentAnimation(_ transitionContext: UIViewControllerContextTransitioning){
    
        // 获取转场上下文中的 目标view 和 容器view,并将目标view添加到容器view上
        let destinationView = transitionContext.view(forKey: UITransitionContextViewKey.to)
        let containerView = transitionContext.containerView
        guard let _ = destinationView else {
            return
        }
        containerView.addSubview(destinationView!)
    
        // 目标控制器
        let destinationVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) as? BlowseBigImageViewController
        // 获取点击的indexPath
        let indexPath = destinationVC?.currentIndexPath
        // 当前控制器
        let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) as? ViewController
        let collectionView = fromVC?.collectionView
        // 获取选择的cell
        let selectedCell = collectionView?.cellForItem(at: indexPath!) as! ImageCollectionViewCell
    
        // 获取cell filterWithImage的图片
        let image = UIImage(cgImage: selectedCell.contentView.layer.contents as! CGImage)
//             //获取cell showImage的图片
//        let image = selectedCell.imgView.image
        // 创建一个imageview用来做动画
        let animateView = UIImageView(image: image)
        animateView.contentMode = .scaleAspectFill
        animateView.clipsToBounds = true
    
        // 获取cell的坐标转换
        let originFrame = collectionView?.convert(selectedCell.frame, to: UIApplication.shared.keyWindow)
        animateView.frame = originFrame!
        containerView.addSubview(animateView)
        let endFrame = showImageToFullScreen(image) //本张图片全屏时的frame
        destinationView?.alpha = 0
    
        // 改变frame和透明度
        UIView.animate(withDuration: duration, animations: {
            animateView.frame = endFrame
    }) { (finieshed) in
            transitionContext.completeTransition(true)
            UIView.animate(withDuration: 0.3, animations: {
                destinationView?.alpha = 1
            }, completion: { (_) in
                animateView.removeFromSuperview()
            })
        }
    }

    // 自定义dismiss动画
    private func customDismissAnimation(_ transitionContext: UIViewControllerContextTransitioning) {
        let fromView = transitionContext.view(forKey: .from)
        let contentView = transitionContext.containerView
        let fromVC = transitionContext.viewController(forKey: .from) as! BlowseBigImageViewController
        // 获取浏览大图控制器中的collectionView
        let collectionView = fromVC.collectionView
        // 获取当前显示的cell
        let dismissCell = collectionView?.visibleCells.first as! ImageCollectionViewCell
        let image = UIImage(cgImage: dismissCell.contentView.layer.contents as! CGImage)
        // 创建一个imageview用来做动画
        let animateView = UIImageView(image: image)
        animateView.contentMode = .scaleAspectFill
        animateView.clipsToBounds = true
        // 获取到当前cell在window的frame,即动画view的初始frame
        animateView.frame = (collectionView?.convert(dismissCell.frame, to: UIApplication.shared.keyWindow))!
        contentView.addSubview(animateView)
    
        // 根据dismissCell的indexPath找到九宫格中对应的cell
        let indexPath = collectionView?.indexPath(for: dismissCell)
        let originView = (transitionContext.viewController(forKey: .to) as! ViewController).collectionView
        var originCell = originView?.cellForItem(at: indexPath!)
        // 当originCell取不到时(证明这个cell不在可视范围内),将cell移动到视野中间
        if originCell == nil {
            originView?.scrollToItem(at: indexPath!, at: .centeredVertically, animated: false)
            originView?.layoutIfNeeded()
        }
        originCell = originView!.cellForItem(at: indexPath!)
        let originFrame = originView?.convert(originCell!.frame, to: UIApplication.shared.keyWindow)
    
        // 开始动画
        UIView.animate(withDuration: duration, animations: {
            animateView.frame = originFrame!
            fromView?.alpha = 0
        }, completion: { (_) in
            animateView.removeFromSuperview()
            transitionContext.completeTransition(true)
        })
    }
}

最后附上demo地址:

https://github.com/skykywind/BlowseImageDemo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,019评论 4 62
  • Swift版本点击这里欢迎加入QQ群交流: 594119878最新更新日期:18-09-17 About A cu...
    ylgwhyh阅读 25,280评论 7 249
  • 11月7 明天迎来了小学生第一次考试,好激动好紧张,希望你能认真快速答题门门全部完美完成,宝贝加油
    张越的阅读 129评论 0 0
  • 一个女人最大的幸福,就是在历经各种苦难,最终可以得到一份完美细腻的依赖,包容你所有的困苦,恐惧,难过和无知。 一个...
    小月的故事阅读 275评论 2 1
  • 团队分工 舵手(林洋平):明确目标的优先级,对文档内容的安排和制定,同时验收报告。 桨手(苟燕、张静、谢秋霞):分...
    bc83268d9989阅读 452评论 4 2