creator-collection-view/doc/md/table-layout-6.md
2025-01-08 23:44:43 +08:00

355 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

通过二分查找优化 table-layout 性能
```ts
enum _yx_table_layout_supplementary_kinds {
HEADER = 'header',
FOOTER = 'footer',
}
export class YXTableLayout extends YXLayout {
/**
* 行高
*/
rowHeight: number | ((indexPath: YXIndexPath) => number) = 100
/**
* 内容上边距
*/
top: number = 0
/**
* 内容下边距
*/
bottom: number = 0
/**
* 节点之间间距
*/
spacing: number = 0
/**
* 区头高度
*/
sectionHeaderHeight: number | ((section: number) => number) = null
/**
* 区尾高度
*/
sectionFooterHeight: number | ((section: number) => number) = null
/**
* 钉住 header 的位置 ( header 吸附在列表可见范围内 )
*/
sectionHeadersPinToVisibleBounds: boolean = false
/**
* 钉住 footer 的位置 ( footer 吸附在列表可见范围内 )
*/
sectionFootersPinToVisibleBounds: boolean = false
/**
* 区头/区尾标识
*/
static SupplementaryKinds = _yx_table_layout_supplementary_kinds
protected originalHeaderRect: Map<number, math.Rect> = new Map() // 保存所有 header 的原始位置
protected originalFooterRect: Map<number, math.Rect> = new Map() // 保存所有 footer 的原始位置
// 为了优化查找,额外维护几个数组按类别管理所有的布局属性,空间换时间
protected allCellAttributes: YXLayoutAttributes[] = []
protected allHeaderAttributes: YXLayoutAttributes[] = []
protected allFooterAttributes: YXLayoutAttributes[] = []
prepare(collectionView: YXCollectionView): void {
// 设置列表的滚动方向(这套布局固定为垂直方向滚动)
collectionView.scrollView.horizontal = false
collectionView.scrollView.vertical = true
if (collectionView.scrollDirection === YXCollectionView.ScrollDirection.HORIZONTAL) {
// 由于这套布局规则只支持垂直方向布局,当外部配置了水平方向滚动时这里可以给个警告
warn(`YXTableLayout 仅支持垂直方向排列`)
}
// 清空一下布局属性数组
this.attributes = []
this.allCellAttributes = []
this.allHeaderAttributes = []
this.allFooterAttributes = []
this.originalHeaderRect.clear()
this.originalFooterRect.clear()
// 获取列表宽度
const contentWidth = collectionView.node.getComponent(UITransform).width
// 声明一个临时变量,用来记录当前所有内容的总高度
let contentHeight = 0
// 获取列表一共分多少个区
let numberOfSections = collectionView.getNumberOfSections()
// 为每条数据对应的生成一个布局属性
for (let section = 0; section < numberOfSections; section++) {
// 创建一个区索引
let sectionIndexPath = new YXIndexPath(section, 0)
// 通过区索引创建一个区头节点布局属性
let sectionHeaderHeight = 0
if (this.sectionHeaderHeight) {
sectionHeaderHeight = this.sectionHeaderHeight instanceof Function ? this.sectionHeaderHeight(section) : this.sectionHeaderHeight
}
if (sectionHeaderHeight > 0) {
let headerAttr = YXLayoutAttributes.layoutAttributesForSupplementary(sectionIndexPath, YXTableLayout.SupplementaryKinds.HEADER)
// 确定这个节点的位置
headerAttr.frame.x = 0
headerAttr.frame.width = contentWidth
headerAttr.frame.height = sectionHeaderHeight
headerAttr.frame.y = contentHeight
// 调整层级
headerAttr.zIndex = 1
// 重要: 保存布局属性
this.attributes.push(headerAttr)
this.originalHeaderRect.set(section, headerAttr.frame.clone())
this.allHeaderAttributes.push(headerAttr)
// 更新整体内容高度
contentHeight = headerAttr.frame.yMax
}
// 将 top 配置应用到每个区
contentHeight = contentHeight + this.top
// 获取这个区内的内容数量,注意这里传入的是 section
let numberOfItems = collectionView.getNumberOfItems(section)
for (let item = 0; item < numberOfItems; item++) {
// 创建索引,注意这里的 section 已经改为正确的 section 了
let indexPath = new YXIndexPath(section, item)
// 通过索引创建一个 cell 节点的布局属性
let attr = YXLayoutAttributes.layoutAttributesForCell(indexPath)
// 通过索引获取这个节点的高度
let rowHeight = this.rowHeight instanceof Function ? this.rowHeight(indexPath) : this.rowHeight
// 确定这个节点的位置
attr.frame.x = 0
attr.frame.width = contentWidth
attr.frame.height = rowHeight
attr.frame.y = contentHeight + (item > 0 ? this.spacing : 0)
// 重要: 保存布局属性
this.attributes.push(attr)
this.allCellAttributes.push(attr)
// 更新当前内容高度
contentHeight = attr.frame.yMax
}
// 高度补一个底部间距,跟 top 一样,也是应用到每个区
contentHeight = contentHeight + this.bottom
// 通过区索引创建一个区尾节点布局属性
let sectionFooterHeight = 0
if (this.sectionFooterHeight) {
sectionFooterHeight = this.sectionFooterHeight instanceof Function ? this.sectionFooterHeight(section) : this.sectionFooterHeight
}
if (sectionFooterHeight > 0) {
let footerAttr = YXLayoutAttributes.layoutAttributesForSupplementary(sectionIndexPath, YXTableLayout.SupplementaryKinds.FOOTER)
// 确定这个节点的位置
footerAttr.frame.x = 0
footerAttr.frame.width = contentWidth
footerAttr.frame.height = sectionFooterHeight
footerAttr.frame.y = contentHeight
// 调整层级
footerAttr.zIndex = 1
// 重要: 保存布局属性
this.attributes.push(footerAttr)
this.originalFooterRect.set(section, footerAttr.frame.clone())
this.allFooterAttributes.push(footerAttr)
// 更新整体内容高度
contentHeight = footerAttr.frame.yMax
}
}
// 重要: 设置内容区域总大小,只有确定了滚动区域的大小列表才能滚动
this.contentSize = new math.Size(contentWidth, contentHeight)
}
initOffset(collectionView: YXCollectionView): void {
// 列表首次刷新时,调整一下列表的偏移位置
collectionView.scrollView.scrollToTop()
}
layoutAttributesForElementsInRect(rect: math.Rect, collectionView: YXCollectionView): YXLayoutAttributes[] {
let result = this.visibleElementsInRect(rect, collectionView)
if (this.sectionHeadersPinToVisibleBounds == false && this.sectionFootersPinToVisibleBounds == false) {
return result // 不需要调整节点位置,直接返回就好
}
let numberOfSections = collectionView.getNumberOfSections()
let scrollOffset = collectionView.scrollView.getScrollOffset()
for (let index = 0; index < result.length; index++) {
const element = result[index];
if (element.elementCategory === 'Supplementary') {
if (this.sectionHeadersPinToVisibleBounds && element.supplementaryKinds === YXTableLayout.SupplementaryKinds.HEADER) {
const originalFrame = this.originalHeaderRect.get(element.indexPath.section)
element.frame.y = originalFrame.y
if (scrollOffset.y > originalFrame.y) {
element.frame.y = scrollOffset.y
}
const nextOriginalFrame = this.getNextOriginalFrame(element.indexPath.section, YXTableLayout.SupplementaryKinds.FOOTER, numberOfSections)
if (nextOriginalFrame) {
if (element.frame.yMax > nextOriginalFrame.y) {
element.frame.y = nextOriginalFrame.y - element.frame.height
}
}
}
if (this.sectionFootersPinToVisibleBounds && element.supplementaryKinds === YXTableLayout.SupplementaryKinds.FOOTER) {
let bottom = scrollOffset.y + collectionView.scrollView.view.height
const originalFrame = this.originalFooterRect.get(element.indexPath.section)
const previousOriginalFrame = this.getPreviousOriginalFrame(element.indexPath.section, YXTableLayout.SupplementaryKinds.HEADER)
element.frame.y = originalFrame.y
if (bottom < originalFrame.yMax) {
element.frame.y = bottom - element.frame.height
if (previousOriginalFrame) {
if (element.frame.y < previousOriginalFrame.yMax) {
element.frame.y = previousOriginalFrame.yMax
}
}
}
}
}
}
return result
}
shouldUpdateAttributesZIndex(): boolean {
return this.sectionHeadersPinToVisibleBounds || this.sectionFootersPinToVisibleBounds
}
shouldUpdateAttributesForBoundsChange(): boolean {
return this.sectionHeadersPinToVisibleBounds || this.sectionFootersPinToVisibleBounds
}
/**
* 获取 `section` 下一个 header 或者 footer 的位置
*/
protected getNextOriginalFrame(section: number, kinds: _yx_table_layout_supplementary_kinds, total: number) {
if (section >= total) { return null }
if (kinds === YXTableLayout.SupplementaryKinds.HEADER) {
let result = this.originalHeaderRect.get(section)
if (result) { return result }
return this.getNextOriginalFrame(section, YXTableLayout.SupplementaryKinds.FOOTER, total)
}
if (kinds === YXTableLayout.SupplementaryKinds.FOOTER) {
let result = this.originalFooterRect.get(section)
if (result) { return result }
return this.getNextOriginalFrame(section + 1, YXTableLayout.SupplementaryKinds.HEADER, total)
}
return null
}
/**
* 获取 `section` 前一个 header 或者 footer 的位置
*/
protected getPreviousOriginalFrame(section: number, kinds: _yx_table_layout_supplementary_kinds) {
if (section < 0) { return null }
if (kinds === YXTableLayout.SupplementaryKinds.HEADER) {
let result = this.originalHeaderRect.get(section)
if (result) { return result }
return this.getPreviousOriginalFrame(section - 1, YXTableLayout.SupplementaryKinds.FOOTER)
}
if (kinds === YXTableLayout.SupplementaryKinds.FOOTER) {
let result = this.originalFooterRect.get(section)
if (result) { return result }
return this.getPreviousOriginalFrame(section, YXTableLayout.SupplementaryKinds.HEADER)
}
return null
}
/**
* 抽出来一个方法用来优化列表性能
* 在优化之前,可以先看一下 @see YXLayout.layoutAttributesForElementsInRect 关于返回值的说明
* 对于有序列表来说,一般都是可以通过二分查找来进行优化
*/
protected visibleElementsInRect(rect: math.Rect, collectionView: YXCollectionView) {
if (this.attributes.length <= 100) { return this.attributes } // 少量数据就不查了,直接返回全部
let result: YXLayoutAttributes[] = []
// header 跟 footer 暂时不考虑,数据相对来说不算很多,直接全部返回
result.push(...this.allHeaderAttributes)
result.push(...this.allFooterAttributes)
// 关于 cell这里用二分查找来优化一下
// 首先通过二分先查出个大概位置
let midIdx = -1
let left = 0
let right = this.allCellAttributes.length - 1
while (left <= right && right >= 0) {
let mid = left + (right - left) / 2
mid = Math.floor(mid)
let attr = this.allCellAttributes[mid]
if (rect.intersects(attr.frame)) {
midIdx = mid
break
}
if (rect.yMax < attr.frame.yMin || rect.xMax < attr.frame.xMin) {
right = mid - 1
} else {
left = mid + 1
}
}
// 二分查找出错了,返回全部的布局属性
if (midIdx < 0) {
return this.attributes
}
// 把模糊查到这个先加进来
result.push(this.allCellAttributes[midIdx])
// 然后依次往前检查,直到超出当前的显示范围
let startIdx = midIdx
while (startIdx > 0) {
let idx = startIdx - 1
let attr = this.allCellAttributes[idx]
if (rect.intersects(attr.frame) == false) {
break
}
result.push(attr)
startIdx = idx
}
// 依次往后检查,直到超出当前的显示范围
let endIdx = midIdx
while (endIdx < this.allCellAttributes.length - 1) {
let idx = endIdx + 1
let attr = this.allCellAttributes[idx]
if (rect.intersects(attr.frame) == false) {
break
}
result.push(attr)
endIdx = idx
}
return result
}
}
```