最近经常看见从九宫格图片到大图浏览时,有个过渡动画,即从九宫格图片的位置放大到整个屏幕,,分析一下,其实主要技术点就是自定义过渡动画,并且在controller切换时获取点击图片位置。
先看下demo效果:
效果分析
无论是九宫格页面还是大图浏览页面,都是多张图片,并且有点击效果,所以用UICollectionView
来做最简单直接。无论点击九宫格图片还是大图图片,都需要记录图片当前位置,并且对应到另一页面的item,这样才能找到图片的始末frame
,来进行动画。
实现页面布局
1, UICollectionViewCell,因为demo
中cell
都是只展示一张图片,所以我只自能定义了ImageCollectionViewCell
共用,并且设置图片为layer
的contents
,(需要交互、图片不沾满整个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地址: