一般我们是使用 UICollectionViewFlowLayout , 熟悉的格子视图。也可以自定制 UICollectionViewLayout ,对于每一个列表元素,想放哪就放哪。

譬如: 固定行距列表左排

这种情况,系统的就不好直接拿来使了,需要自己定制一个 UICollectionViewLayout.

一般 new 一个 UICollectionViewLeftAlignedLayout, 继承自 UICollectionViewFlowLayout

通常要重写 UICollectionViewFlowLayout 的这两个方法,

layoutAttributesForElements(in:), 这个方法需要提供,给定的矩形里面所有的格子的布局属性。给定的矩形区域,就是 UICollectioonView 的内容视图区域 contentSize.

layoutAttributesForItem(at:): 这个方法需要提供,格子视图需要的具体的布局信息。我们要重写这个方法,返回要求的 indexPath 位置上格子的布局属性。

有时候也要重写这个属性:

collectionViewContentSize, 一般我们是把内容区域的尺寸,作为计算属性处理的。他提供格子视图的内容区域的宽度与高度。格子视图的内容区域,不是格子视图的可见区域。

因为格子视图 UICollectionView,继承自 UIScrollView。 格子视图使用该属性,配置他作为可滑动视图 UIScrollView 的内容视图尺寸。

主要代码见如下:

其中辅助函数没有列出来,具体见文尾的 github repo.

