显而易见,UICollectionViewCompositionalLayout 给我们带来易用性的同时也牺牲了 UICollectionViewLayout 的灵活性。当我们决定选择前者的时候,内心多少会祈祷着在需求范围之内不要有什么坑啊拜托!然而,Apple 不遂人意,问就是解决提出问题的人可解千愁~

玩笑终归是玩笑,该搬的🧱一块也少不了……

如“标题”的问题描述

默认情况下,当 NSCollectionLayoutDecorationItem 用作背景时,会包含 NSCollectionLayoutBoundarySupplementaryItem。如果要设置内容的偏移,只能通过 contentInsets 进行 Hard Code。对于固定高度的 Supplementary 自然无压力,但对于自适应宽高的情况就显得力有未逮了。

名为 Re-Layout 的小聪明

在 NSCollectionLayoutSection 中有一个叫做 visibleItemsInvalidationHandler 的回调会告知开发者当前可视 Item 被展示前的准确信息:

A closure called before each layout cycle to allow modification of the items in the section immediately before they’re displayed.

但是其允许修改的属性很有限,我们需要涉及到宽高就不在其中。不过借助其计算出来的 frame 等信息,我们可以在适当的时机进行重新布局,变相完成我们的需求。以下贴出核心代码逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
UICollectionViewCompositionalLayout { sectionIdx, env in
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(150))
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitem: item, count: 3)
let result = NSCollectionLayoutSection(group: group)
result.contentInsets = .init(top: 14, leading: 34, bottom: 14, trailing: 34)
let decorationItem = NSCollectionLayoutDecorationItem.background(elementKind: String(describing: Decoration.self))
decorationItem.contentInsets = .init(top: self._headerHeight ?? 0, leading: 20, bottom: 0, trailing: 20)

result.decorationItems = [decorationItem]
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(20)), elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
result.boundarySupplementaryItems = [header]
result.visibleItemsInvalidationHandler = { items, offset, env in
if let decorationItem = items.first(where: { $0.representedElementCategory == .decorationView }),
let header = items.first(where: { $0.representedElementCategory == .supplementaryView }),
decorationItem.frame.origin.y == header.frame.origin.y
{
self._headerHeight = header.frame.height
DispatchQueue.main.async {
self._collectionView.collectionViewLayout.invalidateLayout()
}
}
}
return result
}

总结

早在两年前,这里就提出了相关问题,只是一直没有被有效地解答。希望这个“小聪明”能够解决这个问题,更进一步则是期盼 Apple 早日对此做出 API 级别的支持,🙏