1,糊一张装饰视图
装饰视图 Decoration View ,苹果的例子是一个 cell 贴一张背景图。
实际上,一个 section ,贴一张背景图,可以的。
苹果设计的非常灵活,背景图 layout 可以自由设置。
比如:一个 section 里面有很多单元格 item , 可以一个 section 的后面 ,放一张装饰背景图
感觉 layout 都自定义了,没什么具体的范式,根据需求做就好。苹果也没什么好讲
图中,一个 collectionView 的 section, 有 5 个 item,这个 section 后面贴一张背景图
装饰视图是 UICollectionViewLayout 的功能,不是 UICollectionView 的。
UICollectionView 的方法、代理方法 (delegate, datasource)都不涉及装饰视图。
UICollectionView 对装饰视图一无所知,UICollectionView 按照 UICollectionViewLayout 设置的渲染。
要用装饰视图,就要自定制 UICollectionViewLayout,也就是 UICollectionViewLayout 的子类。这个 UICollectionViewLayout 子类,可以添加属性、代理属性,通过设置代理协议方法,来自定制装饰视图。
先实现这个效果
图中,一个 collectionView 的 section, 有 5 个 item,这个 section 有一个阴影
简要说来,自定制的 layout 子类,实现一个装饰视图,五步:
步骤 1,
要有一个 UICollectionResuableView 的子类, 这个就是具体的装饰视图
class ShadowBg: UICollectionReusableView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.white
// 实现阴影,设置三个属性,就可以
layer.shadowOpacity = 1
layer.shadowColor = UIColor.shadowScore.cgColor
layer.shadowOffset = CGSize(width: ShadowFrame.rhs, height: ShadowFrame.bottom)
}
required init?(coder: NSCoder) {
fatalError("没实现")
}
}
步骤 2,
layout 中注册装饰视图。
有了装饰视图,组装在一起 (wire it up)
自定制的 layout 子类中,注册 UICollectionResuableView 的子类,也就是装饰视图。
调用 open func register(_ viewClass: AnyClass?, forDecorationViewOfKind elementKind: String)
方法。
一般在 override func prepare() {
方法中注册。
// 这里用了一个范型
class DecorationFlow<T: UICollectionReusableView>: UICollectionViewFlowLayout {
override func prepare() {
super.prepare()
// 还可以用一些设置
register(T.self, forDecorationViewOfKind: T.id)
}
步骤 3,
设置装饰视图的位置。
override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
方法,
设置装饰视图 UICollectionResuableView
的位置,该方法返回了装饰视图的布局属性。
override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard indexPath.section == 0, let collection = collectionView, elementKind == T.id else{
return nil
}
let attributes = DecorationLayoutAttributes(forDecorationViewOfKind: T.id, with: indexPath)
let totalWid = UI.width - VerticalTabBarInfo.tabBarWidth
let width = totalWid - FrontPageFrame.lhs - FrontPageFrame.rhs + ShadowFrame.rhs
// 单元格的数目可以是奇数,也可以是偶数,
// 通过 ceil 向上取整,保证单元格的数目是奇数的情况
let floor = ceil(Double(collection.numberOfItems(inSection: 0))/2.0)
let height = CGFloat(floor) * FrontPageFrame.itemHeight + ShadowFrame.bottom
attributes.frame = CGRect(x: FrontPageFrame.lhs, y: FrontPageFrame.headerH, width: width, height: height)
attributes.zIndex -= 1
return attributes
}
步骤 4,
重写 override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
方法,
该方法会返回给定区域内,所有视图 (单元格、补充视图(header footer)、装饰视图 ) 的布局属性。
当 collectionView 调用 layoutAttributesForElements
,他会提供 3 种视图的所有布局属性( 单元格、补充视图(header footer)、装饰视图)。自定制的装饰视图布局属性,也在这个时机插入。
这里要糊上装饰视图,layoutAttributesForElements
返回的布局属性数组,需含有调用 override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
方法中设置的布局属性。
这一步比较关键,collectionView 得到了足够的信息,显示装饰视图。
collectionView 对装饰视图是隔离的,一无所知。看到的 collectionView 的装饰视图,是自定制 layout 属性提供的。
注册了装饰视图,即创建了自定制的装饰视图实例。collectionView 会根据布局属性,放置好。
把上一步设置的装饰视图布局属性,交给 collectionView 使用
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var array = super.layoutAttributesForElements(in: rect)
guard let collection = collectionView, collection.numberOfSections > 0 else{
return array
}
let decorations = layoutAttributesForDecorationView(ofKind: T.id, at: IndexPath(item: 0, section: 0))
// 找到那一个 layoutAttributes, 添加到原来的 super.layoutAttributes 中
// 不会影响到我们正常使用 UICollectionViewDelegateFlowLayout
if let decorate = decorations, rect.intersects(decorate.frame){
array?.append(decorate)
}
return array
}
1.1 做一个加强,怎么传值给 Decoration View
Decoration View 能收到消息,才能适应多种情况,状态控制才能有效,根据状态改变 UI
三步走:
含 CollcetionView 的 controller -> layout -> layoutAttributes -> decorationView 装饰视图
实现图片背景效果
自定制 layout 与装饰视图也是隔离的。创建自定制布局属性对象 UICollectionViewLayoutAttributes 来传值,相当于找了一个信使。 给自定制的 layout 一个图片地址属性,
class DecorationLayoutAttributes: UICollectionViewLayoutAttributes {
var imgName: String?
}
传过去,就好了
控制器中,为指定的 collectionView 设置 layout 的图片 name ( 网络图,则为 url ) ,间接控制装饰视图的图片
class DecorationImgController: TabController {
var layout = DecorationImgFlow()
override func viewDidLoad() {
super.viewDidLoad()
layout.imgName = "bg"
// ...
layoutAttributesForDecorationViewOfKind:
中配置,
override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard indexPath.section == 0, let collection = collectionView, elementKind == ImageBg.id else{
return nil
}
let attributes = DecorationLayoutAttributes(forDecorationViewOfKind: ImageBg.id, with: indexPath)
// ...
// 通过属性,传递外部设置的装饰视图实际图片
attributes.imgName = imgName
// ...
最后,
把自定制 LayoutAttributes 的图片 url 传递给装饰视图, 有 override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes)
方法。
当 collectionView 配置装饰视图的时候,会调用该方法。layoutAttributes
作为参数,取出 imgName
属性使用,就可以了
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
if let attribute = layoutAttributes as? DecorationLayoutAttributes{
if let name = attribute.imgName{
bg.image = UIImage(named: name)
}
}
}
2. 通过 Core Graphics, 画一个阴影
除了直接设置图层阴影,也可以画一个阴影
色块出阴影
当前的绘图上下文,阴影设置后,填充的颜色,就会产生对应的阴影
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let frame = bounds.inset(by: UIEdgeInsets(top: 0, left: 0, bottom: ShadowFrame.bottom, right: ShadowFrame.rhs))
let rect = UIBezierPath(roundedRect: frame, cornerRadius: ShadowFrame.corn)
let shadow = UIColor.shadowCompare
context.setShadow(offset: CGSize(width: ShadowFrame.rhs, height: ShadowFrame.bottom), blur: 12, color: shadow.cgColor)
UIColor.white.setFill()
rect.fill()
}
换一个阴影效果
isOdd
的 didSet
方法中,重新绘制
class ShadowBgSecond: UICollectionReusableView {
var isOdd: Bool = false{
didSet{
if isOdd{
setNeedsDisplay()
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clear
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let frame = bounds.inset(by: UIEdgeInsets(top: 0, left: 0, bottom: ShadowFrame.bottom, right: ShadowFrame.rhs))
let rect: UIBezierPath
if isOdd{
// 是奇数,三个色块,拼在一起
// 下面的一个单元格,上面剩余的单元格,两块间过渡的圆弧
let upFrame = frame.inset(by: UIEdgeInsets(top: 0, left: 0, bottom: MusicLayout.itemHeight, right: 0))
rect = UIBezierPath(roundedRect: upFrame, byRoundingCorners: [.topLeft, .topRight, .bottomRight], cornerRadii: CGSize(width: ShadowFrame.corn, height: ShadowFrame.corn))
let downFrame = CGRect(origin: CGPoint(x: upFrame.minX, y: upFrame.maxY), size: CGSize(width: MusicLayout.doubleItemWidth * 0.5, height: MusicLayout.itemHeight))
rect.append(UIBezierPath(roundedRect: downFrame, byRoundingCorners: [.bottomLeft, .bottomRight], cornerRadii: CGSize(width: ShadowFrame.corn, height: ShadowFrame.corn)))
let midArc = UIBezierPath()
let midP = CGPoint(x: upFrame.midX, y: upFrame.maxY)
midArc.move(to: midP)
midArc.addLine(to: midP.v(ShadowFrame.corn))
midArc.addArc(withCenter: midP.offset(ShadowFrame.corn, offsetV: ShadowFrame.corn), radius: ShadowFrame.corn, startAngle: CGFloat.pi , endAngle: CGFloat.pi * 1.5, clockwise: true)
midArc.close()
rect.append(midArc)
}
else{
// 是偶数,单独的一个色块
rect = UIBezierPath(roundedRect: frame, cornerRadius: ShadowFrame.corn)
}
let shadow = UIColor.shadowScore
context.setShadow(offset: CGSize(width: ShadowFrame.rhs, height: ShadowFrame.bottom), blur: 12, color: shadow.cgColor)
UIColor.white.setFill()
rect.fill()
}
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
if let attribute = layoutAttributes as? DecorationLayoutAttributes{
isOdd = attribute.isOdd
}
}
}
要设置对应的 UICollectionViewLayoutAttributes
, 具体与图片设置类似
3, 补充
网络图,要处理下
要判断 collection.numberOfSections > 0
, 使用网络数据的 collectionView , 一开始没有数据,数据请求回来前,不存在实际的 section, indexPath 也没有。
直接取 indexPath ,因为不存在,会崩溃
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var array = super.layoutAttributesForElements(in: rect)
// 添加这一段
guard let collection = collectionView, collection.numberOfSections > 0 else{
return array
}
// ...
更加深入自定制 CollectionView 的 layout, 还可以通过设置代理协议方法,自由布局相关视图。
由控制器的内容,算出单元格具体的尺寸信息,传递给 Custom CollectionView Layout
通过 Core Graphics, 绘制阴影,常用的技巧有
切换绘图上下文
当前绘图上下文的保存与恢复
context.saveGState()
context.restoreGState()
通过透明图层,组合多个色块的阴影
context.beginTransparencyLayer(auxiliaryInfo: nil)
context.endTransparencyLayer()