学习文章
- 一个UICollectionView自定义layout的实现
- iOS6新特征:UICollectionView介绍
- WWDC 2012 Session笔记——205 Introducing Collection Views
- WWDC 2012 Session笔记——219 Advanced Collection Views and Building Custom Layouts
UICollectionView补充
一、 UICollectionView
标准的UICollectionView包含三个部分,它们都是UIView的子类:
- Cells 用于展示内容的主体,对于不同的cell可以指定不同尺寸和不同的内容
- Supplementary Views 追加视图 如果你对UITableView比较熟悉的话,可以理解为每个Section的Header或者Footer,用来标记每个section的view
- Decoration Views 装饰视图 这是每个section的背景
二、 UICollectionViewDataSource
- section的数量 -numberOfSectionsInCollection:
- 某个section里有多少个item -collectionView:numberOfItemsInSection:
- 对于某个位置应该显示什么样的cell -collectionView:cellForItemAtIndexPath:
实现以上三个委托方法,基本上就可以保证CollectionView工作正常了。当然,还有提供Supplementary View的方法
- collectionView:viewForSupplementaryElementOfKind:atIndexPath:
对于Decoration Views,提供方法并不在UICollectionViewDataSource中,而是直接UICollectionViewLayout类中的(因为它仅仅是视图相关,而与数据无关).
三、 UICollectionViewDelegate
数据无关的view的外形啊,用户交互啊什么的,由UICollectionViewDelegate来负责:
- cell的高亮
- cell的选中状态
- 可以支持长按后的菜单
每个cell有独立的高亮事件和选中事件的delegate,用户点击cell的时候,现在会按照以下流程向delegate进行询问:
-collectionView:shouldHighlightItemAtIndexPath: 是否应该高亮?
-collectionView:didHighlightItemAtIndexPath: 如果1回答为是,那么高亮
-collectionView:shouldSelectItemAtIndexPath: 无论1结果如何,都询问是否可以被选中?
-collectionView:didUnhighlightItemAtIndexPath: 如果1回答为是,那么现在取消高亮
-collectionView:didSelectItemAtIndexPath: 如果3回答为是,那么选中cell
四、 关于Cell
UICollectionViewCell不存在各式各样的默认的style,这主要是由于展示对象的性质决定的,因为UICollectionView所用来展示的对象相比UITableView来说要来得灵活,大部分情况下更偏向于图像而非文字,因此需求将会千奇百怪。因此SDK提供给我们的默认的UICollectionViewCell结构上相对比较简单,由下至上:
- 首先是cell本身作为容器view
- 然后是一个大小自动适应整个cell的backgroundView,用作cell平时的背景
- 再其上是selectedBackgroundView,是cell被选中时的背景
- 最后是一个contentView,自定义内容应被加在这个view上
这次Apple给我们带来的好康是被选中cell的自动变化,所有的cell中的子view,也包括contentView中的子view,在当cell被选中时,会自动去查找view是否有被选中状态下的改变。比如在contentView里加了一个normal和selected指定了不同图片的imageView,那么选中这个cell的同时这张图片也会从normal变成selected,而不需要额外的任何代码。
五、 UICollectionViewLayout
这是UICollectionView和UITableView最大的不同。
UICollectionViewLayout可以说是UICollectionView的大脑和中枢,它负责了将各个cell、Supplementary View和Decoration Views进行组织,为它们设定各自的属性,包括但不限于:
- 位置
- 尺寸
- 透明度
- 层级关系
- 形状
- 等等等等…
Layout决定了UICollectionView是如何显示在界面上的。在展示之前,一般需要生成合适的UICollectionViewLayout子类对象,并将其赋予CollectionView的collectionViewLayout属性。
六、 UICollectionViewLayoutAttributes
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
UICollectionViewLayout的功能为向UICollectionView提供布局信息,不仅包括cell的布局信息,也包括追加视图和装饰视图的布局信息。实现一个自定义layout的常规做法是继承UICollectionViewLayout类,然后重载下列方法:
-
-(CGSize)collectionViewContentSize
- 返回collectionView的内容的尺寸
-
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
- 返回rect中的所有的元素的布局属性
- 返回的是包含UICollectionViewLayoutAttributes的NSArray
- UICollectionViewLayoutAttributes可以是cell,追加视图或装饰视图的信息,通过不同的UICollectionViewLayoutAttributes初始化方法可以得到不同类型的UICollectionViewLayoutAttributes
- layoutAttributesForCellWithIndexPath:
- layoutAttributesForSupplementaryViewOfKind:withIndexPath:
- layoutAttributesForDecorationViewOfKind:withIndexPath:
-
-(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来生成更新后的布局。
实现DecorationView
一、 效果
二、 源码
Defination.swift
import UIKit
let screenWidth = UIScreen.mainScreen().bounds.width
DecorationView.swift
import UIKit
let decorationIdentifier = "DecorationIdentifier"
class DecorationView: UICollectionReusableView {
var imageView : UIImageView?
override init(frame: CGRect) {
super.init(frame: frame)
imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: screenWidth, height: 120))
self.imageView!.image = UIImage(named: "SingleBookShelf")
self.addSubview(imageView!)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
DemoCollectionViewCell.swift
import UIKit
let cellIdentifier = "CellIdentifier"
class DemoCollectionViewCell: UICollectionViewCell {
var imageView : UIImageView!
override init(frame: CGRect) {
super.init(frame: frame)
self.imageView = UIImageView(frame: self.bounds)
self.addSubview(self.imageView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
DemoLayout.swift
import UIKit
class DemoLayout: UICollectionViewFlowLayout {
var cellCount : Int?
var sectionCount : Int?
override func prepareLayout() {
super.prepareLayout()
self.cellCount = self.collectionView?.numberOfItemsInSection(0)
self.sectionCount = self.collectionView?.numberOfSections()
self.itemSize = CGSize(width: 100, height: 100)
self.sectionInset = UIEdgeInsets(top: 25, left: 25, bottom: 25, right: 25)
self.minimumInteritemSpacing = 40
self.minimumLineSpacing = 20
self.registerClass(DecorationView.classForCoder(), forDecorationViewOfKind: decorationIdentifier)
}
override func layoutAttributesForDecorationViewOfKind(elementKind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = UICollectionViewLayoutAttributes(forDecorationViewOfKind: elementKind, withIndexPath: indexPath)
attributes.frame = CGRect(x: 0, y: indexPath.row * 120 + 12, width: Int(screenWidth), height: 120)
attributes.zIndex = -1
return attributes
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// 方法1: 通过主动调用 self.layoutAttributesForItemAtIndexPath(indexPath) 和 self.layoutAttributesForDecorationViewOfKind(decorationIdentifier, atIndexPath: indexPath) 生成全部 UICollectionViewLayoutAttributes
var attributesArray = [UICollectionViewLayoutAttributes]()
for i : Int in 0..<cellCount! {
let indexPath = NSIndexPath(forItem: i, inSection: 0)
let attributes = self.layoutAttributesForItemAtIndexPath(indexPath)
attributesArray.append(attributes!)
}
for i : Int in 0..<cellCount!/2 {
let indexPath = NSIndexPath(forRow: i, inSection: 0)
let attributes = self.layoutAttributesForDecorationViewOfKind(decorationIdentifier, atIndexPath: indexPath)
attributesArray.append(attributes!)
}
return attributesArray
// 方法2: 通过调用 super.layoutAttributesForElementsInRect(rect) ,获得已经拥有的 UICollectionViewLayoutAttributes,根据已经拥有的,增添你需要的UICollectionViewLayoutAttributes
// let attributes : [UICollectionViewLayoutAttributes]? = super.layoutAttributesForElementsInRect(rect)
//
// var allAttibutes = attributes
//
// for attribute in attributes! {
//
// if attribute.frame.origin.x == 25 {
//
// print(attribute.indexPath)
// let decorationAttributes : UICollectionViewLayoutAttributes =
// UICollectionViewLayoutAttributes(forDecorationViewOfKind: decorationIdentifier, withIndexPath: attribute.indexPath)
//
// decorationAttributes.frame =
// CGRect(x: 0, y: attribute.frame.origin.y - 10, width: screenWidth, height: 120)
//
// decorationAttributes.zIndex = attribute.zIndex - 1
//
// allAttibutes?.append(decorationAttributes)
// }
// }
//
// return allAttibutes
}
}
ViewController.swift
import UIKit
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
var collectionView : UICollectionView!
var dataArray : [UIImage]?
var demoLayout : DemoLayout!
override func viewDidLoad() {
super.viewDidLoad()
self.buildData()
self.buildCollectionView()
}
func buildData() {
self.dataArray = [UIImage]()
for imageIndex in 1...16 {
let image = UIImage(named: "\(imageIndex)")
self.dataArray?.append(image!)
}
}
func buildCollectionView() {
demoLayout = DemoLayout()
self.collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: demoLayout)
self.collectionView.registerClass(DemoCollectionViewCell.classForCoder(), forCellWithReuseIdentifier: cellIdentifier)
self.collectionView.dataSource = self
self.collectionView.delegate = self
self.view.addSubview(self.collectionView)
}
// MARK: UICollectionViewDataSource
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return (self.dataArray?.count)!
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell =
collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath) as! DemoCollectionViewCell
cell.imageView.image = self.dataArray![indexPath.row]
return cell
}
}