swift:自定义UICollectionViewFlowLayout
写作目的
UICollectionView是ios中一个十分强大的控件,利用它能够十分简单的实现一些很好看的效果。UICollectionView的效果又依赖于UICollectionViewLayout或者它的子类UICollectionViewFlowLayout。而关于自定义UICollectionViewFlowLayout网上介绍的比较少。出于这一目的,写下这边文章,希望能够帮助初学者(我也是)实现一些简单的流水布局效果。下面的演示就是本篇文章的目标。最终版代码和所有图片素材(图片名和项目中有点不一样)已经上传至Github,大家可以下载学习。
几个简单的概念
- UICollectionViewLayout与UICollectionViewFlowLayout
UICollectionView的显示效果几乎全部由UICollectionViewLayout负责(甚至是cell的大小)。所以,一般开发中所说的自定义UICollectionView也就是自定义UICollectionViewLayout。而UICollectionViewFlowLayout是继承自UICollectionViewLayout的,由苹果官方实现的流水布局效果。如果想自己实现一些流水布局效果可以继承自最原始UICollectionViewLayout从头写,也可以继承自UICollectionViewFlowLayout进行修改。文本是继承自UICollectionViewFlowLayt*
- UICollectionViewLayoutAttributes
第二点就说了UICollectionView的显示效果几乎全部由UICollectionViewLayout负责,而真正存储着每一个cell的位置、大小等属性的是UICollectionViewLayoutAttributes。每一个cell对应着一个属于自己的UICollectionViewLayoutAttributes,而UICollectionViewLayout正是利用UICollectionViewLayoutAttributes里存在的信息对每一个cel进行布局。
- 流水布局
所谓流水布局就是:就是cell以一定的规律进行如同流水一般的有规律的一个接着一个的排列。最经典的流水布局便是九宫格布局,绝大部分的图片选择器也是流水布局。
准备工作
- xcode7.0
- swift2.0
- 自己我提供的素材并在控制器中添加如下代码
class ViewController: UIViewController,UICollectionViewDelegate, UICollectionViewDataSource {
lazy var imageArray: [String] = {
var array: [String] = []
for i in 1...20 {
array.append("\(i)-1")
}
return array
}()
override func viewDidLoad() {
super.viewDidLoad()
let collectionView = UICollectionView(frame: CGRectMake(0, 100, self.view.bounds.width, 200), collectionViewLayout: UICollectionViewFlowLayout())
collectionView.backgroundColor = UIColor.blackColor()
collectionView.dataSource = self
collectionView.delegate = self
collectionView.registerClass(ImageTextCell.self, forCellWithReuseIdentifier: "ImageTextCell")
self.view.addSubview(collectionView)
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.imageArray.count;
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("ImageTextCell", forIndexPath: indexPath) as! ImageTextCell
cell.imageStr = self.imageArray[indexPath.item]
return cell
}
}
//这里是自定义cell的代码
class ImageTextCell: UICollectionViewCell {
var imageView: UIImageView?
var imageStr: NSString? {
didSet {
self.imageView!.image = UIImage(named: self.imageStr as! String)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.imageView = UIImageView()
self.addSubview(self.imageView!)
}
override func layoutSubviews() {
super.layoutSubviews()
self.imageView?.frame = self.bounds
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
效果应该是这样的
编码
水平排列
创建一个名为LineLayout.swift的文件(继承自UICollectionViewFlowLayout)。添加如下几行代码
var itemW: CGFloat = 100
var itemH: CGFloat = 100
override init() {
super.init()
//设置每一个元素的大小
self.itemSize = CGSizeMake(itemW, itemH)
//设置滚动方向
self.scrollDirection = .Horizontal
//设置间距
self.minimumLineSpacing = 0.7 * itemW
}
//苹果推荐,对一些布局的准备操作放在这里
override func prepareLayout() {
//设置边距(让第一张图片与最后一张图片出现在最中央)ps:这里可以进行优化
let inset = (self.collectionView?.bounds.width ?? 0) * 0.5 - self.itemSize.width * 0.5
self.sectionInset = UIEdgeInsetsMake(0, inset, 0, inset)
}
效果就成了这样
shouldInvalidateLayoutForBoundsChange方法与layoutAttributesForElementsInRect方法关系
标题所写出的是十分重要的两方法,先看我添加的如下测试代码
/**
返回true只要显示的边界发生改变就重新布局:(默认是false)
内部会重新调用prepareLayout和调用
layoutAttributesForElementsInRect方法获得部分cell的布局属性
*/
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
print(newBounds)
return true
}
/**
用来计算出rect这个范围内所有cell的UICollectionViewLayoutAttributes,
并返回。
*/
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
print("layoutAttributesForElementsInRect==\(rect)")
let ret = super.layoutAttributesForElementsInRect(rect)
// print(ret?.count)
return ret
}
为了解释,我添加了几个打印语句,在shouldInvalidateLayoutForBoundsChange返回值设置为true后,会发现layoutAttributesForElementsInRect方法调用十分频繁,几乎是每滑动一点就会调用一次。观察打印信息可以发现很多秘密。
- 启动程序有如下打印
layoutAttributesForElementsInRect==(0.0, 0.0, 568.0, 568.0)
好像看不太懂,没事,尝试滑动。
- 滑动
(0.5, 0.0, 320.0, 200.0) //这个是shouldInvalidateLayoutForBoundsChange方法的打印的newBounds
layoutAttributesForElementsInRect==(0.0, 0.0, 568.0, 568.0)//这个是layoutAttributesForElementsInRect打印的rect
(1.5, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(0.0, 0.0, 568.0, 568.0)
(3.5, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(0.0, 0.0, 568.0, 568.0)
...
不难发现,shouldInvalidateLayoutForBoundsChange的参数newBounds的意思是UICollectionView的可见矩形。什么叫可见矩阵?,因为UICollectionView也是UIScrollView的子类,所以它真正的“内容”远远不止我们屏幕上看到的那么多(这里不再话时间继续解释可见矩阵)。那好像layoutAttributesForElementsInRect打印出来的东西没有啥变化是怎么回事?不急继续滑动。
- 解密
继续滑动后有这些信息,经过删除一些无用信息,显示如下。(注意看有注释的行)
...
(248.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(0.0, 0.0, 568.0, 568.0)
(249.0, 0.0, 320.0, 200.0) //这里是可见矩阵
layoutAttributesForElementsInRect==(0.0, 0.0, 1136.0, 568.0) //这里变化了1136.0是568.0的2倍(1136代表的是宽度的意思应该知道不需要解释吧)
(250.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(0.0, 0.0, 1136.0, 568.0)
...
(567.5, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(0.0, 0.0, 1136.0, 568.0)
(568.5, 0.0, 320.0, 200.0)//这里是可见矩阵
layoutAttributesForElementsInRect==(568.0, 0.0, 568.0, 568.0) // 这里又变化了,x变成了568,宽度变成了568
(571.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(568.0, 0.0, 568.0, 568.0)
...
(815.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(568.0, 0.0, 568.0, 568.0)
(817.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(568.0, 0.0, 1136.0, 568.0) //还有这里
...
(1135.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(568.0, 0.0, 1136.0, 568.0)
(1136.0, 0.0, 320.0, 200.0)
layoutAttributesForElementsInRect==(1136.0, 0.0, 568.0, 568.0) //还有这里
上面的的数据展示其实已经足够解释一切了。读到这里,推荐你自己去找找规律,通过自己发现的奥秘绝对比直接看我写出答案有意义的多!下面这张图例已经说明了一切
至于为什么会是568的倍数。。因为我是用的5s模拟器。你换成4s就变成480了。至于这样设计的理由,我猜测是为了方便进行范围的确定。
缩放效果
了解了上面shouldInvalidateLayoutForBoundsChange方法与layoutAttributesForElementsInRect方法关系后,可以继续进行编码了。因为主要的内容已经讲解完毕,剩下的就只是一些动画的计算,所以不再继续讲解,直接贴出代码。
class LineLayout: UICollectionViewFlowLayout {
var itemW: CGFloat = 100
var itemH: CGFloat = 100
lazy var inset: CGFloat = {
//这样设置,inset就只会被计算一次,减少了prepareLayout的计算步骤
return (self.collectionView?.bounds.width ?? 0) * 0.5 - self.itemSize.width * 0.5
}()
override init() {
super.init()
//设置每一个元素的大小
self.itemSize = CGSizeMake(itemW, itemH)
//设置滚动方向
self.scrollDirection = .Horizontal
//设置间距
self.minimumLineSpacing = 0.7 * itemW
}
//苹果推荐,对一些布局的准备操作放在这里
override func prepareLayout() {
//设置边距(让第一张图片与最后一张图片出现在最中央)
self.sectionInset = UIEdgeInsetsMake(0, inset, 0, inset)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/**
返回true只要显示的边界发生改变就重新布局:(默认是false)
内部会重新调用prepareLayout和调用
layoutAttributesForElementsInRect方法获得部分cell的布局属性
*/
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
/**
用来计算出rect这个范围内所有cell的UICollectionViewLayoutAttributes,
并返回。
*/
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
//取出rect范围内所有的UICollectionViewLayoutAttributes,然而
//我们并不关心这个范围内所有的cell的布局,我们做动画是做给人看的,
//所以我们只需要取出屏幕上可见的那些cell的rect即可
let array = super.layoutAttributesForElementsInRect(rect)
//可见矩阵
let visiableRect = CGRectMake(self.collectionView!.contentOffset.x, self.collectionView!.contentOffset.y, self.collectionView!.frame.width, self.collectionView!.frame.height)
//接下来的计算是为了动画效果
let maxCenterMargin = self.collectionView!.bounds.width * 0.5 + itemW * 0.5;
//获得collectionVIew中央的X值(即显示在屏幕中央的X)
let centerX = self.collectionView!.contentOffset.x + self.collectionView!.frame.size.width * 0.5;
for attributes in array! {
//如果不在屏幕上,直接跳过
if !CGRectIntersectsRect(visiableRect, attributes.frame) {continue}
let scale = 1 + (0.8 - abs(centerX - attributes.center.x) / maxCenterMargin)
attributes.transform = CGAffineTransformMakeScale(scale, scale)
}
return array
}
/**
用来设置collectionView停止滚动那一刻的位置
- parameter proposedContentOffset: 原本collectionView停止滚动那一刻的位置
- parameter velocity: 滚动速度
- returns: 最终停留的位置
*/
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
//实现这个方法的目的是:当停止滑动,时刻有一张图片是位于屏幕最中央的。
let lastRect = CGRectMake(proposedContentOffset.x, proposedContentOffset.y, self.collectionView!.frame.width, self.collectionView!.frame.height)
//获得collectionVIew中央的X值(即显示在屏幕中央的X)
let centerX = proposedContentOffset.x + self.collectionView!.frame.width * 0.5;
//这个范围内所有的属性
let array = self.layoutAttributesForElementsInRect(lastRect)
//需要移动的距离
var adjustOffsetX = CGFloat(MAXFLOAT);
for attri in array! {
if abs(attri.center.x - centerX) < abs(adjustOffsetX) {
adjustOffsetX = attri.center.x - centerX;
}
}
return CGPointMake(proposedContentOffset.x + adjustOffsetX, proposedContentOffset.y)
}
}
如果在控制器中加入下面两个方法,在你点击控制器,或者点击某个cell会有很炫的动画产生,这都是苹果帮我们做好的。
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
self.imageArray.removeAtIndex(indexPath.item)
collectionView.deleteItemsAtIndexPaths([indexPath])
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
if self.collectionView!.collectionViewLayout.isKindOfClass(LineLayout.self) {
self.collectionView!.setCollectionViewLayout(UICollectionViewFlowLayout(), animated: true)
}else {
self.collectionView!.setCollectionViewLayout(LineLayout(), animated: true)
}
}
总结
本篇文章记录了我在自定义UICollectionViewFlowLayout过程中遇到的一些问题和解决方式(其实有一些坑爹的问题我没有列出,怕误导大家)。上面的全部都是基于UICollectionViewFlowLayout进行的更改。而我在GitHub上面上传的也有一份继承自UICollectionViewLayout的非流水布局。效果如下,因为原理性的东西都差不多,就不再进行分析(代码也有注释)。感兴趣的可以这Github上面下载。如果文章中有什么错误或者更好的方法、建议之类,感谢您的指出。我们共同学习!O(∩_∩)O!
swift:自定义UICollectionViewFlowLayout的更多相关文章
- [IOS]swift自定义uicollectionviewcell
刚刚接触swift以及ios,不是很理解有的逻辑,导致某些问题.这里分享一下swift自定义uicollectionviewcell 首先我的viewcontroller不是直接继承uicollect ...
- Swift 自定义打印方法
Swift 自定义打印方法 代码如下 // MARK:- 自定义打印方法 func MLLog<T>(_ message : T, file : String = #file, funcN ...
- swift 自定义图片轮播视图
Swift封装图片轮播视图: import UIKit class XHAdLoopView: UIView { private var pageControl : UIPageControl? pr ...
- Swift 自定义Subscript
Swift可以方便给自定义类加下标,其中参数和返回值可以在类里定义为任意类型: subscript(parameters) -> ReturnType { get { //return some ...
- swift 自定义TabBarItem
1.效果图 2.NewsViewController.swift // // NewsViewController.swift // NavigationDemo // // Created ...
- Swift - 自定义UIActivity分享
UIActivity可以十分方便地将文字.图片等内容进行分享,比如分享到微信.微博.发送邮件.短信等等.我们不仅可以分享内容出来,也可以在自己的App里添加自己的分享按钮或隐藏已有的分享按钮来实现定制 ...
- Swift - 自定义单元格实现微信聊天界面
1,下面是一个放微信聊天界面的消息展示列表,实现的功能有: (1)消息可以是文本消息也可以是图片消息 (2)消息背景为气泡状图片,同时消息气泡可根据内容自适应大小 (3)每条消息旁边有头像,在左边表示 ...
- swift 自定义弹框
// // ViewController.swift // animationAlert // // Created by su on 15/12/9. // Copyright © 2015 ...
- Swift自定义AlertView
今天项目加新需求,添加积分过期提醒功能: 第一反应就用系统的UIAlertViewController,但是message中积分是需要红色显示. // let str = "尊敬的顾客,您有 ...
随机推荐
- ansible文件模块使用
1. 文件组装模块-assemble assemble主要是将多份配置文件组装为一份配置文件. 参数 必填 默认 选项 说明 Backup 否 No Yes/no 是否创建备份文件,使用时间戳 Del ...
- Ruby相关图书推荐
Ruby基础教程第4版 作 者 [日] 高桥征义,[日] 后藤裕藏 著:何文斯 译:[日] 松本行弘 校 出 版 社 人民邮电出版社 出版时间 2014-09-01 版 次 4 页 ...
- 开扒php内核函数,第三篇 implode
一开始觉得implode挺容易实现,但是写着写着才发现是挺复杂的,不说啦 来看看implode的用法吧 <?php $arr = array('Hello','World!','Beautifu ...
- flappy pig小游戏源码分析(1)——主程序初探
闲逛github发现一个javascript原生实现的小游戏,源码写的很清晰,适合想提高水平的同学观摩学习.读通源码后,我决定写一系列的博客来分析源码,从整体架构到具体实现细节来帮助一些想提高水平的朋 ...
- Windows Azure 配置多个站点的虚拟网络连接
通过上一篇"Windows Azure 虚拟网络配置(Site to Site)" 我们建立了可以进行Site to Site连接的虚拟网络,配置过后有些朋友会有疑问:如果需要连接 ...
- 关于网站编码显示问题 效果是 访问 带有中文注释的sass文件出现编码报错。
首先查看环境变量 export declare -x HOME="/home/piperck" declare -x LANG="en_US.UTF-8" de ...
- 第三百四十四天 how can I 坚持
三言诗,把自己的心情,想要说的话用三句话诗意的表达出来.像: 烦===>好想睡一觉,待日落,盼天明. 愁====>待到花开花落,闲庭信步,蹋碎一世忧愁. 三句话,可以表达的很好. 老是感觉 ...
- delphi 判断调试状态
DebugHook 该变量在调试状态下值为1,运行模式为0,例如,我们可以使用如下的代码来简单控制: if DebugHook=0 then
- 测试peerdroid示例程序步骤
来自JXTA交流群(36855950)...韦发改(992611244) 15:12:25—————————————————————————————————————————————————————— ...
- POJ 1410 Intersection(判断线段交和点在矩形内)
Intersection Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 9996 Accepted: 2632 Desc ...