class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout {// 这个函数没有做什么事情,主要是调用做事情的函数 layoutAttributesForItem,获取信息,提供出去    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {        var attributesCopy: [UICollectionViewLayoutAttributes] = []        if let attributes = super.layoutAttributesForElements(in: rect) {            attributes.forEach({ attributesCopy.append($0.copy() as! UICollectionViewLayoutAttributes) })        }        for attributes in attributesCopy {            if attributes.representedElementKind == nil {                let indexpath = attributes.indexPath                // 做事情的地方                if let attr = layoutAttributesForItem(at: indexpath) {                    attributes.frame = attr.frame                }            }        }        return attributesCopy    }    // 这个函数里面,具体处理了固定行距列表左排的布局    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {                if let currentItemAttributes = super.layoutAttributesForItem(at: indexPath as IndexPath)?.copy() as? UICollectionViewLayoutAttributes, let collection = collectionView {            let sectionInset = evaluatedSectionInsetForItem(at: indexPath.section)            let isFirstItemInSection = indexPath.item == 0            let layoutWidth = collection.frame.width - sectionInset.left - sectionInset.right            // 让每一行的第一个元素排头,分两种情况处理。这是第一种,这个 section 的第一个元素,自然是排头。            guard !isFirstItemInSection else{                currentItemAttributes.leftAlignFrame(with: sectionInset)                return currentItemAttributes            }            let previousIndexPath = IndexPath(item: indexPath.item - 1, section: indexPath.section)            let previousFrame = layoutAttributesForItem(at: previousIndexPath)?.frame ?? CGRect.zero            let previousFrameRightPoint = previousFrame.origin.x + previousFrame.width            let currentFrame = currentItemAttributes.frame            let strecthedCurrentFrame = CGRect(x: sectionInset.left,                                                    y: currentFrame.origin.y,                                                    width: layoutWidth,                                                    height: currentFrame.size.height)            let isFirstItemInRow = !previousFrame.intersects(strecthedCurrentFrame)            // 让每一行的第一个元素排头,分两种情况处理。这是第二种,这个 section 的其他的排头,算出来,就是:上一个格子在上一行,不在当前行,            guard !isFirstItemInRow else{                currentItemAttributes.leftAlignFrame(with: sectionInset)                return currentItemAttributes            }            //  剩下的,简单了。统一处理掉。 剩下的格子都不是排头,与上一个固定间距完了。            var frame = currentItemAttributes.frame            frame.origin.x = previousFrameRightPoint + evaluatedMinimumInteritemSpacing(at: indexPath.section)            currentItemAttributes.frame = frame            return currentItemAttributes                    }        return nil    }// ...}

说一下 func layoutAttributesForItem(at indexPath: IndexPath) 的设计思路。

因为如果使用 UICollectionViewFlowLayout ,什么都不干,与上图的区别就一点。
每一行的元素个数一致,具体也一致,就是那些格子是居中的。毕竟 minimumInteritemSpacing 是最小行内间距的意思,不是固定的行内间距。

然后移一移,就好了。让每一行的第一个元素排头,每一行的其他元素与上一个元素固定间距,这就完了。

例子二: 固定行距列表右排

分析:看起来,右排就是把左边排列好的元素,推往右边。具体就是左边排列好的元素,每一行的元素都添加一个当前行的 OffsetX,

OffsetX = CollectionView 的 frame.width - 左边排列好元素的最后一个 frame.maxX

这是另外一种情况。因为不能改一改左排的 layoutAttributesForItem 方法,就好。左排用的是 previous,没什么问题。

右排用 next , 对于每一个元素,找 next , 知道其 X + width > collectionView 的 width, OffsetX 就出来了。

采用 layoutAttributesForItem 找 next, 会有一个比较烦的递归,当前找 next, next 找 next.
其实第一种情况也是这样,当前找 previous, previous 找 previous.

区别在于苹果做了优化,next 找 next,因为我在 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize 返回的尺寸宽度随机。

然后就乱套了。当前找 next 返回的随机的 size, 到了 next, 会返回另外随机的 size.
previous 找 previous, 就没有这方面的问题。

我采用的是算两遍,先左排,再基于左排的结果,添加 OffsetX , 就变右排了。

// 我用了锁,测试时,每秒刷新一个布局。次数多了,会出现 Mac 上多核心 CPU 异步绘制的结果不太好let lock = NSLock()// 因为要算两次,第一次就不能放在  func layoutAttributesForElements(in rect: CGRect)  方法,// 我放在  func prepare() 方法,采用创建的 UICollectionViewLayoutAttributes    override func prepare() {        lock.lock()        contentHeight = 0        cache.removeAll()        storedCellSize.removeAll()        guard let collectionView = collectionView else {            return        }        var currentXOffset:CGFloat = 0        var nextXOffset:CGFloat = 0        var currentYOffset:CGFloat = 0        var nextYOffset:CGFloat = 0                        for section in 0..<collectionView.numberOfSections{            let sectionInset = evaluatedSectionInsetForItem(at: section)            nextXOffset = sectionInset.left            nextYOffset += sectionInset.top            let count = collectionView.numberOfItems(inSection: section)            for item in 0..<count{                currentXOffset = nextXOffset                currentYOffset = nextYOffset                let indexPath = IndexPath(item: item, section: section)                // 采用创建的 UICollectionViewLayoutAttributes,不是访问系统的 super.layoutAttributesForItem(at:)                let currentItemAttributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)                let currentIndexPathSize = queryItemSize(indexPath)                let currentItemInNext = (currentXOffset + evaluatedMinimumInteritemSpacing(at: section) + currentIndexPathSize.width) > (collectionView.frame.width - sectionInset.right + 0.1)                if currentItemInNext{                    currentXOffset = sectionInset.left                    currentYOffset += (currentIndexPathSize.height + evaluatedMinimumLineSpacing(at: section))                                        nextXOffset = currentXOffset + (currentIndexPathSize.width + evaluatedMinimumInteritemSpacing(at: section))                                       nextYOffset = currentYOffset                }else{                    nextXOffset += (currentIndexPathSize.width + evaluatedMinimumInteritemSpacing(at: section))                }                                                let frame = CGRect(origin: CGPoint(x: currentXOffset, y: currentYOffset), size: currentIndexPathSize)                currentItemAttributes.frame = frame                cache[indexPath] = currentItemAttributes                contentHeight = max(contentHeight, frame.maxY)            }            nextYOffset = contentHeight        }        lock.unlock()    }        // 这个函数没有做什么事情,主要是调用做事情的函数 layoutAttributesForItem,获取信息,提供出去    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {                var attributesCopy: [UICollectionViewLayoutAttributes] = []        if let attributes = super.layoutAttributesForElements(in: rect) {            attributes.forEach({ attributesCopy.append($0.copy() as! UICollectionViewLayoutAttributes) })        }        for attributes in attributesCopy {            if attributes.representedElementKind == nil {                let indexpath = attributes.indexPath                if let attr = layoutAttributesForItem(at: indexpath) {                    attributes.frame = attr.frame                }            }        }        return attributesCopy    }            // 这个函数是第二次计算。把第一计算的结果左排,通过算出 OffsetX 添加上去,变成右排    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {        guard let collectionView = collectionView,let attribute = cache[indexPath] else {            return nil        }                var offsetX: CGFloat = attribute.frame.maxX        let criteria = collectionView.frame.width - evaluatedSectionInsetForItem(at: indexPath.section).right - 0.1        var gap = criteria - offsetX        var ip = indexPath        let sectionCount = collectionView.numberOfItems(inSection: indexPath.section)        while ip.item < sectionCount{           // 通过 Y 值来比较,结果比较稳定。同一行嘛,Y 坐标,自然是一致的。            var conditionSecond = false            if let nextAttri = cache[ip.next]{                conditionSecond = nextAttri.frame.minY != attribute.frame.minY            }            if (ip.item + 1) >= sectionCount || conditionSecond {                gap = criteria - offsetX                break            }            else{                ip = ip.next                offsetX += (evaluatedMinimumInteritemSpacing(at: indexPath.section) + cache[ip]!.frame.width)                           }        }        attribute.trailingAlignFrame(with: gap)        return attribute            }

这里用到了 prepare() 方法,当有布局操作的时候,就调用这个方法。
我们用这个时机,计算出提供给 collectionView 的尺寸和这些格子的位置。

: 例子 3 , 右边反着排

同例子一,把每行第一个元素,铺到最右端,其余的格子保持固定间距就好了。

代码如下:

// 这个函数没有做什么事情,主要是调用做事情的函数 layoutAttributesForItem,获取信息,提供出去override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {                var attributesCopy: [UICollectionViewLayoutAttributes] = []        if let attributes = super.layoutAttributesForElements(in: rect) {            attributes.forEach({ attributesCopy.append($0.copy() as! UICollectionViewLayoutAttributes) })        }                for attributes in attributesCopy {            if attributes.representedElementKind == nil {                let indexpath = attributes.indexPath                if let attr = layoutAttributesForItem(at: indexpath) {                    attributes.frame = attr.frame                }            }        }        return attributesCopy    }        override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {                if let currentItemAttributes = super.layoutAttributesForItem(at: indexPath as IndexPath)?.copy() as? UICollectionViewLayoutAttributes , let collection = collectionView{            let isFirstItemInSection = indexPath.item == 0             // 右边派头情况一            if isFirstItemInSection {                currentItemAttributes.rightAlignFrame(with: collection.frame.size.width)                return currentItemAttributes            }                        let previousIndexPath = IndexPath(item: indexPath.item - 1, section: indexPath.section)                        let previousFrame = layoutAttributesForItem(at: previousIndexPath)?.frame ?? CGRect.zero              let currentFrame = currentItemAttributes.frame            let strecthedCurrentFrame = CGRect(x: 0,                                                    y: currentFrame.origin.y,                                                    width: collection.frame.size.width,                                                    height: currentFrame.size.height)            let isFirstItemInRow = !previousFrame.intersects(strecthedCurrentFrame)             // 右边派头情况二            if isFirstItemInRow {                currentItemAttributes.rightAlignFrame(with: collection.frame.size.width)                return currentItemAttributes            }             // 右边正常的情况            let previousFrameLeftPoint = previousFrame.origin.x            var frame = currentItemAttributes.frame            let minimumInteritemSpacing = evaluatedMinimumInteritemSpacing(at: indexPath.item)            frame.origin.x = previousFrameLeftPoint - minimumInteritemSpacing - frame.size.width            currentItemAttributes.frame = frame            return currentItemAttributes                    }        return nil    }

BoxDengJZ/UICollectionViewLeftAlignedLayout

StackOverFlow: How do you determine spacing between cells in UICollectionView flowLayout