返回 导航

Swift

hangge.com

Swift - 多列表格组件的实现(样例1:基本功能的实现)

作者:hangge | 2016-03-15 09:30
相关文章系列:
[当前文章] Swift - 多列表格组件的实现(样例1:基本功能的实现)
Swift - 多列表格组件的实现(样例2:带排序功能)
Swift - 多列表格组件的实现(样例3:表头、列头固定)
Swift - 多列表格组件的实现(样例4:表格样式美化)

(本文代码已升级至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 viewbounds 在滚动时也会改变。
(3)本例中,collection view 在滚动的情况下没必要计算更新布局,否则拖动滚动条的时候布局会不断地丢弃重新计算,影响性能。 
(4)这里在 shouldInvalidateLayout() 中做判断,只有 collection view 宽度变化时才返回true重新计算布局,否则返回false

4,项目代码
--- UICollectionGridViewController.swift(组件类) ---
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(UICollectionGridViewCell.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! UICollectionGridViewCell
        //第一列的内容左对齐,其它列内容居中
        if indexPath.row == 0 {
            cell.label.textAlignment = .left
            cell.paddingLeft = cellPaddingLeft
        }
        
        //设置列头单元格,内容单元格的数据
        if indexPath.section == 0 {
            let text = NSAttributedString(string: cols[indexPath.row], attributes: [
                NSFontAttributeName:UIFont.boldSystemFont(ofSize: 15)
                ])
            cell.label.attributedText = text
        } else {
            cell.label.font = UIFont.systemFont(ofSize: 15)
            cell.label.text = "\(rows[indexPath.section-1][indexPath.row])"
        }
        
        return cell
    }
    
    //单元格选中事件
    override func collectionView(_ collectionView: UICollectionView,
                                 didSelectItemAt indexPath: IndexPath) {
        //打印出点击单元格的[行,列]坐标
        print("点击单元格的[行,列]坐标: [\(indexPath.section),\(indexPath.row)]")
    }
}

--- UICollectionGridViewCell.swift(组件单元格类) ---
import UIKit

class UICollectionGridViewCell: UICollectionViewCell {
    
    //内容标签
    var label:UILabel!
    
    //标签做边距
    var paddingLeft:CGFloat = 0
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        //单元格边框
        self.layer.borderWidth = 1
        self.backgroundColor = UIColor.white
        self.clipsToBounds = true
        
        //添加内容标签
        self.label = UILabel(frame: .zero)
        self.label.textAlignment = .center
        self.addSubview(self.label)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        label.frame = CGRect(x: paddingLeft, y: 0,
                             width: frame.width - paddingLeft,
                             height: frame.height)
    }
}

--- UICollectionGridViewLayout.swift(布局类) ---
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(测试类) ---
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

样式修改1:每列宽度相等、内容居中

1,效果图
上面的样例中,除第一列外其他列先根据对应表头标题的文字长度来确定宽度,然后将剩下的空间全部分给第一列。这里对样式做个调整。
(1)表格中每一列的宽度都是固定的。
(2)同时无论屏幕如何旋转,单元格内的文字都是居中显示。
         

2,修改的地方
(1)修改了 UICollectionGridViewLayout 类里的 sizeForItemWithColumnIndex 方法,让返回的每个单元格宽度都是一样的。
(2)修改了 UICollectionGridViewController 类里的 cellForItemAt 方法,让每个单元格内容都是居中显示。

3,源码下载hangge_1081.zip

样式修改2:每列设置个最小宽度

前面的样例中我们都是让所有列能在同一屏下全部显示出来,如果表格列数过多,在竖屏下就会显得比较拥挤。

可以修改 UICollectionGridViewLayout 类里的 sizeForItemWithColumnIndex 方法,将列宽限定个最小值。
//计算某一列的单元格尺寸
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
        ])
    
    //修改成所有列都平均分配(但宽度不能小于80)
    let width = max(remainingWidth/CGFloat(columnIndex+1), 80)
    //计算好的宽度还要取整,避免偏移
    return CGSize(width: ceil(width), height:size.height + 10)
}
这里我将最小值设为80,超出屏幕往外的区域可以通过左右拖动查看。
         
评论

全部评论(8)

回到顶部