大家在平常用微信,微博的过程中肯定(对,就是肯定)都有查看过朋友圈和微博所发布的照片,当点击九宫格的某一图片时图片会慢慢的放大并进入全屏,左右滑动查看另一张.轻点图片又会以动画的方式慢慢缩小回到滑动之后对应的图片.说了这么多估计你还是不知道我在讲什么鬼,一张动图胜过千言万语.毕竟语言这东西真不是码农的特长…
上面两张gif点开时的动画不是很明显,你可以在真机上查看更真实效果.接下来我会通过一个Demo来介绍实现这种效果的具体思路,如果你有更好的思路,请求赐教
Demo 预览
在开始之前先看一看最终的效果
这个Demo抓取了美丽说的在线图片,这里对毫不知情的美丽说表示感谢.
在看下面的部分之前假定你已经撑握了Swift,网络请求,会使用UICollectionView等基础组件的技能.如若不能撑握建议先了解相关知识
DemoGitHub地址
Demo 结构分析
在Demo中主要包括两个主要的视图结构:一 缩略图(主视图)的浏览 二 大图的浏览. 这两个视图中所要展示的内容都是有规律的矩形所以都可以用UICollectionView来实现.
两者的区别在于缩略图是垂直方向的布局而大图是水平方向上的布局方式.两个UICollectionView的cell的内容只包含一个UIImageView.在大图浏览视图中有一个
需要注意的细节:为了图片浏览的效果每张图片之间是有一定间隔的,如果让每个cell都填充整个屏幕,图片的宽度等于cell的宽度再去设置cell的间隔来达到间隔的效果会在停止滑动图片时黑色的间隔会显现在屏幕中(如下图),这并不是我们想看到的结果.
出现这个问题的原因是UICollectionView的分页(pagingEnabled)效果是以UICollectionView的宽来滚动的,也就是说不管你的cell有多大每次滚动总是一个UICollectionView自身的宽.要实现这个效果有个小技巧,相关内容会在大图浏览的实现一节中介绍.
主视图图片浏览的实现
根据上一节得出的结论,主视图采用colletionview,这部分实现没什么特别的技巧,但在添加collectionview之前需要添加几个基础组件.
因为我们所需的图片是抓取美丽说的网络图片,所以我们需要一个网络请求组件,另外为展示图片还需要添加对应的数据模型.但这两个组件的内容不是本篇博文主要讨论的问题
另外这两个组件相对较基础,就不废太多口水.具体实现可以参看GitHub源码,每次网络请求这里设置为30条数据,这里提到也是为了让你在下面的章节看到相关部分不至于感到疑惑,
添加完这两个基础组件之后,就可以实现缩略图的浏览部分了.为方便起见缩略图view的控制器采用UICollectionViewController,在viewDidLoad函数中设置流水布局样式,实现collectionview的datasource,delegate.这部分都是一些常规的写法,这里要关注的是datasource和delegate的下两个函数.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { // 从缓存池中取出重用cell let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as? CollectionViewCell // 从模形数组中取出相应的模形 let item = shopitems[indexPath.item]; // 设置模形数据为显示缩略图模式 item.showBigImage = false // 把模形数据赋值给cell,由cell去决定怎样显示,显示什么内容 cell?.item = item // 当滑动到到最后一个cell时请求加载30个数据 if indexPath.item == shopitems.count - 1 { loadMoreHomePageData(shopitems.count) } return cell! } |
这里为使Demo不过于复杂,没有用什么”上拉加载更多”控件,每次滑动到到最后一个cell时请求加载30个数据方式同样能获得良好的滑动体验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { // 当点击某个cell时, 创建大图浏览控制器 let photoVC = PhotoBrowseCollectionVC() // 当前点击cell的indexPathw传给控制器,以使大图浏览器直接显示对应图片 photoVC.indexPath = indexPath // 当前模型数组的内容传给控制器,以使大图浏览能左右滑动 photoVC.items = shopitems // 先以正常形式modal出大图浏览 presentViewController(photoVC, animated: true, completion: nil) } |
这里先以正常的样式(从底部弹出)modal出大图浏览视图,当缩略图和大图的逻辑跳转逻辑完成后再来完善画动逻辑
大图浏览的实现
与缩略图一样,大图浏览也是一个collectionView.这里为大图浏览控制器添加了一个便利构造器,以便在点击缩略图时快速创建固定流水布局的collectionView.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
convenience init() { let layout = UICollectionViewFlowLayout() layout.itemSize = CGSize(width: UIScreen.mainScreen().bounds.width + cellMargin, height: UIScreen.mainScreen().bounds.height) layout.minimumLineSpacing = 0 layout.minimumInteritemSpacing = 0 layout.scrollDirection = .Horizontal self.init(collectionViewLayout: layout) } |
在Demo 结构分析一节中遗留了一个问题,其实要实现全屏图像间隔效果非常简单,只要把collectionView和cell的宽设置为屏宽加固定的间距并且cell之间间距为0
而图片只显示在屏幕正中间(图片与屏等宽),这样在开启pagingEnabled的情况下每次滑动都是滑动一个(图片宽度+间距),相当于在cell中留了一个边距来作间隔而不是在cell
外做间隔,可以参看下图
上图中有两个cell,cell的间距是零.开启pagingEnabled时,每次移动都是一个cell的宽,这样停止滑动时间隔就不会出现在屏幕中了.
大图浏览的collectionView的实现代码几乎与缩略图一样,需要注意的是当modal出大图的时候collectionView是要直接显示对应大图的,这也是为什么在缩略视图控制器的didSelectItemAtIndexPath函数中要传递indexPath的原因.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
override func viewDidLoad() { super.viewDidLoad() // 大图colletionview的frame collectionView?.frame = UIScreen.mainScreen().bounds collectionView?.frame.size.width = UIScreen.mainScreen().bounds.size.width + cellMargin // 开启分页 collectionView?.pagingEnabled = true // 注册重用cell collectionView?.registerClass(CollectionViewCell.self, forCellWithReuseIdentifier: cellID) // collectionView显示时跳转到应的图片 collectionView?.scrollToItemAtIndexPath(indexPath!, atScrollPosition: .Left, animated: false) } |
上面代码中scrollToItemAtIndexPath函数的atScrollPosition参数的意思是停止滚动时对应的cell与collectionView的位置关系,Left是cell的左边与colletionview的
左边对齐.其它的对应关系可依此类推就不废话了. collectionView的比较重要代理函数的实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellID, forIndexPath: indexPath) as! CollectionViewCell let item = items![indexPath.item] item.showBigImage = true cell.item = item return cell } override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { dismissViewControllerAnimated(true, completion: nil) } |
说重要是因为要与缩略图控制器的代理函数对比看,cellForItemAtIndexPath只是常规的设置数据,选中cell直接dismiss当前控制器.
至此缩略图和大图的跳转逻辑你已经清楚了,下面的部分才本博文要讲的真正内容.其实上面分析那么多废话也是因为present和dismiss的动画与跳转前后两个控制器有密切关系
modal出一个View的原理
默认从底部弹出view的modal方式是将要显式的view添加到一个容器view中,然后对容器view添加动画效,动画结束后把跳转之前控制器的view从window中移除.在window中之前
的view完全被弹出的view替代最终看到如下图的视图结构
如你在上图中看到的,黑色的是window,蓝色的为弹出的View,而中间的就是容器View.容器view的类型是UITransitionView
dismiss的过程是present的逆过程,除了从底部弹出的动画UIKit还提供了多种动画效果可以通过设置弹出控制器modalTransitionStyle属性.
这里有个需要注意点,当设置modalPresentationStyle为Custom时原控制器的view并不会从window中移除.同时如果设置了transitioningDelegate
那么modalTransitionStyle设置的动画效果将全部失效,此时动画全权交给代理来完成. UIViewControllerTransitioningDelegate协议包含五个函数
这里只需要关注Getting the Transition Animator Objects的两个函数,这两个函数都需要返回一个实现UIViewControllerAnimatedTransitioning协议的实例对象,
具体的动画逻辑将在这个实例对象的方法中完成.
添加点击跳转到大图浏览动画
按上一节的分析需要在点击缩略图时把大图控制器的modalPresentationStyle设为.Custom,并且过渡动画(transitioningDelegate)设置代理对象,具体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { let photoVC = PhotoBrowseCollectionVC() photoVC.indexPath = indexPath photoVC.items = shopitems photoVC.transitioningDelegate = modalDelegate photoVC.modalPresentationStyle = .Custom presentViewController(photoVC, animated: true, completion: nil) } |
modalDelegate是ModalAnimationDelegate的实例对象,其实现了UIViewControllerTransitioningDelegate协议方法,animationControllerForPresentedController
返回本身的实例对象,所以ModalAnimationDelegate也要实现UIViewControllerAnimatedTransitioning协议方法.
1 2 3 4 5 |
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? { return self } |
现在具体的动画逻辑就转到了UIViewControllerAnimatedTransitioning协议的animateTransition方法中.要实现从选中的图片慢慢放大的效果分成如下几步
- 取出容器view,也就是上一节提到的UITransitionView实例对象
- 取出要弹出的目标view,在这里就是展示大图的colletionview,并添加到容器view
- 新建UIImageView对象,得到选中的UIImage对像,及其在window上的frame
- 把新建的UIImageView对象添加到容器view
- 设置新建UIImageView的放大动画,动画结果束后从容器view中移除
- 通知系统动画完成(主动调用completeTransition)
把动画的实现分解开来是不是清晰很多了,具体实现还是得参看代码