Swift 5 UICollectionView中cell的对齐方法(重写flowlayout)

前言:之前用Swift 3.0进行实现,现在更新到了Swift 5,添加了多个分组、头部视图、尾部视图、装饰视图的计算,添加了跟随模式,代码没有仔细打磨,大家看看就好啦。

如果觉得文章的描写不够清晰, 可以去看看项目的 Demo

需要对齐cell的原因:

UICollectionView是常用的一个应用于流布局的类, 但是很可惜, 有时候苹果官方提供的方法并不能优美地展示你的UI, 毕竟每个人需要展示的效果可能都不一致. 下面是具体的解析:

首先看官方的对齐方式

创建一个新的项目, 在storyboard的ViewController中拽入UICollectionView定好约束, 然后拽入一个UICollectionViewCell并对Cell进行注册

Cell的代码如下:

class TextCell: UICollectionViewCell {
    
    @IBOutlet weak var textLabel: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        //设置文本标签背景颜色
        textLabel.backgroundColor = .hexString("eeeeee")
        //设置超出的部分隐藏
        textLabel.layer.masksToBounds = true
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        //设置四角的弧度, 设置为高度的一般即是左右为半圆, 效果看下面的效果图
        textLabel.layer.cornerRadius = frame.height/2
    }
}

修改ViewController中的代码

class ViewController: UIViewController {
    
    @IBOutlet weak var collectionView: UICollectionView!
    //文字数据
    var dataSource: [String] = ["made","in","China","UIView","UITableView","UICollectionView"]

    override func viewDidLoad() {
        super.viewDidLoad()
        
        //初始化一个AlignFlowLayout实例
        let flowLayout = UICollectionViewFlowLayout()
        //设置行间距
        flowLayout.minimumLineSpacing = 10
        //设置列间距
        flowLayout.minimumInteritemSpacing = 10
        //设置边界的填充距离
        flowLayout.sectionInset = UIEdgeInsets.init(top: 10, left: 10, bottom: 10, right: 10)
        //给collectionView设置布局属性, 也可以通过init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout)方法来创建一个UICollectionView对象
        collectionView.collectionViewLayout = flowLayout
        //设置代理
        collectionView.delegate = self
        collectionView.dataSource = self
    }
}

由于UICollectionViewDelegateFlowLayout这个协议提供了collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize这个方法, 我们可以实现该方法返回item的size达成cell大小自适应. 下面是协议实现:

//MARK: - UICollectionViewDataSource
//返回对应组中item的个数, Demo中只有一个分组, 所以直接返回个数
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return dataSource.count
}
//返回每个item
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TextCell", for: indexPath) as! TextCell
    //设置cell中展示的文字
    cell.textLabel.text = dataSource[indexPath.row]
    return cell
}
//MARK: - UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    //返回每个item的size
    let text = dataSource[indexPath.row]
    let width = text.width(with: .systemFont(ofSize: 14), size: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 30)) + 30
    return CGSize(width: width, height: 30)
}

下面是现在代码的运行效果:

默认对齐方式, 空白部分拉伸撑满显示区域

标记图如下, 标记的单位是px, 20px相当于代码中的10:

间距标记

通过观察可以发现, 在collectionView第一行显示不下UITableView这个cell, 自动把它移动到第二行,第一行的cell平铺开, 我们一开始设置的列间距minimumInteritemSpacing = 10在第一行变得无效了, 因为在官方代码的计算中sectionInset的优先级比minimumInteritemSpacing高, 实际显示的列间距变成了(collectionView.frame.size.width-(同一行各item的宽度之和)-sectionInset.left-sectionInset.right)/(item的个数-1), 然而这样的显示效果有时候不是我们想要的, 有时候我们就想要minimumInteritemSpacing这个列间距生效所有cell向左看齐, 在我目前的摸索中还没有发现方便快捷的属性可以直接改变系统默认的对齐方式, 希望有这方面知识的朋友可以指点我.

对齐的实现(宽度一致)

我参考很多优秀的开源代码, 给了我很好的思路, 现在我用Swift 3.0 语言来把自己的想法通过比较简单的代码展现出来.

