UICollectionView基础用法和简单自定义
注:本文通过几个实例来讲讲UICollectionView基本用法
本次要实现的两个效果。感谢猫神提供的教程 OneV’s Den
第一个界面是一个普通的流布局 UICollectionViewFlowLayout
, 第二个界面是自定义的一个圆形布局。加了点手势操作和动画。老规矩。后面会附上源码
首先来看下基本用法 。
1、UICollectionView基础用法
简单的UICollectionView
相当于GridView
,一个多列的UItableView
,然而UICollectionView
跟UItableView
的操作也非常相似 。都是会设置有一个DataSource
和一个delegate
标准的UICollectionView包含三个部分,它们都是UIView的子类:
Supplementary Views 追加视图 如果你对UITableView比较熟悉的话,可以理解为每个Section的Header或者Footer,用来标记每个section的view
Decoration Views 装饰视图 这是每个section的背景
UICollectionView和UITableView最大的不同就是UICollectionViewLayout,UICollectionViewLayout可以说是UICollectionView的大脑和中枢,它负责了将各个cell、Supplementary View和Decoration Views进行组织,为它们设定各自的属性。包括位置、尺寸、层级、形状等等 。。
Layout决定了UICollectionView是如何显示在界面上的。在展示之前,一般需要生成合适的UICollectionViewLayout子类对象,并将其赋予CollectionView的collectionViewLayout属性
下面我们实现一个最简单的Demo
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = UICollectionViewScrollDirection.Vertical //滚动方向
layout.itemSize = CGSizeMake(60,75) //设置所有cell的size 太重要了 找了半天。(自学就是辛苦呀!!)
layout.minimumLineSpacing = 10.0 //上下间隔
layout.minimumInteritemSpacing = 5.0 //左右间隔
layout.headerReferenceSize = CGSizeMake(20,20)
layout.footerReferenceSize = CGSizeMake(20,20)
这里创建了基本的流布局 设置了一些基本属性。
然后其他的设置和UITableView差不多
let collect:UICollectionView = UICollectionView(frame: self.view.frame,collectionViewLayout:layout)
collect.backgroundColor = UIColor.whiteColor()
collect.delegate = self
collect.dataSource = self
self.view.addSubview(collect)
因为初始的背景色是黑色的,这里指定了背景色
然后实现下面三个基本的方法,就能正常跑了 。最要是cell的显示方法
//设置分区个数
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
//设置每个分区元素个数
func collectionView(collectionView: UICollectionView,numberOfItemsInSection section: Int) -> Int {
return 10
}
//设置元素内容
func collectionView(collectionView: UICollectionView,cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
//这里创建cell
return cell
}
为了得到高效的View,对于cell的重用是必须的,避免了不断生成和销毁对象的操作,在UICollectionView中使用以下方法进行注册:
registerClass:forCellWithReuseIdentifier:
registerClass:forSupplementaryViewOfKind:withReuseIdentifier:
registerNib:forCellWithReuseIdentifier:
registerNib:forSupplementaryViewOfKind:withReuseIdentifier:
先注册 ,使用一个Identifier
,加入重用队列。要是在重用队列里没有可用的cell的话,runtime将自动帮我们生成并初始化一个可用的cell。
我们这里是自己用xib 画了个cell
一个很简单的cell ,把它的class
设置成我们自定义的MyCellContent
,MyCellContent
继承自UICollectionViewCell
,把这两个拖成它的成员属性
import UIKit
class MyCellContent: UICollectionViewCell {
@IBOutlet var contentImage: UIImageView!
@IBOutlet var contentLabel: UILabel!
}
然后,在我们的视图控制器中的viewDidLoad
进行注册
let nib = UINib(nibName: "MyCollectionCell",bundle: NSBundle.mainBundle())
collect.registerNib(nib,forCellWithReuseIdentifier: "DesignViewCell")
然后在cellForItemAtIndexPath
里面就能这样取了
let identify:String = "DesignViewCell"
let cell =collectionView.dequeueReusableCellWithReuseIdentifier(identify,forIndexPath: indexPath) as! MyCellContent
我们事先创建了个结构体,用来存放cell的img和name
struct CellContent{
var img:String
var name:String
}
然后在控制器中声明了一个var dic = Array<CellContent>()
在viewDidLoad
中初始化。
for i in 1...9{
dic.append(CellContent(img: "f"+String(i),name: "歪脖子"+String(i)))
}
我图片存放的名字就是f1-----f9
func collectionView(collectionView: UICollectionView,numberOfItemsInSection section: Int) -> Int {
return self.dic.count
}
这里返回元素个数就可以这么写了
//设置元素内容
func collectionView(collectionView: UICollectionView,cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let identify:String = "DesignViewCell"
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(identify,forIndexPath: indexPath) as! MyCellContent
cell.contentView.backgroundColor = UIColor.grayColor()
cell.contentView.alpha = 0.2
let img = UIImage(named: (self.dic[indexPath.row] as CellContent).img)
cell.contentImage.image = img
cell.contentLabel.text = (self.dic[indexPath.row] as CellContent).name
return cell
}
这个就可以很简单的设置了 。现在运行,第一个页面的效果就有了
但让还有向UITableView中样很多的方法去设置别的,比如单个cell的大小
func collectionView(collectionView: UICollectionView!,layout collectionViewLayout: UICollectionViewLayout!,sizeForItemAtIndexPath indexPath: NSIndexPath!) -> CGSize {
return CGSizeMake(150,150)
}
点击cell
//点击元素
func collectionView(collectionView: UICollectionView,didSelectItemAtIndexPath indexPath: NSIndexPath){
print("点击了第\(indexPath.section) 分区,第\(indexPath.row) 个元素")
}
还有很多,自己慢慢玩吧 。下面看看自定义的
2、自定义UICollectionViewLayout
UICollectionViewLayoutAttributes
是一个非常重要的类,先来看看property列表:
@property (nonatomic) CGRect frame
@property (nonatomic) CGPoint center
@property (nonatomic) CGSize size
@property (nonatomic) CATransform3D transform3D
@property (nonatomic) CGFloat alpha
@property (nonatomic) NSInteger zIndex
@property (nonatomic,getter=isHidden) BOOL hidden
可以看到,UICollectionViewLayoutAttributes
的实例中包含了诸如边框,中心点,大小,形状,透明度,层次关系和是否隐藏等信息。和DataSource
的行为十分类似,当UICollectionView
在获取布局时将针对每一个indexPath
的部件(包括cell
,追加视图和装饰视图),向其上的UICollectionViewLayout
实例询问该部件的布局信息,这个布局信息,就以UICollectionViewLayoutAttributes
的实例的方式给出。
UICollectionViewLayout
的功能为向UICollectionView
提供布局信息,不仅包括cell的布局信息,也包括追加视图和装饰视图的布局信息。实现一个自定义layout
的常规做法是继承UICollectionViewLayout
类,然后重载下列方法:
-(CGSize)collectionViewContentSize //返回collectionView的内容的尺寸
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
//返回rect中的所有的元素的布局属性
-(UICollectionViewLayoutAttributes )layoutAttributesForItemAtIndexPath:(NSIndexPath )indexPath
//返回对应于indexPath的位置的cell的布局属性
-(UICollectionViewLayoutAttributes )layoutAttributesForSupplementaryViewOfKind:(NSString )kind atIndexPath:(NSIndexPath *)indexPath
//返回对应于indexPath的位置的追加视图的布局属性,如果没有追加视图可不重载
-(UICollectionViewLayoutAttributes * )layoutAttributesForDecorationViewOfKind:(NSString)decorationViewKind atIndexPath:(NSIndexPath )indexPath
//返回对应于indexPath的位置的装饰视图的布局属性,如果没有装饰视图可不重载
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
//当边界发生改变时,是否应该刷新布局。如果YES则在边界变化(一般是scroll到其他地方)时,将重新计算需要的布局信息。
在初始化一个UICollectionViewLayout
实例后,会有一系列准备方法被自动调用,以保证layout
实例的正确。
首先,-(void)prepareLayout将被调用,默认下该方法什么没做,但是在自己的子类实现中,一般在该方法中设定一些必要的layout的结构和初始需要的参数等。
之后,-(CGSize) collectionViewContentSize将被调用,以确定collection应该占据的尺寸。注意这里的尺寸不是指可视部分的尺寸,而应该是所有内容所占的尺寸。collectionView的本质是一个scrollView,因此需要这个尺寸来配置滚动行为。
接下来-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect被调用,这个没什么值得多说的。初始的layout的外观将由该方法返回的UICollectionViewLayoutAttributes来决定。
另外,在需要更新
layout
时,需要给当前layout
发送 -invalidateLayout
,该消息会立即返回,并且预约在下一个loop
的时候刷新当前layout
,这一点和UIView
的setNeedsLayout
方法十分类似。在-invalidateLayout
后的下一个collectionView
的刷新loop
中,又会从prepareLayout
开始,依次再调用-collectionViewContentSize
和-layoutAttributesForElementsInRect
来生成更新后的布局。
以上都是猫神的巨作,他写的很好直接拿来用了
下面看下demo
首先创建一个类继承自UICollectionViewLayou
然后声明一些基本的属性
private var _cellCount:Int?
private var _collectSize:CGSize?
private var _center:CGPoint?
private var _radius:CGFloat?
按照上面的步骤
//一般在该方法中设定一些必要的layout的结构和初始需要的参数等
override func prepareLayout() {
super.prepareLayout()
_collectSize = self.collectionView?.frame.size
_cellCount = self.collectionView?.numberOfItemsInSection(0)
_center = CGPointMake(_collectSize!.width / 2.0,_collectSize!.height / 2.0);
_radius = min(_collectSize!.width,_collectSize!.height)/2.5
}
这个方法初始化了一些基本信息
//内容区域的总大小 (不是可见区域)
override func collectionViewContentSize() -> CGSize {
return _collectSize! //这里不用可见区域吧
}
可见区域
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributesArray = [UICollectionViewLayoutAttributes]()
if let count = self._cellCount {
for i in 0 ..< count{
//这里利用了-layoutAttributesForItemAtIndexPath:来获取attributes
let indexPath = NSIndexPath(forItem: i,inSection: 0)
let attributes = self.layoutAttributesForItemAtIndexPath(indexPath)
attributesArray.append(attributes!)
}
}
return attributesArray
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
let attrs = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
attrs.size = CGSizeMake(ITEM_SIZE,ITEM_SIZE)
let x = Double(_center!.x) + Double(_radius!) * cos(Double(2 * indexPath.item) * M_PI/Double(_cellCount!))
let y = Double(_center!.y) + Double(_radius!) * sin(Double(2 * indexPath.item) * M_PI/Double(_cellCount!))
attrs.center = CGPointMake( CGFloat(x),CGFloat(y));
return attrs
}
这个方法layoutAttributesForItemAtIndexPath
对UICollectionViewLayoutAttributes
的一些属性进行设置 ,前面列出过 ,然后layoutAttributesForElementsInRect
方法返回所有UICollectionViewLayoutAttributes
, 以数组的方式
然后再使用的时候把基本用法里的layout
换掉
layout = MyCollectionViewLayout()
collect = UICollectionView(frame: self.view.frame,collectionViewLayout:layout)
collect.backgroundColor = UIColor.whiteColor()
collect.delegate = self
collect.dataSource = self
这样运行 , 圆就出现了
if(layout is MyCollectionViewLayout){
layout = UICollectionViewFlowLayout()
(layout as! UICollectionViewFlowLayout).scrollDirection = UICollectionViewScrollDirection.Vertical //滚动方向
(layout as! UICollectionViewFlowLayout).itemSize = CGSizeMake(60,75)
}else{
layout = MyCollectionViewLayout()
}
self.collect.setCollectionViewLayout(layout,animated: true)
可以通过setCollectionViewLayout
方法来切换layout
然后给这个界面添加手势
//注册tap手势事件
let tapRecognizer = UITapGestureRecognizer(target: self,action: "handleTap:")
collect.addGestureRecognizer(tapRecognizer)
func handleTap(sender:UITapGestureRecognizer){
if sender.state == UIGestureRecognizerState.Ended{
let tapPoint = sender.locationInView(self.collect)
if let indexPath = self.collect.indexPathForItemAtPoint(tapPoint)
{
//点击了cell
//这个方法可以用来对collectionView中的元素进行批量的插入,删除,移动等操作,同时将触发collectionView所对应的layout的对应的动画。
print("------")
self.collect.performBatchUpdates({ () -> Void in self.collect.deleteItemsAtIndexPaths([indexPath]) self.dic.removeAtIndex(indexPath.row) },completion: nil) }else{ let val = arc4random_uniform(8)+1 self.dic.append(CellContent(img: "f"+String(val),name: "歪脖子"+String(val))) self.collect.insertItemsAtIndexPaths([NSIndexPath(forItem: Int(val),inSection: 0)]) // dispatch_async(dispatch_get_global_queue(0,0),{ () -> Void in // let val = arc4random_uniform(9) // self.dic.append(CellContent(img: "f"+String(val),name: "歪脖子"+String(val))) // dispatch_async(dispatch_get_main_queue()) { // self.collect.reloadData() // // } // }) //点击了不是cell的区域 print("+++++++") } } }
我注释掉这段GCD的代码也是可以执行的 ,就是没有动画 。
这个方法performBatchUpdates:completion
可以用来对collectionView中的元素进行批量的插入,删除,移动等操作,同时将触发collectionView所对应的layout的对应的动画。相应的动画由layout中的下列四个方法来定义:
initialLayoutAttributesForAppearingItemAtIndexPath:
initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:
finalLayoutAttributesForDisappearingItemAtIndexPath:
finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:
默认的动画是这样的
我们可以自定义动画
每次重新给出layout时都会调用prepareLayout,这样在以后如果有collectionView大小变化的需求时也可以自动适应变化。
override func initialLayoutAttributesForAppearingItemAtIndexPath(itemIndexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
// Must call super
var attributes = super.initialLayoutAttributesForAppearingItemAtIndexPath(itemIndexPath)
if self.insertIndexPaths.contains(itemIndexPath) {
if let _ = attributes{
attributes = self.layoutAttributesForItemAtIndexPath(itemIndexPath)
}
// Configure attributes ...
attributes!.alpha = 0.0;
attributes!.center = CGPointMake(_center!.x,_center!.y);
//attributes?.size = CGSizeMake(1000,1000)
}
return attributes;
}
override func prepareForCollectionViewUpdates(updateItems: [UICollectionViewUpdateItem]) {
super.prepareForCollectionViewUpdates(updateItems)
self.insertIndexPaths = [NSIndexPath]()
for update in updateItems{
if update.updateAction == UICollectionUpdateAction.Insert{
self.insertIndexPaths.append(update.indexPathAfterUpdate)
}
}
首先会调用prepareForCollectionViewUpdates
,我们在这里拿到那个新增的NSIndexPath
,然后initialLayoutAttributesForAppearingItemAtIndexPath
在这个方法中设置一些初始位置。
看下效果
这个是从中间散出去的 ,同理也可以搞一些别的效果 。大概就这些吧。当然UICollectionView可以玩的还很多,期待大家一起探索。多分享哦!
(本实例使用xcode 7 bate , swift 2.0)
最后附上源码:https://github.com/smalldu/SwiftStudy