UICollectionViewLayoutInvalidationContext性能优化 详细流程图 + 范例

起步基础

  • UICollectionViewLayout 基本使用

  • UICollectionViewLayoutAttributes

Attributes赋值

这里泛指了以下两个主函数,就不在赘述两个功能,以及 UICollectionViewLayoutAttributes 需处理的变量。

1
2
3
4
5
6
7
8
9
10
11
class AutoSizingLayout: UICollectionViewLayout {

override func prepare() {
super.prepare()
}

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return attributes
}

}

Without Invalidating

先来看看在 没有需要重新改变Attributes 下的流程(以下简称 配置流程):

Without Invalidating

data reload时,prepare计算一次,layoutAttributesForElements调用多次直到,系统已经有所有IndexPath的atrribute,就不会在调用这些functiom,直到collectionView reload。

With Invalidating

现在我们把失效的概念加进来

强制失效 UICollectionViewLayout.invalidateLayout()

invalidateLayout()可随时呼叫,他会将所有系统已取得的 Attribute 全部标记为 invalid 并舍弃。

准确的update时机并不是调用后,而是在下一次 layout 的 update Cycle里后,重新调用prepare. 堆栈如图:

https://developer.apple.com/documentation/uikit/uicollectionviewlayout/1617728-invalidatelayout

若有overrider此方法,必须call super.invalidateLayout()

条件失效 UICollectionViewLayout.shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool

https://developer.apple.com/documentation/uikit/uicollectionviewlayout/1617781-shouldinvalidatelayout

默认回传 false。

overrider后,可借由传入的 newBounds 判断是否需要 invalidLayout,若回传 true 则跟 InvalidateLayout()之后的流程(堆栈)相同。

例如内容下半部,需要不断更新Attribute

1
2
3
4
5
6
7
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
guard let collectionView = collectionView else { return false }
if newBounds.maxY > contentSize.height / 2 {
return true
}
return false
}

newBounds: CGRect

此bounds的触发时机,为collectionView可视范围改动时(contentOffset Change)

为了简化流程图,我们将固定一起出现的这几个步骤,划成一个:

UICollectionViewLayoutInvalidationContext

(以下简称 InvalidationContext)
https://developer.apple.com/documentation/uikit/uicollectionviewlayoutinvalidationcontext

这的context跟出现在其他地方的Context上下文概念差不多,先借由一个function的参数,对此上下文进行设置,回传后再下一个function对设置的内容进行处理。

基于系统『原生』的 InvalidationContext 失效layout

这里再多覆写了两个函数

1
2
3
4
5
override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
let invalidationContext = super.invalidationContext(forBoundsChange: newBounds)
return invalidationContext
}

  • invalidateLayout(with:)

    根据上一部处理好的逻辑或 『失效标记』 做属性处理,必调用super.invalidateLayout(with: invalidationContext)

    1
    2
    3
    override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) {
    //以context的资讯,做些update
    }

系统提供可用的失效标记,无任何标记将会重新进入『配置流程』

这些标记的功用,是在第一个function根据bounds作上标记,在第二个funciton中可以根据以下对应的变数取得当初的标记,做对应的『局部属性预处理』。

有标记可做 局部属性预处理 ,并会被重新询问 Attribute

重新询问 Attribute

例如:若滑超过 1/2 Y,使 row 17 失效。

1
2
3
4
5
6
7
8

override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
let invalidationContext = super.invalidationContext(forBoundsChange: newBounds)
if newBounds.maxY > contentSize.height {
invalidationContext.invalidateItems(at: [IndexPath(row: 17, section: 0)])
}
return invalidationContext
}

覆写的此function就会被调用并询问 row17 的attribute

1
2
3
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return row17's attribute
}

局部属性预处理

局部属性预处理 其实就是为了上一步 『重新询问 Attribute』这块做预先计算,

例如:若滑超过 1/2 Y,使 某Decoration失效。

1
2
3
4
5
6
7
8
9
10
11
override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
let invalidationContext = super.invalidationContext(forBoundsChange: newBounds)
if newBounds.maxY > contentSize.height {
invalidationContext.invalidateDecorationElements(
ofKind: "Footer",
at: [IndexPath(item: 1, section: 0)]
)
}
return invalidationContext
}

并在 invalidateLayout(with:) 从context里查询是否对应的Decoration包含在失效名单内,并提前计算好心的Attribute存在持有变量

1
2
3
4
5
6
override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) {
let invalidationContext = context
if let dic = context.invalidatedDecorationIndexPaths, let idx = dic["Footer"] {
prepareFooterViewAttributes()
}
}

在询问的时候,将预先计算好的Attribute 回传

1
2
3
4
5
6
7

override public func layoutAttributesForDecorationView(
ofKind elementKind: String,
at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return self.footerLayoutAttributes
}

特殊标记 (get-only)

这两个特殊标记是会在触发 collectionView.reloadData()时会被系统自动启用,不能自己设置,并且仍会重新进入『配置流程』。

基于系统『自定义』的 InvalidationContext 失效layout

第一步当然是写一个继承UICollectionViewLayoutInvalidationContext的类,并且在UICollectionViewLayout类里覆写以下

1
2
3
override class var invalidationContextClass: AnyClass {
return InvalidationContext子类名.self
}

自定义 InvalidationContext 的好处不外乎就是能自己增加字段,能更清晰也更有逻辑的衔接前后两个函数

例如:沿用前面的例子,但InvalidationContext为自定义,增加一个Bool,判断哪部分需要失效或是需要被标记

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 LayoutInvalidationContext: UICollectionViewLayoutInvalidationContext {
var invalidateFooter = false
}

override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
let invalidationContext = super.invalidationContext(forBoundsChange: newBounds) as! LayoutInvalidationContext
guard let collectionView = collectionView else { return invalidationContext }
let originChanged = !collectionView.bounds.origin.equalTo(newBounds.origin)
if originChanged && newBounds.maxY > contentSize.height {
invalidationContext.invalidateFooter = true
}
return invalidationContext
}

override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) {
let invalidationContext = context as! LayoutInvalidationContext
if invalidationContext.invalidateFooter {
prepareFooterViewAttributes()
invalidationContext.invalidateDecorationElements(
ofKind: "Footer",
at: [IndexPath(item: 1, section: 0)]
)
}
super.invalidateLayout(with: invalidationContext)
}

Comments

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×