创建一个枚举, 用于区分对齐方向

/// 对齐方向的枚举, 可拓展, 命名可根据自己喜好
enum AlignDirection: Int {
    case start = 0,   //左对齐
         end,         //右对齐  左起显示
         dataEnd,     //右对齐  右起显示
         center,      //中间对齐
         auto         //自动对齐,系统默认效果
}

创建一个类AlignFlowLayout继承于UICollectionViewFlowLayout, 该类内部的代码如下:

AlignDelegateFlowLayout协议

protocol AlignDelegateFlowLayout: UICollectionViewDelegateFlowLayout {
    //代理方法, 返回collectionView内容的尺寸
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, collectionViewContentSize contentSize: CGSize)
}

AlignFlowLayout类创建以下属性

class AlignFlowLayout: UICollectionViewFlowLayout {

    /// 默认自动对齐
    var direction: AlignDirection = .auto
    
    /// 是否靠边对齐
    var isFollow: Bool = false

    /// 所有cell的布局属性
    private var layoutAttributes: [UICollectionViewLayoutAttributes] = []
    
    /// 每一行cell的布局属性
    private var layoutLine: [UICollectionViewLayoutAttributes] = []

    /// 滚动范围
    private var contentSize: CGSize = CGSize.zero

    override var collectionViewContentSize: CGSize {
        return contentSize
    }
}

继承并修改prepare方法

    override func prepare() {
        super.prepare()
        
        // 清空之前的布局属性
        layoutAttributes.removeAll()
        layoutLine.removeAll()
        
        if let collection = collectionView {
            // 获取分组个数
            let sections = collection.numberOfSections
            // 遍历分组
            for i in 0..<sections {
                // 获取组内元素个数
                let rows = collection.numberOfItems(inSection: i)
                // 获取Header的布局属性,‘UICollectionView.elementKindSectionHeader’ 是注册头部视图的字符串,可以替换成自己注册的
                if let layoutAttr = layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, at: IndexPath.init(row: 0, section: i)) {
                    layoutAttributes.append(layoutAttr)
                }
                // 遍历获取组内每个元素的布局属性
                for j in 0..<rows {
                    if let layoutAttr = layoutAttributesForItem(at: IndexPath(row: j, section: i)) {
                        layoutAttributes.append(layoutAttr)
                    }
                }
                // 获取Footer的布局属性,‘UICollectionView.elementKindSectionFooter’ 是注册脚部视图的字符串,可以替换成自己注册的
                if let layoutAttr = layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, at: IndexPath(row: 0, section: i)) {
                    layoutAttributes.append(layoutAttr)
                }
                // 添加装饰视图支持,内容需要自定义
                // MARK: DecorationView Example
//                if let layoutAttr = layoutAttributesForDecorationView(ofKind: kCustomDecorationViewKind, at: IndexPath(row: 0, section: i)) {
//                    layoutAttributes.append(layoutAttr)
//                }
            }
        }
        
        if let collection = self.collectionView {
            var contentWidth: CGFloat = 0, contentHeight: CGFloat = 0
            // 逆向遍历
            for item in layoutAttributes.reversed() {
                // 判断最后一个元素是Footer还是Item, 忽略DecorationView
                if item.representedElementCategory == .cell {
                    contentWidth = item.frame.maxX + sectionInset.right
                    contentHeight = item.frame.maxY + sectionInset.bottom
                    break
                } else if item.representedElementCategory == .supplementaryView {
                    contentWidth = item.frame.maxX
                    contentHeight = item.frame.maxY
                    break
                }
            }
            // 设置内容尺寸
            switch scrollDirection {
            case .horizontal:
                contentSize = CGSize(width: contentWidth, height: collection.bounds.height)
            case .vertical:
                contentSize = CGSize(width: collection.bounds.width, height: contentHeight)
            default:
                contentSize = collection.bounds.size
            }
            
            if let alignDelegate = collection.delegate as? AlignDelegateFlowLayout {
                alignDelegate.collectionView(collection, layout: self, collectionViewContentSize: contentSize)
            }
        }
        
    }

