Swift 5 UICollectionView中cell的對齊方法(重寫flowlayout)

前言:之前用Swift 3.0進(jìn)行實(shí)現(xiàn)兵罢,現(xiàn)在更新到了Swift 5族壳,添加了多個分組这橙、頭部視圖补履、尾部視圖日丹、裝飾視圖的計算鲤桥,添加了跟隨模式坏平,代碼沒有仔細(xì)打磨拢操,大家看看就好啦。

如果覺得文章的描寫不夠清晰, 可以去看看項(xiàng)目的 Demo

需要對齊cell的原因:

UICollectionView是常用的一個應(yīng)用于流布局的類, 但是很可惜, 有時候蘋果官方提供的方法并不能優(yōu)美地展示你的UI, 畢竟每個人需要展示的效果可能都不一致. 下面是具體的解析:

首先看官方的對齊方式

創(chuàng)建一個新的項(xiàng)目, 在storyboard的ViewController中拽入UICollectionView定好約束, 然后拽入一個UICollectionViewCell并對Cell進(jìn)行注冊

Cell的代碼如下:

class TextCell: UICollectionViewCell {
    
    @IBOutlet weak var textLabel: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        //設(shè)置文本標(biāo)簽背景顏色
        textLabel.backgroundColor = .hexString("eeeeee")
        //設(shè)置超出的部分隱藏
        textLabel.layer.masksToBounds = true
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        //設(shè)置四角的弧度, 設(shè)置為高度的一般即是左右為半圓, 效果看下面的效果圖
        textLabel.layer.cornerRadius = frame.height/2
    }
}

修改ViewController中的代碼

class ViewController: UIViewController {
    
    @IBOutlet weak var collectionView: UICollectionView!
    //文字?jǐn)?shù)據(jù)
    var dataSource: [String] = ["made","in","China","UIView","UITableView","UICollectionView"]

    override func viewDidLoad() {
        super.viewDidLoad()
        
        //初始化一個AlignFlowLayout實(shí)例
        let flowLayout = UICollectionViewFlowLayout()
        //設(shè)置行間距
        flowLayout.minimumLineSpacing = 10
        //設(shè)置列間距
        flowLayout.minimumInteritemSpacing = 10
        //設(shè)置邊界的填充距離
        flowLayout.sectionInset = UIEdgeInsets.init(top: 10, left: 10, bottom: 10, right: 10)
        //給collectionView設(shè)置布局屬性, 也可以通過init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout)方法來創(chuàng)建一個UICollectionView對象
        collectionView.collectionViewLayout = flowLayout
        //設(shè)置代理
        collectionView.delegate = self
        collectionView.dataSource = self
    }
}

由于UICollectionViewDelegateFlowLayout這個協(xié)議提供了collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize這個方法, 我們可以實(shí)現(xiàn)該方法返回item的size達(dá)成cell大小自適應(yīng). 下面是協(xié)議實(shí)現(xiàn):

//MARK: - UICollectionViewDataSource
//返回對應(yīng)組中item的個數(shù), Demo中只有一個分組, 所以直接返回個數(shù)
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
    //設(shè)置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)
}

下面是現(xiàn)在代碼的運(yùn)行效果:

默認(rèn)對齊方式, 空白部分拉伸撐滿顯示區(qū)域

標(biāo)記圖如下, 標(biāo)記的單位是px, 20px相當(dāng)于代碼中的10:

間距標(biāo)記

通過觀察可以發(fā)現(xiàn), 在collectionView第一行顯示不下UITableView這個cell, 自動把它移動到第二行,第一行的cell平鋪開, 我們一開始設(shè)置的列間距minimumInteritemSpacing = 10在第一行變得無效了, 因?yàn)樵诠俜酱a的計算中sectionInset的優(yōu)先級比minimumInteritemSpacing高, 實(shí)際顯示的列間距變成了(collectionView.frame.size.width-(同一行各item的寬度之和)-sectionInset.left-sectionInset.right)/(item的個數(shù)-1), 然而這樣的顯示效果有時候不是我們想要的, 有時候我們就想要minimumInteritemSpacing這個列間距生效所有cell向左看齊, 在我目前的摸索中還沒有發(fā)現(xiàn)方便快捷的屬性可以直接改變系統(tǒng)默認(rèn)的對齊方式, 希望有這方面知識的朋友可以指點(diǎn)我.

對齊的實(shí)現(xiàn)(寬度一致)

我參考很多優(yōu)秀的開源代碼, 給了我很好的思路, 現(xiàn)在我用Swift 3.0 語言來把自己的想法通過比較簡單的代碼展現(xiàn)出來.

創(chuàng)建一個枚舉, 用于區(qū)分對齊方向

