与桌面、Web应用不同,受限于屏幕尺寸,移动APP常常采用单列表格来显示列表数据。但有时我们需要使用多列表格来展示数据(比如:报表数据显示,或iPad这种大屏设备上展示多栏数据),这些通过网格(UICollectionView)的自定义布局功能就可以实现。
1,多列表格(multi-column table control)效果图
2,功能说明:
(1)表格列头的标题文字加粗,内容区域的文字正常
(2)表格边框为1像素黑色边框
(3)第一列文字居左,其余列文字居中显示(居左的文字离左侧还是有5个像素距离)
(4)每列单元格宽度不是平均分配的。而是从右往左,根据表头文字计算当前列的宽度。剩下的空间就都分配给第一列。
(5)整个组件内部设置了 contentInset,给左右两侧各设置了10像素的距离。这样组件外部设置100%宽时,左右边框也不会顶到屏幕边缘。同时如果有滚动条的时候,滚动条也不会盖在表格内容区域上方。
(6)点击单元格控制台会打印出对应的坐标位置。
3,关于collection view重新计算布局时机
(1)shouldInvalidateLayoutForBoundsChange() 方法返回 true,表示当 collection view 的 bounds 改变时,就要重新计算布局。
(2)除了collection view 改变尺寸大小时 bounds 会改变, scroll view 的 bounds 在滚动时也会改变。
(3)本例中,collection view 在滚动的情况下没必要计算更新布局,否则拖动滚动条的时候布局会不断地丢弃重新计算,影响性能。
(4)这里在 shouldInvalidateLayoutForBoundsChange() 中做判断,只有 collection view 宽度变化时才返回true重新计算布局,否则返回false。
4,项目代码
--- UICollectionGridViewController.swift(组件类) ---
import Foundation
import UIKit
//多列表格组件(通过CollectionView实现)
class UICollectionGridViewController: UICollectionViewController {
//表头数据
var cols: [String]! = []
//行数据
var rows: [[AnyObject]]! = []
//单元格内容居左时的左侧内边距
private var cellPaddingLeft:CGFloat = 5
init() {
//初始化表格布局
let layout = UICollectionGridViewLayout()
super.init(collectionViewLayout: layout)
layout.viewController = self
collectionView!.backgroundColor = UIColor.whiteColor()
collectionView!.registerClass(UICollectionViewCell.self,
forCellWithReuseIdentifier: "cell")
collectionView!.delegate = self
collectionView!.dataSource = self
collectionView!.directionalLockEnabled = true
collectionView!.contentInset = UIEdgeInsetsMake(0, 10, 0, 10)
collectionView!.bounces = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("UICollectionGridViewController.init(coder:) has not been implemented")
}
//设置列头数据
func setColumns(columns: [String]) {
cols = columns
}
//添加行数据
func addRow(row: [AnyObject]) {
rows.append(row)
collectionView!.collectionViewLayout.invalidateLayout()
collectionView!.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidLayoutSubviews() {
collectionView!.frame = CGRectMake(0, 0, view.frame.width, view.frame.height)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
//返回表格总行数
override func numberOfSectionsInCollectionView(collectionView: UICollectionView)
-> Int {
if cols.isEmpty {
return 0
}
//总行数是:记录数+1个表头
return rows.count + 1
}
//返回表格的列数
override func collectionView(collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return cols.count
}
//单元格内容创建
override func collectionView(collectionView: UICollectionView,
cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell",
forIndexPath: indexPath) as UICollectionViewCell
//单元格边框
cell.layer.borderWidth = 1
cell.backgroundColor = UIColor.whiteColor()
cell.clipsToBounds = true
//先清空内部原有的元素
for subview in cell.subviews {
subview.removeFromSuperview()
}
//添加内容标签
let label = UILabel(frame: CGRectMake(0, 0, cell.frame.width,
cell.frame.height))
//第一列的内容左对齐,其它列内容居中
if indexPath.row != 0 {
label.textAlignment = NSTextAlignment.Center
}else {
label.textAlignment = NSTextAlignment.Left
label.frame.origin.x = cellPaddingLeft
}
//设置列头单元格,内容单元格的数据
if indexPath.section == 0 {
let text = NSAttributedString(string: cols[indexPath.row], attributes: [
NSFontAttributeName:UIFont.boldSystemFontOfSize(15)
])
label.attributedText = text
} else {
label.font = UIFont.systemFontOfSize(15)
label.text = "\(rows[indexPath.section-1][indexPath.row])"
}
cell.addSubview(label)
return cell
}
//单元格选中事件
override func collectionView(collectionView: UICollectionView,
didSelectItemAtIndexPath indexPath: NSIndexPath) {
//打印出点击单元格的[行,列]坐标
print("点击单元格的[行,列]坐标: [\(indexPath.section),\(indexPath.row)]")
}
}
--- UICollectionGridViewLayout.swift(布局类) ---
import Foundation
import UIKit
//多列表格组件布局类
class UICollectionGridViewLayout: UICollectionViewLayout {
//记录每个单元格的布局属性
private var itemAttributes: [[UICollectionViewLayoutAttributes]] = []
private var itemsSize: [NSValue] = []
private var contentSize: CGSize = CGSizeZero
//表格组件视图控制器
var viewController: UICollectionGridViewController!
//准备所有view的layoutAttribute信息
override func prepareLayout() {
if collectionView!.numberOfSections() == 0 {
return
}
var column = 0
var xOffset: CGFloat = 0
var yOffset: CGFloat = 0
var contentWidth: CGFloat = 0
var contentHeight: CGFloat = 0
if itemAttributes.count > 0 {
return
}
itemAttributes = []
itemsSize = []
if itemsSize.count != viewController.cols.count {
calculateItemsSize()
}
for var section = 0; section < collectionView?.numberOfSections(); section++ {
var sectionAttributes: [UICollectionViewLayoutAttributes] = []
for var index = 0; index < viewController.cols.count; index++ {
let itemSize = itemsSize[index].CGSizeValue()
let indexPath = NSIndexPath(forItem: index, inSection: section)
let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath:
indexPath)
//除第一列,其它列位置都左移一个像素,防止左右单元格间显示两条边框线
if index == 0{
attributes.frame = CGRectIntegral(CGRectMake(xOffset, yOffset,
itemSize.width, itemSize.height))
}else {
attributes.frame = CGRectIntegral(CGRectMake(xOffset-1, yOffset,
itemSize.width+1, itemSize.height))
}
sectionAttributes.append(attributes)
xOffset = xOffset+itemSize.width
column++
if column == viewController.cols.count {
if xOffset > contentWidth {
contentWidth = xOffset
}
column = 0
xOffset = 0
yOffset += itemSize.height
}
}
itemAttributes.append(sectionAttributes)
}
let attributes = itemAttributes.last!.last! as UICollectionViewLayoutAttributes
contentHeight = attributes.frame.origin.y + attributes.frame.size.height
contentSize = CGSizeMake(contentWidth, contentHeight)
}
//需要更新layout时调用
override func invalidateLayout() {
itemAttributes = []
itemsSize = []
contentSize = CGSizeZero
super.invalidateLayout()
}
// 返回内容区域总大小,不是可见区域
override func collectionViewContentSize() -> CGSize {
return contentSize
}
// 这个方法返回每个单元格的位置和大小
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath)
-> UICollectionViewLayoutAttributes? {
return itemAttributes[indexPath.section][indexPath.row]
}
// 返回所有单元格位置属性
override func layoutAttributesForElementsInRect(rect: CGRect)
-> [UICollectionViewLayoutAttributes]? {
var attributes: [UICollectionViewLayoutAttributes] = []
for section in itemAttributes {
attributes.appendContentsOf(section.filter(
{(includeElement: UICollectionViewLayoutAttributes) -> Bool in
return CGRectIntersectsRect(rect, includeElement.frame)
}))
}
return attributes
}
//当边界发生改变时,是否应该刷新布局。
//本例在宽度变化时,将重新计算需要的布局信息。
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
let oldBounds = self.collectionView?.bounds
if CGRectGetWidth(oldBounds!) != CGRectGetWidth(newBounds) {
return true
}else {
return false
}
}
//计算所有单元格的尺寸(每一列各一个单元格)
func calculateItemsSize() {
var remainingWidth = collectionView!.frame.width -
collectionView!.contentInset.left - collectionView!.contentInset.right
for var index = viewController.cols.count-1; index >= 0; index-- {
let newItemSize = sizeForItemWithColumnIndex(index,
remainingWidth: remainingWidth)
remainingWidth -= newItemSize.width
let newItemSizeValue = NSValue(CGSize: newItemSize)
//由于遍历列的时候是从尾部开始遍历了,因此将结果插入数组的时候都是放人第一个位置
itemsSize.insert(newItemSizeValue, atIndex: 0)
}
}
//计算某一列的单元格尺寸
func sizeForItemWithColumnIndex(columnIndex: Int, remainingWidth: CGFloat) -> CGSize {
let columnString = viewController.cols[columnIndex]
//根据列头标题文件,估算各列的宽度
let size = NSString(string: columnString).sizeWithAttributes([
NSFontAttributeName:UIFont.systemFontOfSize(15),
NSUnderlineStyleAttributeName:NSUnderlineStyle.StyleSingle.rawValue
])
//如果有剩余的空间则都给第一列
if columnIndex == 0 {
return CGSizeMake(max(remainingWidth, size.width + 17), size.height + 10)
}
//行高增加10像素,列宽增加17像素
return CGSizeMake(size.width + 17, size.height + 10)
}
}
--- ViewController.swift(测试类) ---
import UIKit
class ViewController: UIViewController {
var gridViewController: UICollectionGridViewController!
override func viewDidLoad() {
super.viewDidLoad()
gridViewController = UICollectionGridViewController()
gridViewController.setColumns(["客户", "消费金额", "消费次数", "满意度"])
gridViewController.addRow(["hangge", "100", "8", "60%"])
gridViewController.addRow(["张三", "223", "16", "81%"])
gridViewController.addRow(["李四", "143", "25", "93%"])
gridViewController.addRow(["王五", "75", "2", "53%"])
gridViewController.addRow(["韩梅梅", "43", "12", "33%"])
gridViewController.addRow(["李雷", "33", "27", "45%"])
gridViewController.addRow(["王大力", "33", "22", "15%"])
view.addSubview(gridViewController.view)
}
override func viewDidLayoutSubviews() {
gridViewController.view.frame = CGRectMake(0, 50, view.frame.width,
view.frame.height-60)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}