继承并修改layoutAttributesForItem(at indexPath: IndexPath)方法

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        if let attribute = super.layoutAttributesForItem(at: indexPath) {
            // 判断滚动方向
            switch scrollDirection {
            case .vertical:
                // 竖直滚动,判断对齐方向
                switch direction {
                case .start, .end, .center:
                    // 左对齐、右对齐、中间对齐有部分相同的计算步骤
                    if let last = layoutAttributes.last, let collection = self.collectionView {
                        if last.representedElementCategory == .cell {
                            if last.frame.maxX + minimumInteritemSpacing + attribute.frame.width + sectionInset.right < collection.bounds.width {
                                // 同一行显示
                                attribute.ex_x = last.frame.maxX + minimumInteritemSpacing
                            } else {
                                // 下一行显示
                                attribute.ex_x = sectionInset.left
                            }
                            // 判断是否处于贴紧状态
                            if isFollow {
                                // 获取同组的布局属性
                                let filter = layoutAttributes.filter { (item) -> Bool in
                                    return item.indexPath.section == attribute.indexPath.section && item.representedElementCategory == .cell
                                }
                                if filter.isEmpty {
                                    // 如果没有同组布局属性,则现有布局属性为该组的第一个布局属性
                                    attribute.ex_y = last.frame.maxY + self.sectionInset.top
                                } else {
                                    // 如果有同组布局属性,遍历同组cell,获取cell的最小maxY,记录该cell的minX值,获取同组cell的maxX值
                                    var minY: CGFloat = -1, minX: CGFloat = 0, maxX: CGFloat = 0
                                    _ = filter.map({ (item) in
                                        if item.frame.maxY < minY || minY == -1 {
                                            let sameX = filter.filter { (sameItem) -> Bool in
                                                return sameItem != item && sameItem.frame.minX == item.frame.minX && sameItem.frame.maxY > item.frame.maxY
                                            }
                                            if sameX.isEmpty {
                                                minY = item.frame.maxY
                                                minX = item.frame.minX
                                            }
                                        }
                                        if item.frame.maxX > maxX {
                                            maxX = item.frame.maxX
                                        }
                                    })
                                    // 判断直接添加此cell到collectionView的下方是否越界
                                    if maxX + minimumInteritemSpacing + attribute.frame.width + sectionInset.right > collection.bounds.width {
                                        // 越界,采用贴紧坐标
                                        attribute.ex_x = minX
                                        attribute.ex_y = minY + minimumLineSpacing
                                    } else {
                                        // 不越界,右方直接添加
                                        attribute.ex_x = last.frame.maxX + minimumInteritemSpacing
                                        attribute.ex_y = last.frame.minY
                                    }
                                }
                            } else {
                                // 不处于贴紧状态下,切换行时会调整上一列的布局属性
                                if attribute.frame.minX < last.frame.minX {
                                    reloadlayoutAttributes()
                                }
                            }
                        } else if last.representedElementCategory == .supplementaryView {
                            attribute.ex_x = self.sectionInset.left
                            // 如果上一个布局属性是属于头部或者尾部的,当前布局属性需要增加组内填充距离
                            if isFollow {
                                attribute.ex_y = last.frame.maxY + self.sectionInset.top
                            }
                        }
                    } else if isFollow {
                        // 没有上一个布局属性,当前是第一个cell,直接设置x坐标
                        attribute.ex_y = self.sectionInset.top
                    }
                    // 添加进当前行
                    layoutLine.append(attribute)
                    // 判断当前元素是当前组的最后一个元素,重置当前行的布局属性
                    if let items = collectionView?.numberOfItems(inSection: indexPath.section), indexPath.row == items - 1 {
                        reloadlayoutAttributes()
                    }
                case .dataEnd:
                    // 右起显示需要获取collectionView显示区域的宽度
                    if let collection = collectionView {
                        if isFollow {
                            if let last = layoutAttributes.last {
                                if last.representedElementCategory == .cell {
                                    var minY: CGFloat = -1, minX: CGFloat = 0, leftX: CGFloat = -1
                                    _ = layoutAttributes.map { (item) in
                                        if item.frame.maxY < minY || minY == -1 {
                                            let sameX = layoutAttributes.filter { (sameItem) -> Bool in
                                                return sameItem != item && sameItem.frame.minX == item.frame.minX && sameItem.frame.maxY > item.frame.maxY
                                            }
                                            if sameX.isEmpty {
                                                minX = item.frame.minX
                                                minY = item.frame.maxY
                                            }
                                        }
                                        if item.frame.minX < leftX || leftX == -1 {
                                            leftX = item.frame.minX
                                        }
                                    }
                                    if leftX - minimumInteritemSpacing - attribute.frame.width - sectionInset.left < 0 {
                                        // 越界,切换行显示
                                        attribute.ex_x = minX
                                        attribute.ex_y = minY + minimumInteritemSpacing
                                    } else {
                                        // 不越界,左边方直接添加
                                        attribute.ex_x = last.frame.minX - minimumInteritemSpacing - attribute.frame.width
                                        attribute.ex_y = last.frame.minY
                                    }
                                } else if last.representedElementCategory == .supplementaryView {
                                    attribute.ex_x = collection.bounds.width - sectionInset.right - attribute.frame.width
                                    attribute.ex_y = last.frame.maxY + sectionInset.top
                                }
                            } else {
                                attribute.ex_x = collection.bounds.width - sectionInset.right - attribute.frame.width
                                attribute.ex_y = sectionInset.top
                            }
                        } else {
                            if let last = layoutAttributes.last, attribute.frame.minY == last.frame.minY {
                                // 同行显示,向左逐步显示
                                attribute.ex_x = last.frame.minX - minimumInteritemSpacing - attribute.frame.width
                            } else {
                                // 下一行显示
                                attribute.ex_x = collection.bounds.width - sectionInset.right - attribute.frame.width
                            }
                        }
                    }
                default:
                    break
                }
            case .horizontal:
                switch direction {
                case .start, .center, .end:
                    // 获取上一个布局属性
                    if let last = layoutAttributes.last, let collection = self.collectionView {
                        // 判断是不是cell
                        if last.representedElementCategory == .cell {
                            if last.frame.maxY + minimumInteritemSpacing + attribute.frame.height + sectionInset.bottom < collection.bounds.height {
                                // 同一列显示
                                attribute.ex_y = last.frame.maxY + minimumInteritemSpacing
                            } else {
                                // 下一列显示
                                attribute.ex_y = sectionInset.top
                            }
                            // 判断是否处于贴紧状态
                            if isFollow {
                                // 获取同组的布局属性
                                let filter = layoutAttributes.filter { (item) -> Bool in
                                    return item.indexPath.section == attribute.indexPath.section && item.representedElementCategory == .cell
                                }
                                if filter.isEmpty {
                                    // 如果没有同组布局属性,则现有布局属性为该组的第一个布局属性
                                    attribute.ex_x = last.frame.maxX + self.sectionInset.left
                                } else {
                                    // 如果有同组布局属性,遍历同组cell,获取cell的最小maxX,记录该cell的minY值,获取同组cell的maxY值
                                    var minX: CGFloat = -1, minY: CGFloat = 0, maxY: CGFloat = 0
                                    _ = filter.map({ (item) in
                                        if item.frame.maxX < minX || minX == -1 {
                                            let sameY = filter.filter { (sameItem) -> Bool in
                                                return sameItem != item && sameItem.frame.minY == item.frame.minY && sameItem.frame.maxX > item.frame.maxX
                                            }
                                            if sameY.isEmpty {
                                                minX = item.frame.maxX
                                                minY = item.frame.minY
                                            }
                                        }
                                        if item.frame.maxY > maxY {
                                            maxY = item.frame.maxY
                                        }
                                    })
                                    // 判断直接添加此cell到collectionView的下方是否越界
                                    if maxY + minimumInteritemSpacing + attribute.frame.height + sectionInset.bottom > collection.bounds.height {
                                        // 越界,采用贴紧坐标
                                        attribute.ex_x = minX + minimumLineSpacing
                                        attribute.ex_y = minY
                                    } else {
                                        // 不越界,下方直接添加
                                        attribute.ex_x = last.frame.minX
                                        attribute.ex_y = last.frame.maxY + minimumInteritemSpacing
                                    }
                                }
                            } else {
                                // 不处于贴紧状态下,切换列时会调整上一列的布局属性
                                if attribute.frame.minX <= last.frame.minX {
                                    reloadlayoutAttributes()
                                }
                            }
                        } else if last.representedElementCategory == .supplementaryView {
                            attribute.ex_y = self.sectionInset.top
                            // 如果上一个布局属性是属于头部或者尾部的,当前布局属性需要增加组内填充距离
                            if isFollow {
                                attribute.ex_x = last.frame.maxX + self.sectionInset.left
                            }
                        }
                    } else if isFollow {
                        // 没有上一个布局属性,当前是第一个cell,直接设置x坐标
                        attribute.ex_x = self.sectionInset.left
                    }
                    // 添加进当前列
                    layoutLine.append(attribute)
                    // 判断当前元素是当前组的最后一个元素,重置当前列的布局属性
                    if let items = collectionView?.numberOfItems(inSection: indexPath.section), indexPath.row == items - 1 {
                        reloadlayoutAttributes()
                    }
                case .dataEnd:
                    // 下起显示需要获取collectionView显示区域的高度
                    if let collection = collectionView {
                        if isFollow {
                            if let last = layoutAttributes.last {
                                if last.representedElementCategory == .cell {
                                    var minX: CGFloat = -1, minY: CGFloat = 0, topY: CGFloat = -1
                                    _ = layoutAttributes.map { (item) in
                                        if item.frame.maxX < minX || minX == -1 {
                                            let sameY = layoutAttributes.filter { (sameItem) -> Bool in
                                                return sameItem != item && sameItem.frame.minY == item.frame.minY && sameItem.frame.maxX > item.frame.maxX
                                            }
                                            if sameY.isEmpty {
                                                minX = item.frame.maxX
                                                minY = item.frame.minY
                                            }
                                        }
                                        if item.frame.minY < topY || topY == -1 {
                                            topY = item.frame.minY
                                        }
                                    }
                                    if topY - minimumInteritemSpacing - attribute.frame.height - sectionInset.top < 0 {
                                        // 越界,切换列显示
                                        attribute.ex_x = minX + minimumLineSpacing
                                        attribute.ex_y = minY
                                    } else {
                                        // 不越界,上方直接添加
                                        attribute.ex_x = last.frame.minX
                                        attribute.ex_y = last.frame.minY - minimumInteritemSpacing - attribute.frame.height
                                    }
                                } else if last.representedElementCategory == .supplementaryView {
                                    attribute.ex_x = last.frame.maxX + sectionInset.left
                                    attribute.ex_y = collection.bounds.height - sectionInset.bottom - attribute.frame.height
                                }
                            } else {
                                attribute.ex_x = sectionInset.left
                                attribute.ex_y = collection.bounds.height - sectionInset.bottom - attribute.frame.height
                            }
                        } else {
                            
                            if let last = layoutAttributes.last, last.representedElementCategory == .cell, attribute.frame.minX < last.frame.maxX {
                                // 同列显示,向上逐步显示
                                attribute.ex_y = last.frame.minY - minimumInteritemSpacing - attribute.frame.height
                            } else {
                                // 下一行显示
                                attribute.ex_y = collection.bounds.height - sectionInset.bottom - attribute.frame.height
                            }
                        }
                    }
                default:
                    break
                }
            default:
                break
            }
            // 返回新的布局属性
            return attribute
        }
        // 默认布局,返回原始布局属性
        return super.layoutAttributesForItem(at: indexPath)
    }

