前言
在项目新版本中,要实现类似以下的效果:给每个section
区域添加一个卡片装饰背景以及一个袖标装饰图标(卡片在所有的cell
下,袖标在cell
上面)。
这可以通过UICollectionView
的 DecorationView
特性来达到以上效果。本文主要是总结 DecorationView
的实现、重用机制和存在的坑。
DecorationView 的实现(包括坑)
实现原理
- 继承
UICollectionViewLayoutAttributes
,实现用于描述装饰视图的布局属性的类,如描述卡片装饰视图的SectionCardDecorationCollectionViewLayoutAttributes
- 继承
UICollectionReusableView
,实现自己的装饰视图,如卡片装饰视图 SectionCardDecorationReusableView
- 继承
UICollectionViewFlowLayout
,实现自己的布局计算:主要是注册自定义的装饰视图和计算管理这些装饰视图的布局属性。如 SectionCardDecorationCollectionViewLayout
- 继承
UICollectionView
,override layoutSubviews
方法,解决装饰视图的一个坑(关于此坑,请看文章具体描述)
核心代码
1. 自定义装饰图的布局属性
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
| class SectionCardDecorationCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {
var backgroundColor = UIColor.white
override func copy(with zone: NSZone? = nil) -> Any { let copy = super.copy(with: zone) as! SectionCardDecorationCollectionViewLayoutAttributes copy.backgroundColor = self.backgroundColor return copy }
override func isEqual(_ object: Any?) -> Bool { guard let rhs = object as? SectionCardDecorationCollectionViewLayoutAttributes else { return false }
if !self.backgroundColor.isEqual(rhs.backgroundColor) { return false } return super.isEqual(object) } }
|
2. 自定义装饰图
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 27 28 29 30 31 32 33 34 35 36 37 38 39
| class SectionCardDecorationReusableView: UICollectionReusableView {
override init(frame: CGRect) { super.init(frame: frame) self.customInit() }
required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder)! self.customInit() }
func customInit() { self.backgroundColor = UIColor.white
self.layer.cornerRadius = 6.0 self.layer.borderColor = UIColor.clear.cgColor self.layer.borderWidth = 1.0 self.layer.shadowColor = UIColor.black.cgColor self.layer.shadowOpacity = 0.17 self.layer.shadowOffset = CGSize.init(width: 0, height: 1.0) self.layer.shadowRadius = 1 }
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { super.apply(layoutAttributes)
guard let attr = layoutAttributes as? SectionCardDecorationCollectionViewLayoutAttributes else { return }
self.backgroundColor = attr.backgroundColor } }
let SectionCardDecorationViewKind = "SectionCardDecorationReuseIdentifier"
|
3. 自定义 UICollectionViewFlowLayout
自定义 UICollectionViewFlowLayout
,主要是实现自己的布局计算。主要的计算操作有:
- 初始化时进行装饰视图的注册操作(对应
setup
方法)
- override
prepare
方法,计算生成装饰视图的布局属性
- override
layoutAttributesForElements
方法,返回可视范围下装饰视图的布局属性
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
| class SectionCardDecorationCollectionViewLayout: UICollectionViewFlowLayout {
private var cardDecorationViewAttrs: [Int:UICollectionViewLayoutAttributes] = [:] private var armbandDecorationViewAttrs: [Int:UICollectionViewLayoutAttributes] = [:]
public weak var decorationDelegate: SectionCardDecorationCollectionViewLayoutDelegate?
override init() { super.init() setup() }
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) }
override func awakeFromNib() { super.awakeFromNib()
setup() }
func setup() { self.register(SectionCardDecorationReusableView.self, forDecorationViewOfKind: SectionCardDecorationViewKind)
self.register(SectionCardArmbandDecorationReusableView.self, forDecorationViewOfKind: SectionCardArmbandDecorationViewKind) }
override func prepare() { super.prepare()
guard let numberOfSections = self.collectionView?.numberOfSections else { return }
let flowLayoutDelegate: UICollectionViewDelegateFlowLayout? = self.collectionView?.delegate as? UICollectionViewDelegateFlowLayout
guard let strongCardDecorationDelegate = decorationDelegate else { return }
self.cardDecorationViewAttrs.removeAll() self.armbandDecorationViewAttrs.removeAll()
for section in 0..<numberOfSections { guard let numberOfItems = self.collectionView?.numberOfItems(inSection: section), numberOfItems > 0, let firstItem = self.layoutAttributesForItem(at: IndexPath(item: 0, section: section)), let lastItem = self.layoutAttributesForItem(at: IndexPath(item: numberOfItems - 1, section: section)) else { continue }
var sectionInset = self.sectionInset if let inset = flowLayoutDelegate?.collectionView?(self.collectionView!, layout: self, insetForSectionAt: section) { sectionInset = inset }
var sectionFrame = firstItem.frame.union(lastItem.frame) if self.scrollDirection == .horizontal { sectionFrame.origin.x -= sectionInset.left sectionFrame.origin.y = sectionInset.top sectionFrame.size.width += sectionInset.left + sectionInset.right sectionFrame.size.height = self.collectionView!.frame.height } else { sectionFrame.origin.x = sectionInset.left sectionFrame.origin.y -= sectionInset.top sectionFrame.size.width = self.collectionView!.frame.width sectionFrame.size.height += sectionInset.top + sectionInset.bottom }
let cardDisplayed = strongCardDecorationDelegate.collectionView(self.collectionView!, layout: self, decorationDisplayedForSectionAt: section) guard cardDisplayed == true else { continue }
let cardDecorationInset = strongCardDecorationDelegate.collectionView(self.collectionView!, layout: self, decorationInsetForSectionAt: section) var cardDecorationFrame = sectionFrame if self.scrollDirection == .horizontal { cardDecorationFrame.origin.x = sectionFrame.origin.x + cardDecorationInset.left cardDecorationFrame.origin.y = cardDecorationInset.top } else { cardDecorationFrame.origin.x = cardDecorationInset.left cardDecorationFrame.origin.y = sectionFrame.origin.y + cardDecorationInset.top } cardDecorationFrame.size.width = sectionFrame.size.width - (cardDecorationInset.left + cardDecorationInset.right) cardDecorationFrame.size.height = sectionFrame.size.height - (cardDecorationInset.top + cardDecorationInset.bottom)
let cardAttr = SectionCardDecorationCollectionViewLayoutAttributes( forDecorationViewOfKind: SectionCardDecorationViewKind, with: IndexPath(item: 0, section: section)) cardAttr.frame = cardDecorationFrame
cardAttr.zIndex = -1 let backgroundColor = strongCardDecorationDelegate.collectionView(self.collectionView!, layout: self, decorationColorForSectionAt: section) cardAttr.backgroundColor = backgroundColor
self.cardDecorationViewAttrs[section] = cardAttr
let armbandDisplayed = strongCardDecorationDelegate.collectionView(self.collectionView!, layout: self, armbandDecorationDisplayedForSectionAt: section) guard armbandDisplayed == true else { continue }
guard let imageName = strongCardDecorationDelegate.collectionView(self.collectionView!, layout: self, armbandDecorationImageForSectionAt: section) else { continue }
var armbandDecorationInset = cardDecorationInset armbandDecorationInset.left = 1 armbandDecorationInset.top = 18 if let topOffset = strongCardDecorationDelegate.collectionView(self.collectionView!, layout: self, armbandDecorationTopOffsetForSectionAt: section) { armbandDecorationInset.top = topOffset } var armbandDecorationFrame = sectionFrame if self.scrollDirection == .horizontal { armbandDecorationFrame.origin.x = sectionFrame.origin.x + armbandDecorationInset.left armbandDecorationFrame.origin.y = armbandDecorationInset.top } else { armbandDecorationFrame.origin.x = armbandDecorationInset.left armbandDecorationFrame.origin.y = sectionFrame.origin.y + armbandDecorationInset.top } armbandDecorationFrame.size.width = 80 armbandDecorationFrame.size.height = 53
let armbandAttr = SectionCardArmbandDecorationCollectionViewLayoutAttributes( forDecorationViewOfKind: SectionCardArmbandDecorationViewKind, with: IndexPath(item: 0, section: section)) armbandAttr.frame = armbandDecorationFrame armbandAttr.zIndex = 1 armbandAttr.imageName = imageName self.armbandDecorationViewAttrs[section] = armbandAttr } }
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { var attrs = super.layoutAttributesForElements(in: rect) attrs?.append(contentsOf: self.cardDecorationViewAttrs.values.filter { return rect.intersects($0.frame) }) attrs?.append(contentsOf: self.armbandDecorationViewAttrs.values.filter { return rect.intersects($0.frame) }) return attrs }
override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { let section = indexPath.section if elementKind == SectionCardDecorationViewKind { return self.cardDecorationViewAttrs[section] } else if elementKind == SectionCardArmbandDecorationViewKind { return self.armbandDecorationViewAttrs[section] } return super.layoutAttributesForDecorationView(ofKind: elementKind, at: indexPath) } }
|
4. 自定义 UICollectionView
,解决装饰视图的坑
在描述这个坑前,需要先普及一个知识点:如何控制UICollectionView
的子视图的层级关系,如让卡片装饰视图居于cell下面?
答案是:使用UICollectionViewLayoutAttributes
的 zIndex
属性。 UICollectionView
进行布局时,会依据子视图的布局属性的 zIndex
的值的大小来控制子视图的 front-to-back 层级关系(在前或者在后)。cell 的布局属性的 zIndex
的值为0,所以若要卡片装饰视图在 cell 下面,只要设置其布局属性的 zIndex
的值小于0即可。
在知道这个知识点后,让我来具体描述一下的 UICollectionView
的在装饰视图的坑:在iOS10+上,zIndex
会随机失效。具体表现为,卡片装饰视图的布局属性的 zIndex
设置为 -1,比 cell 的小,理论上进行布局时,卡片装饰视图应该总是在 cell 下面;但是实际上,当你的 UICollectionView
比较复杂时,会 随机 出现某些 cell 布局在了卡片装饰视图下面,如图所示(由于这个“随机问题”只出现在具体的项目中,不出现在Demo中,为了方便说明问题,特意“手动”实现这种“随机问题”的效果来生成截图😂):
对于这个“随机”问题,国外论坛也有对应的讨论。
在该讨论的帖子下,有开发者建议通过设置 cell.layer.zPosition
来解决,但是我在尝试后,发现这个方法无效。最后,我使用了另一个方法来解决:自定义 UICollectionView
,override layoutSubviews
方法,手动调整装饰视图和cell的层级关系。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class CardCollectionView: UICollectionView {
override func layoutSubviews() { super.layoutSubviews()
var sectionCardViews: [UIView] = []
self.subviews.forEach { (subview) in if let decorationView = subview as? SectionCardDecorationReusableView { sectionCardViews.append(decorationView) } }
sectionCardViews.forEach { (decorationView) in self.sendSubview(toBack: decorationView) } } }
|
DecorationView 的重用机制
在 UICollectionView
里,DecorationView 的重用机制和 Cell 的重用机制是一致的:使用前,先注册(只不过 DecorationView 的注册是由UICollectionViewFlowLayout
来发起——实际还是 UICollectionView 进行最终的注册操作);使用时,由UICollectionView
根据上下文创建新的 DecorationView 或者返回旧的 DecorationView。
那么以上结论的依据是什么呢?请看下面的UICollectionView
的重用队列属性即可知道:
在 UICollectionView
里面有2种视图类型的重用队列,分别是 Cell 类型(对应cellReuseQueues
) 和 Supplementary 类型(对应supplementaryReuseQueues
)。这2种类型的重用机制是一样的。其中,DecorationView 是 SupplementaryView 的一种。
结语
最后,附上Demo代码。具体,请点击这个 repo。