自定义视图UICollectionViewFlowLayout怎么添加头部视图

当前位置:
& Swift - 实现UICollectionView分组头悬停效果(方法1:使用自定义布局)
Swift - 实现UICollectionView分组头悬停效果(方法1:使用自定义布局)
发布:hangge
我们知道表格 UITableView 可以设置多个 section(分区、分组),而且如果 tableView 是使用 plain 样式的话,分组头还会有有 sticky 效果(粘性效果、悬停效果)。
而在 iOS9 之前,UICollectionView 虽然也可以设置多个 section,但其 section header 并没有悬停效果,而是跟随单元格一同上下移动。
下面演示如何通过自定义布局类,来实现 collectionView 的分组头悬停效果。
可以看到随着视图上下滚动,当前分组的分组头会一直停留在固定位置(对应分组可视区域的顶端)。
2,实现代码
(1)自定义单元格类:MyCollectionViewCell.swift(创建的时候生成对应的 xib 文件)
import UIKit
//自定义的Collection View单元格
class MyCollectionViewCell: UICollectionViewCell {
//用于显示书籍封面图片
@IBOutlet weak var imageView: UIImageView!
override func awakeFromNib() {
super.awakeFromNib()
(2)自定义分组头:MySectionHeader.swift(创建的时候生成对应的 xib 文件)
import UIKit
//自定义的Collection View分组头
class MySectionHeader: UICollectionReusableView {
//用于显示分组标题
@IBOutlet weak var titleLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
(3)自定义布局类:StickyHeadersFlowLayout.swift
import UIKit
//自定义的具有粘性分组头的Collection View布局类
class StickyHeadersFlowLayout: UICollectionViewFlowLayout {
//边界发生变化时是否重新布局(视图滚动的时候也会调用)
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -& Bool {
return true
//所有元素的位置属性
override func layoutAttributesForElements(in rect: CGRect)
-& [UICollectionViewLayoutAttributes]? {
//从父类得到默认的所有元素属性
guard let layoutAttributes = super.layoutAttributesForElements(in: rect)
else { return nil }
//用于存储元素新的布局属性,最后会返回这个
var newLayoutAttributes = [UICollectionViewLayoutAttributes]()
//存储每个layout attributes对应的是哪个section
let sectionsToAdd = NSMutableIndexSet()
//循环老的元素布局属性
for layoutAttributesSet in layoutAttributes {
//如果元素师cell
if layoutAttributesSet.representedElementCategory == .cell {
//将布局添加到newLayoutAttributes中
newLayoutAttributes.append(layoutAttributesSet)
} else if layoutAttributesSet.representedElementCategory == .supplementaryView {
//将对应的section储存到sectionsToAdd中
sectionsToAdd.add(layoutAttributesSet.indexPath.section)
//遍历sectionsToAdd,补充视图使用正确的布局属性
for section in sectionsToAdd {
let indexPath = IndexPath(item: 0, section: section)
//添加头部布局属性
if let headerAttributes = self.layoutAttributesForSupplementaryView(ofKind:
UICollectionElementKindSectionHeader, at: indexPath) {
newLayoutAttributes.append(headerAttributes)
//添加尾部布局属性
if let footerAttributes = self.layoutAttributesForSupplementaryView(ofKind:
UICollectionElementKindSectionFooter, at: indexPath) {
newLayoutAttributes.append(footerAttributes)
return newLayoutAttributes
//补充视图的布局属性(这里处理实现粘性分组头,让分组头始终处于分组可视区域的顶部)
override func layoutAttributesForSupplementaryView(ofKind elementKind: String,
at indexPath: IndexPath) -& UICollectionViewLayoutAttributes? {
//先从父类获取补充视图的布局属性
guard let layoutAttributes = super.layoutAttributesForSupplementaryView(ofKind:
elementKind, at: indexPath) else { return nil }
//如果不是头部视图则直接返回
if elementKind != UICollectionElementKindSectionHeader {
return layoutAttributes
//根据section索引,获取对应的边界范围
guard let boundaries = boundaries(forSection: indexPath.section)
else { return layoutAttributes }
guard let collectionView = collectionView else { return layoutAttributes }
//保存视图内入垂直方向的偏移量
let contentOffsetY = collectionView.contentOffset.y
//补充视图的frame
var frameForSupplementaryView = layoutAttributes.frame
//计算分组头垂直方向的最大最小值
let minimum = boundaries.minimum - frameForSupplementaryView.height
let maximum = boundaries.maximum - frameForSupplementaryView.height
//如果内容区域的垂直偏移量小于分组头最小的位置,则将分组头置于其最小位置
if contentOffsetY & minimum {
frameForSupplementaryView.origin.y = minimum
//如果内容区域的垂直偏移量大于分组头最小的位置,则将分组头置于其最大位置
else if contentOffsetY & maximum {
frameForSupplementaryView.origin.y = maximum
//如果都不满足,则说明内容区域的垂直便宜量落在分组头的边界范围内。
//将分组头设置为内容偏移量,从而让分组头固定在集合视图的顶部
frameForSupplementaryView.origin.y = contentOffsetY
//更新布局属性并返回
layoutAttributes.frame = frameForSupplementaryView
return layoutAttributes
//根据section索引,获取对应的边界范围(返回一个元组)
func boundaries(forSection section: Int) -& (minimum: CGFloat, maximum: CGFloat)? {
//保存返回结果
var result = (minimum: CGFloat(0.0), maximum: CGFloat(0.0))
//如果collectionView属性为nil,则直接fanhui
guard let collectionView = collectionView else { return result }
//获取该分区中的项目数
let numberOfItems = collectionView.numberOfItems(inSection: section)
//如果项目数位0,则直接返回
guard numberOfItems & 0 else { return result }
//从流布局属性中获取第一个、以及最后一个项的布局属性
let first = IndexPath(item: 0, section: section)
let last = IndexPath(item: (numberOfItems - 1), section: section)
if let firstItem = layoutAttributesForItem(at: first),
let lastItem = layoutAttributesForItem(at: last) {
//分别获区边界的最小值和最大值
result.minimum = firstItem.frame.minY
result.maximum = lastItem.frame.maxY
//将分区都的高度考虑进去,并调整
result.minimum -= headerReferenceSize.height
result.maximum -= headerReferenceSize.height
//将分区的内边距考虑进去,并调整
result.minimum -= sectionInset.top
result.maximum += (sectionInset.top + sectionInset.bottom)
//返回最终的边界值
return result
(4)使用样例:ViewController.swift
import UIKit
//每月书籍
struct BookPreview {
var title:String
var images:[String]
class ViewController: UIViewController {
//重用的单元格和分区头的Identifier
let CellIdentifier = "myCell"
let HeaderIdentifier = "myHeader"
//所有书籍数据
let books = [
BookPreview(title: "五月新书", images: ["0.jpg", "1.jpg","2.jpg", "3.jpg",
"4.jpg","5.jpg","6.jpg"]),
BookPreview(title: "六月新书", images: ["7.jpg", "8.jpg", "9.jpg"]),
BookPreview(title: "七月新书", images: ["10.jpg", "11.jpg", "12.jpg", "13.jpg"])
override func viewDidLoad() {
super.viewDidLoad()
//去除存在导航栏时内内边距自动调整功能,防止对自定义的Collection View分区头停留功能造成影响
self.automaticallyAdjustsScrollViewInsets = false
//初始化Collection View
initCollectionView()
private func initCollectionView() {
//初始化自定义的flow布局
let layout = StickyHeadersFlowLayout()
//Collection View的位置尺寸
let frame = CGRect(x: 0, y: 64, width: view.bounds.width,
height: view.bounds.height - 64)
//初始化Collection View
let collectionView = UICollectionView(frame: frame, collectionViewLayout: layout)
//Collection View代理设置
collectionView.delegate = self
collectionView.dataSource = self
collectionView.backgroundColor = .white
//注册重用的单元格
let cellXIB = UINib.init(nibName: "MyCollectionViewCell", bundle: Bundle.main)
collectionView.register(cellXIB, forCellWithReuseIdentifier: CellIdentifier)
//注册重用的分组头
let headerXIB = UINib.init(nibName: "MySectionHeader", bundle: Bundle.main)
collectionView.register(headerXIB, forSupplementaryViewOfKind:
UICollectionElementKindSectionHeader, withReuseIdentifier: HeaderIdentifier)
//将Collection View添加到主视图中
view.addSubview(collectionView)
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
//Collection View数据源协议相关方法
extension ViewController: UICollectionViewDataSource {
//获取分区数
func numberOfSections(in collectionView: UICollectionView) -& Int {
return books.count
//获取每个分区里单元格数量
func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -& Int {
return books[section].images.count
//返回每个单元格视图
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -& UICollectionViewCell {
//获取重用的单元格
let cell = collectionView.dequeueReusableCell(withReuseIdentifier:
CellIdentifier, for: indexPath) as! MyCollectionViewCell
//设置内部显示的图片
cell.imageView.image = UIImage(named: books[indexPath.section].images[indexPath.item])
return cell
//分区的header
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind
kind: String, at indexPath: IndexPath) -& UICollectionReusableView {
// 获取重用的分组头
if let header = collectionView.dequeueReusableSupplementaryView(ofKind:
UICollectionElementKindSectionHeader, withReuseIdentifier: HeaderIdentifier,
for: indexPath) as? MySectionHeader {
//设置分组标题
header.titleLabel.text = books[indexPath.section].title
return header
fatalError("获取重用视图失败!")
//Collection View样式布局协议相关方法
extension ViewController: UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
//返回分组头大小
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
referenceSizeForHeaderInSection section: Int) -& CGSize {
return CGSize(width: collectionView.bounds.width, height: 45.0)
//返回单元格大小
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -& CGSize {
let itemWidth = (collectionView.bounds.width - 5)/3
let itemHeight = itemWidth / 3 * 4
return CGSize(width: itemWidth, height: itemHeight)
//每个分组的内边距
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAt section: Int) -& UIEdgeInsets {
return UIEdgeInsets.zero
//单元格的行间距
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -& CGFloat {
return 2.0
//单元格横向的最小间距
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumInteritemSpacingForSectionAt section: Int) -& CGFloat {
return 0.0
源码下载:
补充:给CollectionView添加个页眉
(1)collectionView 除了每个分组都有对应的分组头外,还有个整体的 header view。
(2)单元格上移时,整体的页眉也会上移,而分组头有悬停效果。
& & && & & &
2,ViewController.swift(高亮部分为修改的地方)
import UIKit
//每月书籍
struct BookPreview {
var title:String
var images:[String]
class ViewController: UIViewController {
//重用的单元格和分区头的Identifier
let CellIdentifier = "myCell"
let HeaderIdentifier = "myHeader"
//所有书籍数据
let books = [
BookPreview(title: "五月新书", images: ["0.jpg", "1.jpg","2.jpg", "3.jpg",
"4.jpg","5.jpg","6.jpg"]),
BookPreview(title: "六月新书", images: ["7.jpg", "8.jpg", "9.jpg"]),
BookPreview(title: "七月新书", images: ["10.jpg", "11.jpg", "12.jpg", "13.jpg"])
override func viewDidLoad() {
super.viewDidLoad()
//去除存在导航栏时内内边距自动调整功能,防止对自定义的Collection View分区头停留功能造成影响
self.automaticallyAdjustsScrollViewInsets = false
//初始化Collection View
initCollectionView()
private func initCollectionView() {
//初始化自定义的flow布局
let layout = StickyHeadersFlowLayout()
//Collection View的位置尺寸
let frame = CGRect(x: 0, y: 64, width: view.bounds.width,
height: view.bounds.height - 64)
//初始化Collection View
let collectionView = UICollectionView(frame: frame, collectionViewLayout: layout)
//Collection View代理设置
collectionView.delegate = self
collectionView.dataSource = self
collectionView.backgroundColor = .white
//注册重用的单元格
let cellXIB = UINib.init(nibName: "MyCollectionViewCell", bundle: Bundle.main)
collectionView.register(cellXIB, forCellWithReuseIdentifier: CellIdentifier)
//注册重用的分组头
let headerXIB = UINib.init(nibName: "MySectionHeader", bundle: Bundle.main)
collectionView.register(headerXIB, forSupplementaryViewOfKind:
UICollectionElementKindSectionHeader, withReuseIdentifier: HeaderIdentifier)
//页眉高度
let headerViewH:CGFloat = 60
//创建页眉
let headerView:UIView = UIView(frame:
CGRect(x:0, y:-headerViewH, width:frame.size.width, height:60))
let headerlabel:UILabel = UILabel(frame: headerView.bounds)
headerlabel.textColor = UIColor.white
headerlabel.backgroundColor = UIColor.clear
headerlabel.font = UIFont.systemFont(ofSize: 16)
headerlabel.text = "CollectionView 页眉"
headerView.addSubview(headerlabel)
headerView.backgroundColor = UIColor.black
//将头部headView添加到collectionView
collectionView.addSubview(headerView)
//插入位置
collectionView.contentInset = UIEdgeInsets(top: headerViewH, left: 0, bottom: 0, right: 0)
//将Collection View添加到主视图中
view.addSubview(collectionView)
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
//Collection View数据源协议相关方法
extension ViewController: UICollectionViewDataSource {
//获取分区数
func numberOfSections(in collectionView: UICollectionView) -& Int {
return books.count
//获取每个分区里单元格数量
func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -& Int {
return books[section].images.count
//返回每个单元格视图
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -& UICollectionViewCell {
//获取重用的单元格
let cell = collectionView.dequeueReusableCell(withReuseIdentifier:
CellIdentifier, for: indexPath) as! MyCollectionViewCell
//设置内部显示的图片
cell.imageView.image = UIImage(named: books[indexPath.section].images[indexPath.item])
return cell
//分区的header
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind
kind: String, at indexPath: IndexPath) -& UICollectionReusableView {
// 获取重用的分组头
if let header = collectionView.dequeueReusableSupplementaryView(ofKind:
UICollectionElementKindSectionHeader, withReuseIdentifier: HeaderIdentifier,
for: indexPath) as? MySectionHeader {
//设置分组标题
header.titleLabel.text = books[indexPath.section].title
return header
fatalError("获取重用视图失败!")
//Collection View样式布局协议相关方法
extension ViewController: UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
//返回分组头大小
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
referenceSizeForHeaderInSection section: Int) -& CGSize {
return CGSize(width: collectionView.bounds.width, height: 45.0)
//返回单元格大小
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -& CGSize {
let itemWidth = (collectionView.bounds.width - 5)/3
let itemHeight = itemWidth / 3 * 4
return CGSize(width: itemWidth, height: itemHeight)
//每个分组的内边距
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAt section: Int) -& UIEdgeInsets {
return UIEdgeInsets.zero
//单元格的行间距
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -& CGFloat {
return 2.0
//单元格横向的最小间距
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumInteritemSpacingForSectionAt section: Int) -& CGFloat {
return 0.03444人阅读
iOS 布局(41)
给UICollectionView添加头视图
#import &PhotoWallCell.h&
#import &PhotoWallModel.h&
#define kScreenWidth [UIScreen mainScreen].bounds.size.width
#define kScreenHeight [UIScreen mainScreen].bounds.size.height
NSString *collectionCellIndentider =
@&collectionCellIndentider&;
@interface
ViewController () &UITextFieldDelegate,UICollectionViewDataSource,UICollectionViewDelegate&
@property (nonatomic ,weak)
UICollectionView& & & & *collectionV
@property (nonatomic,
strong) NSMutableArray& & & & *dataS
@property (nonatomic,
strong) NSMutableArray& & & & *section1;
@property (nonatomic,
strong) NSMutableArray& & & & *section2;
- (void)setupSomeParamars
& & _section1 = [NSMutableArray
& & _section2 = [NSMutableArray
for(int num =
0;num&5;num++){
& & & & PhotoWallModel *model = [[PhotoWallModel
& & & & model.photo = [UIImage
imageNamed:@&1.png&];
& & & & [_section1
addObject:model];
for(int num =
0;num&6;num++){
& & & & PhotoWallModel *model = [[PhotoWallModel
& & & & model.photo = [UIImage
imageNamed:@&1.png&];
& & & & [_section2
addObject:model];
& & _dataSource = [NSMutableArray
arrayWithObjects:_section1,_section2,
- (UICollectionView *)collectionView{
& & if (_collectionView) {
& & & & return
_collectionView;
CGFloat collectionViewHeight =
kScreenHeight - 190;
CGRect frame = CGRectMake(0,
150, kScreenWidth, collectionViewHeight);
& & UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout
layout.headerReferenceSize = CGSizeMake(320, 50);
UICollectionView *collectionView = [[UICollectionView
alloc] initWithFrame:frame
collectionViewLayout:layout];
& & collectionView.opaque =
& & collectionView.backgroundColor =
self.view.backgroundColor;
& & collectionView.dataSource =
& & collectionView.delegate =
& & collectionView.pagingEnabled =
& & collectionView.showsVerticalScrollIndicator =
& & collectionView.showsHorizontalScrollIndicator =
& & [self.view
addSubview:collectionView];
_collectionView = collectionV
&& & *& @brief&
& & [collectionView registerClass:[PhotoWallCell
class] forCellWithReuseIdentifier:collectionCellIndentider];
[collectionView registerClass:[PhotoWallCell class]& forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:collectionCellIndentider];
return collectionV
#pragma mark -
#pragma mark -cell Delegate
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath
*)indexPath{
& & PhotoWallCell *cell = [collectionView
dequeueReusableCellWithReuseIdentifier:collectionCellIndentider
forIndexPath:indexPath];
& & cell.model =
self.dataSource[indexPath.section][indexPath.row];
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath
*)indexPath
& & PhotoWallModel *model = [[PhotoWallModel
& & model.photo = [UIImage
imageNamed:@&1&];
& & [_section1
addObject:model];
& & [self.collectionView
reloadData];
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
return [self.dataSource[section]
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{
& & return
self.dataSource.count;
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath
*)indexPath{
& & UICollectionReusableView *reusableview =
& & if (kind == UICollectionElementKindSectionHeader){
& & & & UICollectionReusableView *headerView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:collectionCellIndentider
forIndexPath:indexPath];
& & & & headerView.backgroundColor = [UIColor redColor];
& & & & reusableview = headerV
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:261466次
积分:4465
积分:4465
排名:第6869名
原创:167篇
转载:120篇
评论:33条
阅读:6407
(3)(5)(6)(3)(6)(10)(4)(3)(4)(7)(8)(6)(4)(8)(12)(21)(20)(6)(14)(7)(2)(13)(7)(11)(6)(21)(18)(33)(16)(5)
(window.slotbydup = window.slotbydup || []).push({
id: '4740887',
container: s,
size: '250,250',
display: 'inlay-fix'原文出自:&
UICollectionView在iOS6中第一次被介绍,也是UIKit视图类中的一颗新星。它和UITableView共享API设计,但也在UITableView上做了一些扩展。UICollectionView最强大、同时显著超出UITableView的特色就是其完全灵活的布局结构。在这篇文章中,我们将会实现一个相当复杂的自定义collection view布局,并且顺便讨论一下这个类设计的重要部分。项目的示例代码在上。
UITableView和UICollectionView都是由驱动的。他们为其显示的子视图集扮演为愚蠢的容器(dumb containers),对他们真实的内容(contents)毫不知情。
UICollectionView进一步抽象了。它将其子视图的位置,大小和外观的控制权委托给一个单独的布局对象。通过提供一个自定义布局对象,你几乎可以实现任何你能想象到的布局。布局继承自UICollectionViewLayout这个抽象基类。iOS6中以UICollectionViewFlowLayout类的形式提出了一个具体的布局实现。
flow layout可以被用来实现一个标准的grid view,这可能是在collection view中最常见的使用案例了。尽管大多数人都这么想,但是Apple很聪明,没有明确的命名这个类为UICollectionViewGridLayout。而使用了更为通用的术语flow layout,这更好的描述了该类的能力:它通过一个接一个的放置cell来建立自己的布局,当需要的时候,插入横排或竖排的分栏符。通过自定义滚动方向,大小和cell之间的间距,flow layout也可以在单行或单列中布局cell。实际上,UITableView的布局可以想象成flow layout的一种特殊情况。
在你准备自己写一个UICollectionViewLayout的子类之前,你需要问你自己,你是否能够使用UICollectionViewFlowLayout实现你心里的布局。这个类是很容易定制的,并且可以继承本身进行近一步的定制。感兴趣的看。
Cells和其他Views
为了适应任意布局,collection view建立一个了类似,但比table view更灵活的视图层级(view hierarchy)。像往常一样,你的主要内容显示在cell中,cell可以被任意分组到section中。Collection view的cells必须是UICollectionViewCell的子类。除了cells,collection view额外管理着两种视图:supplementary views和decoration views。
collection view中的Supplementary views相当于table view的section header和footer views。像cells一样,他们的内容都由数据源对象驱动。然而,和table view中用法不一样,supplementary view并不一定会作为header或footer view;他们的数量和放置的位置完全由布局控制。
Decoration views纯粹为一个装饰品。他们完全属于布局对象,并被布局对象管理,他们并不从数据源获取他们的contents。当布局对象指定它需要一个decoration view的时候,collection view会自动创建,并为其应用布局对象提供的布局参数。并不需要准备任何自定义视图的内容。
Supplementary views和decoration views必须是UICollectionResuableView的子类。每个你布局所使用的视图类都需要在collection view中注册,这样当data source让他从reuse pool中出列时,它才能够创建新的实例。如果你是使用的Interface Builder,则可以通过在可视编辑器中拖拽一个cell到collection view上完成cell在collection view中的注册。同样的方法也可以用在supplementary view上,前提是你使用了UICollectionViewFlowLayout。如果没有,你只能通过调用registerClass:或者registerNib:方法手动注册视图类了。你需要在viewDidLoad中做这些操作。
自定义布局
作为一个非常有意义的自定义collection view布局的例子,我们不妨设想一个典型的日历应用程序中的周(week)视图。日历一次显示一周,星期中的每一天显示在列中。每一个日历事件将会在我们的collection view中以一个cell显示,位置和大小代表事件起始日期时间和持续时间。
一般有两种类型的collection view布局:
1.独立于内容的布局计算。这正是你所知道的像UITableView和UICollectionViewFlowLayout这些情况。每个cell的位置和外观不是基于其显示的内容,但所有cell的显示顺序是基于内容的顺序。可以把默认的flow layout做为例子。每个cell都基于前一个cell放置(或者如果没有足够的空间,则从下一行开始)。布局对象不必访问实际数据来计算布局。
2.基于内容的布局计算。我们的日历视图正是这样类型的例子。为了计算显示事件的起始和结束时间,布局对象需要直接访问collection view的数据源。在很多情况下,布局对象不仅需要取出当前可见cell的数据,还需要从所有记录中取出一些决定当前哪些cell可见的数据。
在我们的日历示例中,布局对象如果访问某一个矩形内cells的属性,那就必须迭代数据源提供的所有事件来决定哪些位于要求的时间窗口中。 与一些相对简单,数据源独立计算的flow layout比起来,这足够计算出cell在一个矩形内的index paths了(假设网格中所有cells的大小都一样)。
如果有一个依赖内容的布局,那就是暗示你需要写自定义的布局类了,同时不能使用自定义的UICollectionViewFlowLayout。所以这正是我们需要做的事情。
列出了子类需要重写的方法。
collectionViewContentSize
由于collection view对它的content并不知情,所以布局首先要提供的信息就是滚动区域大小,这样collection view才能正确的管理滚动。布局对象必须在此时计算它内容的总大小,包括supplementary views和decoration views。注意,尽管大多数经典的collection view限制在一个轴方向上滚动(正如UICollectionViewFlowLayout一样),但这不是必须的。
在我们的日历示例中,我们想要视图垂直的滚动。比如,如果我们想要在垂直空间上一个小时占去100点,这样显示一整天的内容高度就是2400点。注意,我们不能够水平滚动,这就意味这我们collection view只能显示一周。为了能够在日历中的多个星期间分页,我们可以在一个独立(分页)的scroll view(可以使用UIPageViewController)中使用多个collection view(一周一个),或者坚持使用一个collection view并且返回足够大的内容宽度,这会使得用户感觉在两个方向上滑动自由。
-&(CGSize)collectionViewContentSize&
CGFloat&contentWidth&=&self.collectionView.bounds.size.&
CGFloat&contentHeight&=&DayHeaderHeight&+&(HeightPerHour&*&HoursPerDay);&
CGSize&contentSize&=&CGSizeMake(contentWidth,&contentHeight);&
return&contentS&
为了清楚起见,我选择布局在一个非常简单模型上:假定每周天数相同,每天时长相同,
也就是说天数用0-6表示。在一个真实的日历程序中,布局将会为自己的计算大量使用基于NSCalendar的日期。
layoutAttributesForElementsInRect:
这是任何布局类中最重要的方法了,同时可能也是最容易让人迷惑的方法。collection view调用这个方法并传递一个自身坐标系统中的矩形过去。这个矩形代表了这个视图的可见矩形区域(也就是它的bounds),你需要准备好处理传给你的任何矩形。
你的实现必须返回一个包含UICollectionViewLayoutAttributes对象的数组,为每一个cell包含这样的一个对象,supplementary view或decoration view在矩形区域内是可见的。UICollectionViewLayoutAttributes类包含了collection view内item的所有相关布局属性。默认情况下,这个类包含frame,center,size,transform3D,alpha,zIndex属性(properties),和hidden特性(attributes)。如果你的布局想要控制其他视图的属性(比如,背景颜色),你可以建一个UICollectionViewLayoutAttributes的子类,然后加上你自己的属性。
布局属性对象通过indexPath属性和他们对应的cell,supplementary view或者decoration view关联在一起。collection view为所有items从布局对象中请求到布局属性后,它将会实例化所有视图,并将对应的属性应用到每个视图上去。
注意!这个方法涉及到所有类型的视图,也就是cell,supplementary views和decoration views。一个幼稚的实现可能会选择忽略传入的矩形,并且为collection view中的所有视图返回布局属性。在原型设计和开发布局阶段,这是一个有效的方法。但是,这将对性能产生非常坏的影响,特别是可见cell远少于所有cell数量的时候,collection view和布局对象将会为那些不可见的视图做额外不必要的工作。
你的实现需要做这几步:
1.创建一个空的mutable数组来存放所有的布局属性。
2.确定index paths中哪些cells的frame完全或部分位于矩形中。这个计算需要你从collection view的数据源中取出你需要显示的数据。然后在循环中调用你实现的layoutAttributesForItemAtIndexPath:方法为每个index path创建并配置一个合适的布局属性对象,并将每个对象添加到数组中。
3.如果你的布局包含supplementary views,计算矩形内可见supplementary view的index paths。在循环中调用你实现的layoutAttributesForSupplementaryViewOfKind:atIndexPath:,并且将这些对象加到数组中。通过为kind参数传递你选择的不同字符,你可以区分出不同种类的supplementary views(比如headers和footers)。当需要创建视图时,collection view会将kind字符传回到你的数据源。记住supplementary和decoration views的数量和种类完全由布局控制。你不会受到headers和footers的限制。
4.如果布局包含decoration views,计算矩形内可见decoration views的index paths。在循环中调用你实现的layoutAttributesForDecorationViewOfKind:atIndexPath:,并且将这些对象加到数组中。
5.返回数组。
我们自定义的布局没有使用decoration views,但是使用了两种supplementary views(column headers和row headers)
-&(NSArray&*)layoutAttributesForElementsInRect:(CGRect)rect&
NSMutableArray&*layoutAttributes&=&[NSMutableArray&array];&
NSArray&*visibleIndexPaths&=&[self&indexPathsOfItemsInRect:rect];&
for&(NSIndexPath&*indexPath&in&visibleIndexPaths)&{&
UICollectionViewLayoutAttributes&*attributes&=&
[self&layoutAttributesForItemAtIndexPath:indexPath];&
[layoutAttributes&addObject:attributes];&
NSArray&*dayHeaderViewIndexPaths&=&[self&indexPathsOfDayHeaderViewsInRect:rect];&
for&(NSIndexPath&*indexPath&in&dayHeaderViewIndexPaths)&{&
UICollectionViewLayoutAttributes&*attributes&=&
[self&layoutAttributesForSupplementaryViewOfKind:@"DayHeaderView"&
atIndexPath:indexPath];&
[layoutAttributes&addObject:attributes];&
NSArray&*hourHeaderViewIndexPaths&=&[self&indexPathsOfHourHeaderViewsInRect:rect];&
for&(NSIndexPath&*indexPath&in&hourHeaderViewIndexPaths)&{&
UICollectionViewLayoutAttributes&*attributes&=&
[self&layoutAttributesForSupplementaryViewOfKind:@"HourHeaderView"&
atIndexPath:indexPath];&
[layoutAttributes&addObject:attributes];&
return&layoutA&
layoutAttributesFor&IndexPath
有时,collection view会为某个特殊的cell,supplementary或者decoration view向布局对象请求布局属性,而非所有可见的对象。这就是当其他三个方法开始起作用时,你实现的layoutAttributesForItemAtIndexPath:需要创建并返回一个单独的布局属性对象,这样才能正确的格式化传给你的index path所对应的cell。
你可以通过调用 +[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:]这个方法,然后根据index path修改属性。为了得到需要显示在这个index path内的数据,你可能需要访问collection view的数据源。到目前为止,至少确保设置了frame属性,除非你所有的cell都位于彼此上方。
-&(UICollectionViewLayoutAttributes&*)layoutAttributesForItemAtIndexPath:(NSIndexPath&*)indexPath&
CalendarDataSource&*dataSource&=&self.collectionView.dataS&
id&CalendarEvent&&event&=&[dataSource&eventAtIndexPath:indexPath];&
UICollectionViewLayoutAttributes&*attributes&=&
[UICollectionViewLayoutAttributes&layoutAttributesForCellWithIndexPath:indexPath];&
attributes.frame&=&[self&frameForEvent:event];&
如果你正在使用自动布局,你可能会感到惊讶,我们正在直接修改布局参数的frame属性,而不是和约束共事,但这正是UICollectionViewLayout的工作。尽管你可能使用自动布局来定义collection view的frame和它内部每个cell的布局,但cells的frames还是需要通过老式的方法计算出来。
类似的,layoutAttributesForSupplementaryViewOfKind:atIndexPath: 和 layoutAttributesForDecorationViewOfKind:atIndexPath:方法分别需要为supplementary和decoration views做相同的事。只有你的布局包含这样的视图你才需要实现这两个方法。UICollectionViewLayoutAttributes包含另外两个工厂方法,+layoutAttributesForSupplementaryViewOfKind:withIndexPath: 和 +layoutAttributesForDecorationViewOfKind:withIndexPath:,他们是用来创建正确的布局属性对象。
shouldInvalidateLayoutForBoundsChange:
最后,当collection view的bounds改变时,布局需要告诉collection view是否需要重新计算布局。我的猜想是:当collection view改变大小时,大多数布局会被作废,比如设备旋转的时候。因此,一个幼稚的实现可能只会简单的返回YES。虽然实现功能很重要,但是scroll view的bounds在滚动时也会改变,这意味着你的布局每秒会被丢弃多次。根据计算的复杂性判断,这将会对性能产生很大的影响。
当collection view的宽度改变时,我们自定义的布局必须被丢弃,但这滚动并不会影响到布局。幸运的是,collection view将它的新bounds传给shouldInvalidateLayoutForBoundsChange: method。这样我们便能比较视图当前的bounds和新的bounds来确定返回值:
-&(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds&
CGRect&oldBounds&=&self.collectionView.&
if&(CGRectGetWidth(newBounds)&!=&CGRectGetWidth(oldBounds))&{&
return&YES;&
return&NO;&
插入和删除
UITableView中的cell自带了一套非常漂亮的插入和删除动画。但是当为UICollectionView增加和删除cell定义动画功能时,UIKit工程师遇到这样一个问题:如果collection view的布局是完全可变的,那么预先定义好的动画就没办法和开发者自定义的布局很好的融合。他们提出了一个优雅的方法:当一个cell(或者supplementary或者decoration view)被插入到collection view中时,collection view不仅向其布局请求cell正常状态下的布局属性,同时还请求其初始的布局属性,比如,需要在开始有插入动画的cell。collection view会简单的创建一个animation block,并在这个block中,将所有cell的属性从初始(initial)状态改变到常态(normal)。
通过提供不同的初始布局属性,你可以完全自定义插入动画。比如,设置初始的alpha为0将会产生一个淡入的动画。同时设置一个平移和缩放将会产生移动缩放的效果。
同样的原理应用到删除上,这次动画是从常态到一系列你设置的最终布局属性。这些都是你需要在布局类中为initial或final布局参数实现的方法.
initialLayoutAttributesForAppearingItemAtIndexPath:
initialLayoutAttributesForAppearingSupplementaryElementOfKind:atIndexPath:
initialLayoutAttributesForAppearingDecorationElementOfKind:atIndexPath:
finalLayoutAttributesForDisappearingItemAtIndexPath:
finalLayoutAttributesForDisappearingSupplementaryElementOfKind:atIndexPath:
finalLayoutAttributesForDisappearingDecorationElementOfKind:atIndexPath:
布局间切换
可以通过类似的方式将一个collection view布局动态的切换到另外一个布局。当发送一个setCollectionViewLayout:animated:消息时,collection view会为cells在新的布局中查询新的布局参数,然后动态的将每个cell(通过index path在新旧布局中判断出相同的cell)从旧参数变换到新的布局参数。你不需要做任何事情。
根据自定义collection view布局的复杂性,写一个通常很不容易。确切的说,本质上这和从头写一个完整的实现相同布局自定义视图类一样困难了。因为所涉及的计算需要确定哪些子视图当前是可见的,以及他们的位置。尽管如此,使用UICollectionView还是给你带来了一些很好的效果,比如cell重用,自动支持动画,更不要提整洁的独立布局,子视图管理,以及数据提供架构规定(data preparation its architecture prescribes.)。
自定义collection view布局也是向轻量级view controller迈出很好的一步,正如你的view controller不要包含任何布局代码。正如Chris的文章中解释的一样,将这一切和一个独立的datasource类结合在一起,collection view的视图控制器将很难再包含任何代码。
每当我使用UICollectionView的时候,我被其简洁的设计所折服。对于一个有经验的Apple工程师,为了想出如此灵活的类,很可能需要首先考虑NSTableView和UITableView。
阅读(...) 评论()}

我要回帖

更多关于 listview添加头部视图 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信