继承并修改layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath)方法

    override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        // 获取头部或尾部视图的布局属性
        let attribute = super.layoutAttributesForSupplementaryView(ofKind: elementKind, at: indexPath)
        if elementKind == UICollectionView.elementKindSectionHeader {
            //TODO: 第一组的头部暂时用不到修改属性,有需要可以自己修改
            if indexPath.section > 0 {
                var max: CGFloat = 0
                // 获取上一组的布局属性
                let filter = layoutAttributes.filter { (item) -> Bool in
                    return item.indexPath.section == indexPath.section - 1
                }
                // 判断上一组布局属性是否为空,如果为空暂时不需要修改
                if !filter.isEmpty {
                    // 如果上一组的布局属性不为空,获取新坐标
                    if scrollDirection == .horizontal {
                        _ = filter.map({ (item) in
                            if item.frame.maxX > max {
                                max = item.frame.maxX
                            }
                        })
                        attribute?.ex_x = max
                    } else if scrollDirection == .vertical {
                        _ = filter.map({ (item) in
                            if item.frame.maxY > max {
                                max = item.frame.maxY
                            }
                        })
                        attribute?.ex_y = max
                    }
                    return attribute
                }
            }
        } else if elementKind == UICollectionView.elementKindSectionFooter {
            var max: CGFloat = 0
            // 获取同一组cell的布局属性
            let filter = layoutAttributes.filter { (item) -> Bool in
                return item.indexPath.section == indexPath.section && item.representedElementCategory == .cell
            }
            if !filter.isEmpty {
                // 根据同一组cell的边距来修改footer的位置
                if scrollDirection == .horizontal {
                    _ = filter.map({ (item) in
                        if item.frame.maxX > max {
                            max = item.frame.maxX
                        }
                    })
                    attribute?.ex_x = max + sectionInset.right
                } else if scrollDirection == .vertical {
                    _ = filter.map({ (item) in
                        if item.frame.maxY > max {
                            max = item.frame.maxY
                        }
                    })
                    attribute?.ex_y = max + sectionInset.bottom
                }
                return attribute
            }
        }
        return attribute
    }

