通常咱们是使用 UICollectionViewFlowLayout , 熟悉的格子视图。也能够自定制 UICollectionViewLayout ,对于每个列表元素,想放哪就放哪。git
这种状况,系统的就很差直接拿来使了,须要本身定制一个 UICollectionViewLayout.github
通常 new 一个 UICollectionViewLeftAlignedLayout, 继承自 UICollectionViewFlowLayout app
一般要重写 UICollectionViewFlowLayout 的这两个方法,异步
layoutAttributesForElements(in:)
, 这个方法须要提供,给定的矩形里面全部的格子的布局属性。给定的矩形区域,就是 UICollectioonView 的内容视图区域 contentSize. ide
layoutAttributesForItem(at:):
这个方法须要提供,格子视图须要的具体的布局信息。咱们要重写这个方法,返回要求的 indexPath 位置上格子的布局属性。函数
有时候也要重写这个属性:布局
collectionViewContentSize
, 通常咱们是把内容区域的尺寸,做为计算属性处理的。他提供格子视图的内容区域的宽度与高度。格子视图的内容区域,不是格子视图的可见区域。测试
由于格子视图 UICollectionView,继承自 UIScrollView。 格子视图使用该属性,配置他做为可滑动视图 UIScrollView 的内容视图尺寸。优化
主要代码见以下:ui
其中辅助函数没有列出来,具体见文尾的 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, 就没有这方面的问题。
// 我用了锁,测试时,每秒刷新一个布局。次数多了,会出现 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 的尺寸和这些格子的位置。
同例子一,把每行第一个元素,铺到最右端,其他的格子保持固定间距就行了。
代码以下:
// 这个函数没有作什么事情,主要是调用作事情的函数 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 }