(本文代码已升级至Swift3)
与桌面、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)
shouldInvalidateLayout()
方法返回
true,表示当
collection view 的
bounds 改变时,就要重新计算布局。
(2)除了 collection view 改变尺寸大小时 bounds 会改变, scroll view 的 bounds 在滚动时也会改变。
(3)本例中, collection view 在滚动的情况下没必要计算更新布局,否则拖动滚动条的时候布局会不断地丢弃重新计算,影响性能。
(4)这里在
shouldInvalidateLayout()
中做判断,只有
collection view 宽度变化时才返回
true重新计算布局,否则返回
false。
4,项目代码
--- UICollectionGridViewController.swift(组件类) ---
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
|
import
Foundation
import
UIKit
//多列表格组件(通过CollectionView实现)
class
UICollectionGridViewController
:
UICollectionViewController
{
//表头数据
var
cols: [
String
]! = []
//行数据
var
rows: [[
Any
]]! = []
//单元格内容居左时的左侧内边距
private
var
cellPaddingLeft:
CGFloat
= 5
init
() {
//初始化表格布局
let
layout =
UICollectionGridViewLayout
()
super
.
init
(collectionViewLayout: layout)
layout.viewController =
self
collectionView!.backgroundColor =
UIColor
.white
collectionView!.register(
UICollectionViewCell
.
self
,
forCellWithReuseIdentifier:
"cell"
)
collectionView!.delegate =
self
collectionView!.dataSource =
self
collectionView!.isDirectionalLockEnabled =
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: [
Any
]) {
rows.append(row)
collectionView!.collectionViewLayout.invalidateLayout()
collectionView!.reloadData()
}
override
func
viewDidLoad() {
super
.viewDidLoad()
}
override
func
viewDidLayoutSubviews() {
collectionView!.frame =
CGRect
(x:0, y:0,
width:view.frame.width, height:view.frame.height)
}
override
func
didReceiveMemoryWarning() {
super
.didReceiveMemoryWarning()
}
//返回表格总行数
override
func
numberOfSections(
in
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
,
cellForItemAt indexPath:
IndexPath
) ->
UICollectionViewCell
{
let
cell = collectionView.dequeueReusableCell(withReuseIdentifier:
"cell"
,
for
: indexPath)
as
UICollectionViewCell
//单元格边框
cell.layer.borderWidth = 1
cell.backgroundColor =
UIColor
.white
cell.clipsToBounds =
true
//先清空内部原有的元素
for
subview
in
cell.subviews {
subview.removeFromSuperview()
}
//添加内容标签
let
label =
UILabel
(frame:
CGRect
(x:0, y:0, width:cell.frame.width,
height:cell.frame.height))
//第一列的内容左对齐,其它列内容居中
if
indexPath.row != 0 {
label.textAlignment = .center
}
else
{
label.textAlignment = .left
label.frame.origin.x = cellPaddingLeft
}
//设置列头单元格,内容单元格的数据
if
indexPath.section == 0 {
let
text =
NSAttributedString
(string: cols[indexPath.row], attributes: [
NSFontAttributeName
:
UIFont
.boldSystemFont(ofSize: 15)
])
label.attributedText = text
}
else
{
label.font =
UIFont
.systemFont(ofSize: 15)
label.text =
"\(rows[indexPath.section-1][indexPath.row])"
}
cell.addSubview(label)
return
cell
}
//单元格选中事件
override
func
collectionView(_ collectionView:
UICollectionView
,
didSelectItemAt indexPath:
IndexPath
) {
//打印出点击单元格的[行,列]坐标
print
(
"点击单元格的[行,列]坐标: [\(indexPath.section),\(indexPath.row)]"
)
}
}
|
--- UICollectionGridViewLayout.swift(布局类) ---
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
|
import
Foundation
import
UIKit
//多列表格组件布局类
class
UICollectionGridViewLayout
:
UICollectionViewLayout
{
//记录每个单元格的布局属性
private
var
itemAttributes: [[
UICollectionViewLayoutAttributes
]] = []
private
var
itemsSize: [
NSValue
] = []
private
var
contentSize:
CGSize
=
CGSize
.zero
//表格组件视图控制器
var
viewController:
UICollectionGridViewController
!
//准备所有view的layoutAttribute信息
override
func
prepare() {
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
section
in
0 ..< (collectionView?.numberOfSections)! {
var
sectionAttributes: [
UICollectionViewLayoutAttributes
] = []
for
index
in
0 ..< viewController.cols.count {
let
itemSize = itemsSize[index].cgSizeValue
let
indexPath =
IndexPath
(item: index, section: section)
let
attributes =
UICollectionViewLayoutAttributes
(forCellWith: indexPath)
//除第一列,其它列位置都左移一个像素,防止左右单元格间显示两条边框线
if
index == 0{
attributes.frame =
CGRect
(x:xOffset, y:yOffset, width:itemSize.width,
height:itemSize.height).integral
}
else
{
attributes.frame =
CGRect
(x:xOffset-1, y:yOffset,
width:itemSize.width+1,
height:itemSize.height).integral
}
sectionAttributes.append(attributes)
xOffset = xOffset+itemSize.width
column += 1
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 =
CGSize
(width:contentWidth, height:contentHeight)
}
//需要更新layout时调用
override
func
invalidateLayout() {
itemAttributes = []
itemsSize = []
contentSize =
CGSize
.zero
super
.invalidateLayout()
}
// 返回内容区域总大小,不是可见区域
override
var
collectionViewContentSize:
CGSize
{
get
{
return
contentSize
}
}
// 这个方法返回每个单元格的位置和大小
override
func
layoutAttributesForItem(at indexPath:
IndexPath
)
->
UICollectionViewLayoutAttributes
? {
return
itemAttributes[indexPath.section][indexPath.row]
}
// 返回所有单元格位置属性
override
func
layoutAttributesForElements(
in
rect:
CGRect
)
-> [
UICollectionViewLayoutAttributes
]? {
var
attributes: [
UICollectionViewLayoutAttributes
] = []
for
section
in
itemAttributes {
attributes.append(contentsOf: section.
filter
(
{(includeElement:
UICollectionViewLayoutAttributes
) ->
Bool
in
return
rect.intersects(includeElement.frame)
}))
}
return
attributes
}
//当边界发生改变时,是否应该刷新布局。
//本例在宽度变化时,将重新计算需要的布局信息。
override
func
shouldInvalidateLayout(forBoundsChange newBounds:
CGRect
) ->
Bool
{
let
oldBounds =
self
.collectionView?.bounds
if
oldBounds!.width != newBounds.width {
return
true
}
else
{
return
false
}
}
//计算所有单元格的尺寸(每一列各一个单元格)
func
calculateItemsSize() {
var
remainingWidth = collectionView!.frame.width -
collectionView!.contentInset.left - collectionView!.contentInset.right
var
index = viewController.cols.count-1
while
index >= 0 {
let
newItemSize = sizeForItemWithColumnIndex(columnIndex: index,
remainingWidth: remainingWidth)
remainingWidth -= newItemSize.width
let
newItemSizeValue =
NSValue
(cgSize: newItemSize)
//由于遍历列的时候是从尾部开始遍历了,因此将结果插入数组的时候都是放人第一个位置
itemsSize.insert(newItemSizeValue, at: 0)
index -= 1
}
}
//计算某一列的单元格尺寸
func
sizeForItemWithColumnIndex(columnIndex:
Int
, remainingWidth:
CGFloat
) ->
CGSize
{
let
columnString = viewController.cols[columnIndex]
//根据列头标题文件,估算各列的宽度
let
size =
NSString
(string: columnString).size(attributes: [
NSFontAttributeName
:
UIFont
.systemFont(ofSize: 15),
NSUnderlineStyleAttributeName
:
NSUnderlineStyle
.styleSingle.rawValue
])
//如果有剩余的空间则都给第一列
if
columnIndex == 0 {
return
CGSize
(width:
max
(remainingWidth, size.width + 17),
height:size.height + 10)
}
//行高增加10像素,列宽增加17像素
return
CGSize
(width:size.width + 17, height:size.height + 10)
}
}
|
--- ViewController.swift(测试类) ---
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import
UIKit
class
ViewController
:
UIViewController
{
var
gridViewController:
UICollectionGridViewController
!
override
func
viewDidLoad() {
super
.viewDidLoad()
gridViewController =
UICollectionGridViewController
()
gridViewController.setColumns(columns: [
"客户"
,
"消费金额"
,
"消费次数"
,
"满意度"
])
gridViewController.addRow(row: [
"hangge"
,
"100"
,
"8"
,
"60%"
])
gridViewController.addRow(row: [
"张三"
,
"223"
,
"16"
,
"81%"
])
gridViewController.addRow(row: [
"李四"
,
"143"
,
"25"
,
"93%"
])
gridViewController.addRow(row: [
"王五"
,
"75"
,
"2"
,
"53%"
])
gridViewController.addRow(row: [
"韩梅梅"
,
"43"
,
"12"
,
"33%"
])
gridViewController.addRow(row: [
"李雷"
,
"33"
,
"27"
,
"45%"
])
gridViewController.addRow(row: [
"王大力"
,
"33"
,
"22"
,
"15%"
])
view.addSubview(gridViewController.view)
}
override
func
viewDidLayoutSubviews() {
gridViewController.view.frame =
CGRect
(x:0, y:50, width:view.frame.width,
height:view.frame.height-60)
}
override
func
didReceiveMemoryWarning() {
super
.didReceiveMemoryWarning()
}
}
|