继承并修改layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath)方法

override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let attribute = UICollectionViewLayoutAttributes(forDecorationViewOfKind: elementKind, with: indexPath)
        let filter = layoutAttributes.filter { (item) -> Bool in
            return item.representedElementCategory == .cell && item.indexPath.section == indexPath.section
        }
        
        if !filter.isEmpty {
            var minX: CGFloat = -1, maxX: CGFloat = 0, minY: CGFloat = -1, maxY: CGFloat = 0
            _ = filter.map({ (item) in
                if item.frame.minX < minX || minX < 0 {
                    minX = item.frame.minX
                }
                if item.frame.maxX > maxX {
                    maxX = item.frame.maxX
                }
                if item.frame.minY < minY || minY < 0 {
                    minY = item.frame.minY
                }
                if item.frame.maxY > maxY {
                    maxY = item.frame.maxY
                }
            })
            
            attribute.frame = CGRect(x: minX - 5, y: minY - 5, width: maxX - minX + 10, height: maxY - minY + 10)
            attribute.zIndex = -1
        }
        
        return attribute
    }

添加reloadlayoutAttributes方法

func reloadlayoutAttributes() {
        if layoutLine.count == 0 {return} //防止越界
        if direction == .end || direction == .center, let collection = collectionView {
            //计算填充比例, rightFlow为1, center为0.5
            let scale: CGFloat = direction == .end ? 1 : 0.5
            
            if isFollow, let first = layoutLine.first {
                switch scrollDirection {
                case .vertical:
                    let firstArr = layoutLine.filter { (item) -> Bool in
                        return item.frame.minY == first.frame.minY
                    }
                    var width = CGFloat(firstArr.count - 1) * minimumInteritemSpacing
                    for item in firstArr {
                        width += item.frame.width
                    }
                    let space = (collection.bounds.width - width - sectionInset.left - sectionInset.right) * scale
                    for item in firstArr {
                        let sameArr = layoutLine.filter { (sameItem) -> Bool in
                            return sameItem.frame.minX == item.frame.minX
                        }
                        for sameItem in sameArr {
                            sameItem.ex_x += space
                        }
                    }
                case .horizontal:
                    let firstArr = layoutLine.filter { (item) -> Bool in
                        return item.frame.minX == first.frame.minX
                    }
                    var height = CGFloat(firstArr.count - 1) * minimumInteritemSpacing
                    for item in firstArr {
                        height += item.frame.height
                    }
                    let space = (collection.bounds.height - height - sectionInset.top - sectionInset.bottom) * scale
                    for item in firstArr {
                        let sameArr = layoutLine.filter { (sameItem) -> Bool in
                            return sameItem.frame.minY == item.frame.minY
                        }
                        for sameItem in sameArr {
                            sameItem.ex_y += space
                        }
                    }
                default:
                    break
                }
            } else {
                if let last = layoutLine.last {
                    //重新绘制布局有右对齐和居中对齐两种
                    switch scrollDirection {
                    case .vertical:
                        let space = (collection.bounds.width - last.frame.maxX - sectionInset.right) * scale
                        for layout in layoutLine {
                            layout.ex_x += space
                        }
                    case .horizontal:
                        let space = (collection.bounds.height - last.frame.maxY - sectionInset.bottom) * scale
                        for layout in layoutLine {
                            layout.ex_y += space
                        }
                    default:
                        break
                    }
                }
            }
        }

        layoutLine.removeAll()
    }

