与桌面、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(组件类) ---
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 | 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(布局类) ---
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155 | 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(测试类) ---
123456789101112131415161718192021222324252627282930 | 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() } } |
5,源码下载: hangge_1081.zip