在开发中,由于业务不同、产品不同、时间不同、亦或是为了美观等原因,为分组设置颜色、图片等情况屡见不鲜,因此如何实现比较通用的模版提高开发效率就变得尤为重要。
要实现这些效果的方法很多,由于经验限制的原因,这里只给出笔者认为比较好的实现方式:
通过自定义 UICollectionViewFlowLayout 计算 DecorationView 的布局Attributes 返回给父类调用实现
实现思路:
- 继承 UICollectionViewFlowLayout,重写 prepare 方法,获取到所有的分组 sections;
- 通过遍历分组,获取每个分组的起始位置 Attributes 和 结束位置 Attributes;
- 利用起始位置 Attributes frame 和 结束位置 Attributes frame 得出装饰视图 DecorationView 的 frame;
- 根据计算的 frame 创建 DecorationView Attributes,存储该 Attributes;
- 重写 layoutAttributesForDecorationView 方法,将存储的 DecorationView Attributes 返回给父类调用;
- 重写 layoutAttributesForElements 方法,拼接 DecorationView Attributes 后返回即可。
Note: 在获取结束位置的 Attributes 时,需要考虑到是否开启footerView的悬停效果,开启悬停效果后,获取到的 footerView Attributes 得到的 frame 并非真实的frame。
具体实现步骤如下:
一、定义装饰视图需要展示的类型
/*
装饰视图展示类型
*/
public enum CollectionViewDecorationViewLayoutType {
/* 装饰视图对items、 headerView、footerView 有效 */
case `default`
/* 装饰视图只对items有效,不包含 headerView 和 footerView*/
case onlyItems
/* 装饰视图对items 和 headerView 有效,不包含 footerView */
case headerAndItems
/* 装饰视图对items 和 footerView 有效,不包含 headerView */
case footerAndItems
}
二、定义装饰视图 DecorationView 配置协议
/*
装饰视图的一些基本配置,提供 delegate, block
*/
public protocol DecorationViewConfigurationDelegate : AnyObject {
/// 返回 不同分组的装饰视图具体类 根据 section 选择设置对应的 装饰视图具体类
/// - Parameters:
/// - section: 分组索引
func decorationViewReusableClass(at section: Int) -> (UICollectionReusableView.Type)?
/// 返回 分组装饰视图Inset default 5
/// - Parameters:
/// - section: 分组索引
func decorationViewExtendEdgeInsets(at section: Int) -> UIEdgeInsets
/// 返回 装饰视图布局类型 default .default
/// - Parameters:
/// - section: 分组索引
func decorationViewLayoutType(at section: Int) -> CollectionDecorationViewLayoutType
}
// MARK: 默认设置
extension DecorationViewConfigurationDelegate {
/// 装饰视图Edge default 5
func decorationViewExtendEdgeInsets(at section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: -5, left: 5, bottom: -5, right: 5)
}
/// 装饰视图布局类型
func decorationViewLayoutType(at section: Int) -> CollectionDecorationViewLayoutType {
return .default
}
}
三、自定义 CollectionDecorationViewFlowLayout 类,具体实现如下:
对于DecorationView的一些基本配置,提供 delegate 方法和 closure 方法
// MARK: 自定义flowLayout
public class CollectionDecorationViewFlowLayout : UICollectionViewFlowLayout {
/// 装饰视图代理
weak var delegate: DecorationViewConfigurationDelegate?
/// 返回 不同分组的装饰视图具体类 根据 section 选择设置对应的 装饰视图具体类
/// - Parameters:
/// - section: 分组索引
var reusableClass: ((_ section: Int) -> (UICollectionReusableView.Type)?)?
/// 返回 分组装饰视图Inset default .zero
/// - Parameters:
/// - section: 分组索引
var extendInsets: ((_ section: Int) -> UIEdgeInsets)?
/// 返回 装饰视图布局类型 default .default
/// - Parameters:
/// - section: 分组索引
var layoutType: ((_ section: Int) -> CollectionDecorationViewLayoutType)?
/// header footer 悬停效果开启 default false
var isPinToSectionHeaders: Bool {
didSet { sectionHeadersPinToVisibleBounds = isPinToSectionHeaders }
}
var isPinToSectionFooters: Bool {
didSet { sectionFootersPinToVisibleBounds = isPinToSectionFooters }
}
/// 装饰视图布局[section:UICollectionViewLayoutAttributes]
fileprivate var decorationLayoutAttrs: [Int: UICollectionViewLayoutAttributes] = [:]
// MARK: 初始化
override init() {
self.isPinToSectionHeaders = false
self.isPinToSectionFooters = false
super.init()
}
required init?(coder: NSCoder) {
fatalError()
}
}
重写父类方法:
// MARK: Override Methods
public extension CollectionDecorationViewFlowLayout {
override func prepare() {
super.prepare()
/// 布局准备
layoutDecorationView()
}
/// 返回装饰视图布局属性
override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let section = indexPath.section
if elementKind == decorationViewOfKind(at: section) {
return decorationLayoutAttrs[section]
}
return super.layoutAttributesForDecorationView(ofKind: elementKind, at: indexPath)
}
/// 返回rect范围所有元素的布局属性
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attrs = super.layoutAttributesForElements(in: rect)
attrs?.append(contentsOf: decorationLayoutAttrs.values.filter {
rect.intersects($0.frame)
})
return attrs
}
}
计算主要方法:
// MARK: Main Methods
private extension CollectionDecorationViewFlowLayout {
func layoutDecorationView() {
/// 清除原有配置
if !decorationLayoutAttrs.isEmpty {
decorationLayoutAttrs.removeAll()
}
/// 布局控件是否可见
guard let collectSize = collectionView?.bounds.size, collectSize.isValidated else { return }
/// 是否有分组数据
guard let sections = collectionView?.numberOfSections, sections > 0 else { return }
/// 分组配置
for section in 0 ..< sections {
/// 分组未设置装饰视图 继续下一组
guard let cls = decorationViewClass(at: section) else { continue }
/// 注册装饰视图
register(cls, forDecorationViewOfKind: cls.identifier)
/// 配置装饰视图
configDecorationView(at: section, kind: cls.identifier, layoutWidth: collectSize.width)
}
}
func configDecorationView(at section: Int, kind: String, layoutWidth: CGFloat) {
/// 根据装饰视图布局类型获取 布局视图起始点attrs
let firstAttr = firstLayoutAttributes(at: section)
/// 根据装饰视图布局类型获取 布局视图终点attr
let items = collectionView?.numberOfItems(inSection: section) ?? -1
let lastAttr = lastLayoutAttributes(at: .lastIdx(at: section, items: items))
/// 分组中无数据 继续下一组
guard let first = firstAttr, let last = lastAttr else { return }
/// 分组边距
let contentInsets = collectionView!.contentInset
let secInsets = sectionInsets(at: section)
/// 装饰视图边距
let decInsets = decorationViewInsets(at: section)
/// 该分组对应的类型,是否有header和footer
/// 根据布局类型决定是否需要以 header footer 作为布局的起始点与终点
/// 需要注意的是:当设置了 footerView 且 sectionFootersPinToVisibleBounds 为true时,footer具有悬停效果,此时footer的最终位置并不准确
/// decorationInsets: left,right 决定 decorationView 的 origin.x 以及 width
/// top,bottom 决定 decorationView 的 origin.y 以及 height
/// 改分组是否存在 header footer
let hasHeader = hasHaderAttr(at: section)
let hasFooter = hasFooterAttr(at: section)
/// 根据 header footer 设置垂直方向的增量
let topAdd = hasHeader ? 0 : (-secInsets.top)
var bottomAdd = hasFooter ? 0 : secInsets.bottom
/// 对于悬停footer 最终位置偏移需更改
if isPinToSectionFooters {
bottomAdd += sectionFooterViewSize(in: section).height
}
/// 承接装饰视图布局frame计算的临时变量
var frame: CGRect = .zero
/// 起始位置
frame.origin.y = first.frame.minY + topAdd
/// 结束位置
frame.size.height = last.frame.maxY - frame.minY + bottomAdd
/// 更新x,y
frame.origin.x = decInsets.left - contentInsets.left
frame.origin.y -= decInsets.top
/// 更新height, width
frame.size.height += (decInsets.top + decInsets.bottom)
frame.size.width = layoutWidth - decInsets.left - decInsets.right
/// 获取分组装饰视图 完成单组配置
let decAttr = UICollectionViewLayoutAttributes(forDecorationViewOfKind: kind, with: .firstIdx(at: section))
decAttr.frame = frame
decAttr.zIndex = -1
/// 添加到配置组里
decorationLayoutAttrs[section] = decAttr
}
}
辅助计算的一些私有方法:
// MARK: Private Methods
private extension CollectionDecorationViewFlowLayout {
/// 返回 当前分组装饰视图布局类型
/// - Parameters:
/// - section: 当前分组
func decorationViewLayoutType(at section: Int) -> CollectionDecorationViewLayoutType {
guard let type = delegate?.decorationViewLayoutType(at: section) else { return layoutType?(section) ?? .default }
return type
}
/// 返回 当前分组是否存在 header attr
/// - Parameters:
/// - section: 当前分组
func hasHaderAttr(at section: Int) -> Bool {
return headerViewLayoutAttributes(at: section)?.visible ?? false
}
/// 返回 当前分组 headerView attr
/// - Parameters:
/// - section: 当前分组
func headerViewLayoutAttributes(at section: Int) -> UICollectionViewLayoutAttributes? {
return layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, at: .firstIdx(at: section))
}
/// 返回 当前分组是否存在 footer attr
/// - Parameters:
/// - section: 当前分组
func hasFooterAttr(at section: Int) -> Bool {
return footerViewLayoutAttributes(at: section)?.visible ?? false
}
/// 返回 当前分组footerView attr
/// - Parameters:
/// - section: 当前分组
func footerViewLayoutAttributes(at section: Int) -> UICollectionViewLayoutAttributes? {
return layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, at: .firstIdx(at: section))
}
/// 返回 根据当前分组装饰视图布局类型获取起始位置的attr
/// - Parameters:
/// - section: 当前分组
func firstLayoutAttributes(at section: Int) -> UICollectionViewLayoutAttributes? {
switch decorationViewLayoutType(at: section) {
// contain header
case .default, .headerAndItems:
guard let header = headerViewLayoutAttributes(at: section), header.visible else {
return layoutAttributesForItem(at: .firstIdx(at: section))
}
return header
default:
return layoutAttributesForItem(at: .firstIdx(at: section))
}
}
/// 返回 根据当前分组装饰视图布局类型获取最终位置的attr
/// - Parameters:
/// - section: 当前分组
func lastLayoutAttributes(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
/// 开启footer悬停效果
if isPinToSectionFooters {
return layoutAttributesForItem(at: indexPath)
}
switch decorationViewLayoutType(at: indexPath.section) {
// contain footer
case .default, .footerAndItems:
guard let footer = footerViewLayoutAttributes(at: indexPath.section), footer.visible else {
return layoutAttributesForItem(at: indexPath)
}
return footer
default:
return layoutAttributesForItem(at: indexPath)
}
}
/// 返回 当前分组装饰视图类
/// - Parameters:
/// - section: 当前分组
func decorationViewClass(at section: Int) -> (UICollectionReusableView.Type)? {
guard let cls = delegate?.decorationViewReusableClass(at: section) else {
return reusableClass?(section)
}
return cls
}
/// 返回 当前分组装饰视图 注册类
/// - Parameters:
/// - section: 当前分组
private func decorationViewOfKind(at section: Int) -> String? {
return decorationViewClass(at: section)?.identifier
}
/// 返回 当前分组装饰视图 insets
/// left: 决定origin.x, left+right 决定 decorationView 宽度width
/// top: 决定origin.y, top+bottom 决定 decorationView 高度height
/// - Parameters:
/// - section: 当前分组
func decorationViewInsets(at section: Int) -> UIEdgeInsets {
guard let insets = delegate?.decorationViewExtendEdgeInsets(at: section) else {
return extendInsets?(section) ?? .zero
}
return insets
}
/// 返回 当前分组 insets
/// - Parameters:
/// - section: 当前分组
func sectionInsets(in section: Int) -> UIEdgeInsets {
var sectionInsets: UIEdgeInsets = sectionInset
if let delegate = collectionView?.delegate,
delegate.responds(to: #selector(UICollectionViewDelegateFlowLayout.collectionView(_:layout:insetForSectionAt:)))
{
sectionInsets = (delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, insetForSectionAt: section) ?? .zero
}
return sectionInsets
}
/// 返回 footerView size
/// - Parameters:
/// - section: 当前分组
func sectionFooterViewSize(in section: Int) -> CGSize {
var size: CGSize = footerReferenceSize
if let delegate = collectionView?.delegate,
delegate.responds(to: #selector(UICollectionViewDelegateFlowLayout.collectionView(_:layout:referenceSizeForFooterInSection:)))
{
size = (delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, referenceSizeForFooterInSection: section) ?? .zero
}
return size
}
}
private extension IndexPath {
static func lastIdx(at section: Int, items: Int) -> IndexPath {
IndexPath(item: items - 1, section: section)
}
static func firstIdx(at section: Int) -> IndexPath {
IndexPath(item: 0, section: section)
}
}
private extension UICollectionViewLayoutAttributes {
var visible: Bool {
return self.size.isValidated
}
}
private extension NSObject {
static var identifier: String {
return String(describing: "ObjcIdentifier.\(Self.self)")
}
}
private extension CGSize {
/// 判断一个 CGSize 是否存在 NaN
var isNaN: Bool {
return self.width.isNaN || self.height.isNaN
}
/// 判断一个 CGSize 是否存在 infinite
var isInf: Bool {
return self.width.isInfinite || self.height.isInfinite
}
/// 判断一个 CGSize 是否为空(宽或高为0)
var isEmpty: Bool {
return self.width <= 0 || self.height <= 0
}
/// 判断一个 CGSize 是否合法(例如不带无穷大的值、不带非法数字)
var isValidated: Bool {
return !self.isEmpty && !self.isInf && !self.isNaN
}
}
到这里,自定义CollectionDecorationViewFlowLayout实现不同分组使用不同装饰视图的实现已经完成,其余一些细节,代码里以详细说明,在此不再赘述。
四、使用案例:使用颜色和图片作为分组作为装饰视图
- 创建DecorationResuableView, 此类必须继承于UICollectionReusableView, 如果只使用颜色作为装饰视图,只需设置背景颜色即可,如下:
/*
颜色背景
*/
class SectionDecorationColorReuseableView : UICollectionReusableView {
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .cyan
self.layer.cornerRadius = 10
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
- 创建 colletionView 并使用 CollectionDecorationViewFlowLayout 作为 布局类:
let layout = CollectionDecorationViewFlowLayout()
lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
- 设置 layout 代理 或 closure:
layout.reusableClass = {section in
switch section {
case 0: return SectionDecorationColorReuseableView.self
case 1 : return nil
default:
return SectionDecorationColorReuseableView.self
}
}
layout.extendInsets = {section in
switch section {
case 0: return UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 15)
case 1 : return .zero
default:
return UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
}
}
layout.layoutType = {section in
switch section {
case 0: return .headerAndItems
case 1 : return .default
default:
return .footerAndItems
}
}
- 实现colletionView的delegate和dataSource方法即可,详细的代码就不贴出来了。
最终可实现效果如下(颜色及图片背景):
更多复杂效果,可通过自定义DecorationViewReusable类实现!
经验有限,存在的不足在所难免,还请大佬们不吝赐教,感激不尽!!!