继承并修改layoutAttributesForElements(in rect:CGRect)方法

    override func layoutAttributesForElements(in rect:CGRect) -> [UICollectionViewLayoutAttributes]{
        return layoutAttributes
    }

添加拓展

extension UICollectionViewLayoutAttributes {
    var ex_x: CGFloat {
        set {
            var newFrame = frame
            newFrame.origin.x = newValue
            frame = newFrame
        } get {
            return frame.origin.x
        }
    }
    
    var ex_y: CGFloat {
        set {
            var newFrame = frame
            newFrame.origin.y = newValue
            frame = newFrame
        } get {
            return frame.origin.y
        }
    }
}

1. 左对齐

上面的代码中就是左对齐的实现, 调用的代码如下:

//初始化一个AlignFlowLayout实例
let flowLayout = AlignFlowLayout()
//设置方向
flowLayout.direction = .start
//设置行间距
flowLayout.minimumLineSpacing = 10
//设置列间距
flowLayout.minimumInteritemSpacing = 10
//设置边界的填充距离
flowLayout.sectionInset = UIEdgeInsets.init(top: 10, left: 10, bottom: 10, right: 10)
//注册DecorationView
flowLayout.register(CustomDecorationView.self, forDecorationViewOfKind: kCustomDecorationViewKind)
        