/// 對齊方向的枚舉, 可拓展, 命名可根據(jù)自己喜好
enum AlignDirection: Int {
    case start = 0,   //左對齊
         end,         //右對齊  左起顯示
         dataEnd,     //右對齊  右起顯示
         center,      //中間對齊
         auto         //自動對齊舶替,系統(tǒng)默認(rèn)效果
}

創(chuàng)建一個類AlignFlowLayout繼承于UICollectionViewFlowLayout, 該類內(nèi)部的代碼如下:

AlignDelegateFlowLayout協(xié)議

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

AlignFlowLayout類創(chuàng)建以下屬性

class AlignFlowLayout: UICollectionViewFlowLayout {

    /// 默認(rèn)自動對齊
    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 {
            // 獲取分組個數(shù)
            let sections = collection.numberOfSections
            // 遍歷分組
            for i in 0..<sections {
                // 獲取組內(nèi)元素個數(shù)
                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)
                }
                // 遍歷獲取組內(nèi)每個元素的布局屬性
                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)
                }
                // 添加裝飾視圖支持,內(nèi)容需要自定義
                // 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
                }
            }
            // 設(shè)置內(nèi)容尺寸
            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
                            }
                            // 判斷是否處于貼緊狀態(tài)
                            if isFollow {
                                // 獲取同組的布局屬性
                                let filter = layoutAttributes.filter { (item) -> Bool in
                                    return item.indexPath.section == attribute.indexPath.section && item.representedElementCategory == .cell
                                }
                                if filter.isEmpty {
                                    // 如果沒有同組布局屬性钉跷,則現(xiàn)有布局屬性為該組的第一個布局屬性
                                    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 {
                                        // 越界,采用貼緊坐標(biāo)
                                        attribute.ex_x = minX
                                        attribute.ex_y = minY + minimumLineSpacing
                                    } else {
                                        // 不越界膝晾,右方直接添加
                                        attribute.ex_x = last.frame.maxX + minimumInteritemSpacing
                                        attribute.ex_y = last.frame.minY
                                    }
                                }
                            } else {
                                // 不處于貼緊狀態(tài)下栓始,切換行時會調(diào)整上一列的布局屬性
                                if attribute.frame.minX < last.frame.minX {
                                    reloadlayoutAttributes()
                                }
                            }
                        } else if last.representedElementCategory == .supplementaryView {
                            attribute.ex_x = self.sectionInset.left
                            // 如果上一個布局屬性是屬于頭部或者尾部的,當(dāng)前布局屬性需要增加組內(nèi)填充距離
                            if isFollow {
                                attribute.ex_y = last.frame.maxY + self.sectionInset.top
                            }
                        }
                    } else if isFollow {
                        // 沒有上一個布局屬性血当,當(dāng)前是第一個cell幻赚,直接設(shè)置x坐標(biāo)
                        attribute.ex_y = self.sectionInset.top
                    }
                    // 添加進(jìn)當(dāng)前行
                    layoutLine.append(attribute)
                    // 判斷當(dāng)前元素是當(dāng)前組的最后一個元素,重置當(dāng)前行的布局屬性
                    if let items = collectionView?.numberOfItems(inSection: indexPath.section), indexPath.row == items - 1 {
                        reloadlayoutAttributes()
                    }
                case .dataEnd:
                    // 右起顯示需要獲取collectionView顯示區(qū)域的寬度
                    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
                            }
                            // 判斷是否處于貼緊狀態(tài)
                            if isFollow {
                                // 獲取同組的布局屬性
                                let filter = layoutAttributes.filter { (item) -> Bool in
                                    return item.indexPath.section == attribute.indexPath.section && item.representedElementCategory == .cell
                                }
                                if filter.isEmpty {
                                    // 如果沒有同組布局屬性巍扛,則現(xiàn)有布局屬性為該組的第一個布局屬性
                                    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 {
                                        // 越界,采用貼緊坐標(biāo)
                                        attribute.ex_x = minX + minimumLineSpacing
                                        attribute.ex_y = minY
                                    } else {
                                        // 不越界胧瓜,下方直接添加
                                        attribute.ex_x = last.frame.minX
                                        attribute.ex_y = last.frame.maxY + minimumInteritemSpacing
                                    }
                                }
                            } else {
                                // 不處于貼緊狀態(tài)下矢棚,切換列時會調(diào)整上一列的布局屬性
                                if attribute.frame.minX <= last.frame.minX {
                                    reloadlayoutAttributes()
                                }
                            }
                        } else if last.representedElementCategory == .supplementaryView {
                            attribute.ex_y = self.sectionInset.top
                            // 如果上一個布局屬性是屬于頭部或者尾部的,當(dāng)前布局屬性需要增加組內(nèi)填充距離
                            if isFollow {
                                attribute.ex_x = last.frame.maxX + self.sectionInset.left
                            }
                        }
                    } else if isFollow {
                        // 沒有上一個布局屬性府喳,當(dāng)前是第一個cell蒲肋,直接設(shè)置x坐標(biāo)
                        attribute.ex_x = self.sectionInset.left
                    }
                    // 添加進(jìn)當(dāng)前列
                    layoutLine.append(attribute)
                    // 判斷當(dāng)前元素是當(dāng)前組的最后一個元素,重置當(dāng)前列的布局屬性
                    if let items = collectionView?.numberOfItems(inSection: indexPath.section), indexPath.row == items - 1 {
                        reloadlayoutAttributes()
                    }
                case .dataEnd:
                    // 下起顯示需要獲取collectionView顯示區(qū)域的高度
                    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
        }
        // 默認(rèn)布局,返回原始布局屬性
        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 {
                    // 如果上一組的布局屬性不為空,獲取新坐標(biāo)
                    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 {
                // 根據(jù)同一組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. 左對齊

上面的代碼中就是左對齊的實(shí)現(xiàn), 調(diào)用的代碼如下:

//初始化一個AlignFlowLayout實(shí)例
let flowLayout = AlignFlowLayout()
//設(shè)置方向
flowLayout.direction = .start
//設(shè)置行間距
flowLayout.minimumLineSpacing = 10
//設(shè)置列間距
flowLayout.minimumInteritemSpacing = 10
//設(shè)置邊界的填充距離
flowLayout.sectionInset = UIEdgeInsets.init(top: 10, left: 10, bottom: 10, right: 10)
//注冊DecorationView
flowLayout.register(CustomDecorationView.self, forDecorationViewOfKind: kCustomDecorationViewKind)
        
//給collectionView設(shè)置布局屬性, 也可以通過init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout)方法來創(chuàng)建一個UICollectionView對象
collectionView.collectionViewLayout = flowLayout

實(shí)質(zhì)上就是簡單地把UICollectionViewFlowLayout替換成我們自定義的AlignFlowLayout, 運(yùn)行效果如下:

左對齊

2. 右對齊

右對齊有兩種顯示方式, 一種是數(shù)據(jù)源從右到左顯示, 另一種是數(shù)據(jù)源從左到右顯示但是UI向右靠攏路鹰,因?yàn)榍懊嬖O(shè)置了對齊方式是左對齊, 所以現(xiàn)在要修改對齊方式:

flowLayout.direction = .end

運(yùn)行效果如下:


右對齊

第二種方式調(diào)用是一樣的, 修改對齊方式即可:

flowLayout.direction = .dataEnd

下面是效果:

數(shù)據(jù)右起對齊

3. 居中對齊

計算思路可以簡單地分兩種:

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

按照計算步驟來考慮, 第一種方法比較好

最終效果如下:

居中對齊

4. 2021年新增內(nèi)容

多個分組
多個分組
裝飾視圖
裝飾視圖
跟隨模式

有時候贷洲,使用collectionView會用到不規(guī)則的矩形,如果不進(jìn)行適配會如下顯示:


不規(guī)則矩形

應(yīng)用跟隨模式

flowLayout.isFollow = true

效果如下:


不規(guī)則矩形(跟隨模式)
水平方向滾動
水平方向滾動

注意:

  1. 盡量不要讓item的寬度與sectionInset之和超過collectionView的實(shí)際寬度, 否則可能會導(dǎo)致顯示出錯.
  2. 跟隨模式只適用于豎直滾動模式下寬度一致高度不一致和水平模式滾動下高度一致而寬度不一致的情況晋柱,其他情況請關(guān)閉
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末优构,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子趣斤,更是在濱河造成了極大的恐慌俩块,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件浓领,死亡現(xiàn)場離奇詭異玉凯,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)联贩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門漫仆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人泪幌,你說我怎么就攤上這事盲厌。” “怎么了祸泪?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵吗浩,是天一觀的道長。 經(jīng)常有香客問我没隘,道長懂扼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任右蒲,我火速辦了婚禮阀湿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘瑰妄。我一直安慰自己陷嘴,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布间坐。 她就那樣靜靜地躺著灾挨,像睡著了一般邑退。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上涨醋,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天瓜饥,我揣著相機(jī)與錄音,去河邊找鬼浴骂。 笑死,一個胖子當(dāng)著我的面吹牛宪潮,可吹牛的內(nèi)容都是我干的溯警。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼狡相,長吁一口氣:“原來是場噩夢啊……” “哼梯轻!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起尽棕,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤喳挑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后滔悉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體伊诵,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年回官,在試婚紗的時候發(fā)現(xiàn)自己被綠了曹宴。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡歉提,死狀恐怖笛坦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情苔巨,我是刑警寧澤版扩,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站侄泽,受9級特大地震影響礁芦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蔬顾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一宴偿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧诀豁,春花似錦窄刘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽活翩。三九已至,卻和暖如春翻伺,著一層夾襖步出監(jiān)牢的瞬間材泄,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工吨岭, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拉宗,地道東北人。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓辣辫,卻偏偏與公主長得像旦事,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子急灭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容