//给collectionView设置布局属性, 也可以通过init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout)方法来创建一个UICollectionView对象
collectionView.collectionViewLayout = flowLayout

实质上就是简单地把UICollectionViewFlowLayout替换成我们自定义的AlignFlowLayout, 运行效果如下:

左对齐

2. 右对齐

右对齐有两种显示方式, 一种是数据源从右到左显示, 另一种是数据源从左到右显示但是UI向右靠拢,因为前面设置了对齐方式是左对齐, 所以现在要修改对齐方式:

flowLayout.direction = .end

运行效果如下:


右对齐

第二种方式调用是一样的, 修改对齐方式即可:

flowLayout.direction = .dataEnd

下面是效果:

数据右起对齐

3. 居中对齐

计算思路可以简单地分两种:

1.先用.left方式计算出每行所有item的位置, 然后将右边空白部分的一半宽度填充到左边
2.先用.dataEnd方式计算出每行所有item的位置, 然后将左边空白部分的一半填充到右边

按照计算步骤来考虑, 第一种方法比较好

最终效果如下:

居中对齐

4. 2021年新增内容

多个分组
多个分组
装饰视图
装饰视图
跟随模式

有时候,使用collectionView会用到不规则的矩形,如果不进行适配会如下显示:


不规则矩形

应用跟随模式

flowLayout.isFollow = true

效果如下:


不规则矩形(跟随模式)
水平方向滚动
水平方向滚动

注意:

  1. 尽量不要让item的宽度与sectionInset之和超过collectionView的实际宽度, 否则可能会导致显示出错.
  2. 跟随模式只适用于竖直滚动模式下宽度一致高度不一致和水平模式滚动下高度一致而宽度不一致的情况,其他情况请关闭
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容