# 前言
最近加班比较频繁,导致没啥时间更新,距离上次写博客还是上次了,感觉过了好一阵子呀,怎么说呢,挣钱嘛,不寒碜。好了,回归正题,最近接了一个新的案子,聊天相关的,聊天数据嘛,毋庸置疑是十分庞大的,所以也要用到循环列表,但之前写的那个循环列表 (CocosCreator 之垂直循环滚动列表) 肯定是无法满足需求的呀,那么咋办呢,没办法呀,需要再写一个进阶版、功能更复杂亿点点、可支持多预制复用的循环列表呗,GoGoGo!!!
# 需求分析
- 需要支持多预制的循环复用
既然要循环复用,那么肯定还是要用对象池的,多预制的话,就用一个对象池 Map,预制体名字为 key,value 为 pool,在更新显示时传入对应的预制体名数组就好啦~像这样:
export class UICompositeVerticalLoopScrollViewComp extends Component { | |
private _nodePoolMap: Map<string, ObjectPool<UICompositeScrollViewItemGetter>> = new Map<string, ObjectPool<UICompositeScrollViewItemGetter>>(); | |
public resetList(namesList: string[]) { | |
... | |
} | |
} |
- 预制体需要支持动态更新高度
由于聊天文本的长度会存在差别,当文本超过一定长度时就要支持换行功能,这就导致了预制体的高度需要进行动态更新,但总所周知,为了性能考虑,大部分游戏引擎的渲染刷新策略基本都是在当前帧给一个 dirty 标记,然后在下一帧去统一将 dirty 标记的对象去刷新渲染的,因此导致了如果你想要在当前帧获取刷新后的高度的话就必须调用强制刷新节点的方法,这就必然会导致性能的浪费,但以聊天来说的话,通常情况下只有在初始化的时候回去要求同步高度值,之后只要把这个值记录下来更新位置,不需要在执行强制刷新函数就好了,因此在组件上提供一个重置 Size 的函数,供外部刷新吧。
export class UICompositeVerticalLoopScrollViewComp extends Component { | |
public OnResizeItem: (item: Node, nodeKey: string, idx: number) => void; | |
private asyncShowListPos(startIdx: number, endIdx: number) { | |
... | |
} | |
} | |
export class xxxClass extends Component { | |
public resetContentSize(txt: string): math.Size { | |
this._itemLab.string = showStr; | |
// 用到了 cocos 文本的自动设置宽高,但下一帧才会刷新,因此调用强制刷新函数 | |
this._itemLab.updateRenderData(true); | |
return this._uiTrans.contentSize; | |
} | |
} |
注意一定要当前帧去刷新大小,因为如果组件内延迟一帧去获取大小的话,滚动条在滚动时会频繁去执行下一帧刷新位置的方法,这样会导致逻辑变得更加混乱,算了,还是浪费点性能当前刷新吧 hhh
聊天列表需要自下到上进行刷新↑
又于聊天列表基本都是自下向上刷新的,因此需要支持反方向刷新功能支持 gif 表情
github 上找个 gif 解析库,然后稍微操作一番(以后有空再写 x
# 设计思路
与之前循环列表不同的是,由于之前的预制高度是固定不变的,因此可以通过头索引 (StartIdx) 以及遮罩的高度来求出需要显示的预制体的数量,但现在我们的高度是会动态变化的,一个头索引肯定满足不了要求,因此需要再加一个尾索引 (EndIdx),当 StartIdx 或者 EndIdx 超出遮罩范围时回收预制并进行索引的 ++ 和 --,然后再根据头尾索引对遮罩范围内的预制进行 size 计算,补充高度至超出遮罩高度,刷新 content 的高度,然后再进行位置的校正就好啦~实现后的效果大概就像这样:
# 代码实现
- UICompositeScrollViewPos
UI 位置接口,用于记录当前预制坐标、高度、是否初始化过大小以及是否需要进行位置矫正信息
export interface UICompositeScrollViewPos { | |
pos: Vec3; | |
height: number; | |
resized: boolean; | |
isAsyncPos: boolean; | |
} |
- UICompositeScrollViewItemGetter
复用预制体上需要挂载的脚本,用于获取当前预制体的宽高,以及判断是否超出遮罩范围
export class UICompositeScrollViewItemGetter extends Component { | |
private _maskSize: math.Size; | |
public set MaskSize(size: math.Size) { | |
this._maskSize = size; | |
} | |
private _index: number; | |
public get Index(): number { | |
return this._index; | |
} | |
public set Index(idx: number) { | |
this._index = idx; | |
} | |
private _trans: UITransform; | |
getSize(): math.Size { | |
if (!this._trans) | |
this._trans = this.getComponent(UITransform); | |
return this._trans.contentSize; | |
} | |
outSizeCheckWithUp2Bottom(isTop: boolean, offsetY: number): boolean { | |
//↓ | |
var y = Math.abs(this.node.position.y); | |
var height = this.getSize().height; | |
return isTop ? y + height < offsetY : y - height > offsetY + this._maskSize.height; | |
} | |
outSizeCheckWithBottom2Up(isTop: boolean, offsetY: number): boolean { | |
//↑ | |
var y = Math.abs(this.node.position.y); | |
var height = this.getSize().height; | |
return isTop ? y - height > offsetY : y < offsetY - this._maskSize.height; | |
} | |
show() { | |
this.node.active = true; | |
} | |
hide() { | |
this.node.active = false; | |
this.Index = -1; | |
} | |
clear() { | |
this.hide(); | |
this.node.destroy(); | |
} | |
} |
注意,这里将预制体的锚点设置为了 (0.5,1), 因此判断是否超出预制范围区域是以此来计算的
- UICompositeVerticalLoopScrollViewComp
重点来啦~业务组件,原理在上边,具体实现看看代码注释吧~
// 枚举,决定从↑到↓,还是从↓到↑ | |
enum eCompositeVerticalDir { | |
Top2Bottom = 0, | |
Bottom2Top, | |
} | |
Enum(eCompositeVerticalDir); | |
@ccclass('UICompositeVerticalLoopScrollViewComp') | |
@requireComponent(ScrollView) | |
export class UICompositeVerticalLoopScrollViewComp extends Component { | |
@property({ type: eCompositeVerticalDir }) | |
dir: eCompositeVerticalDir = eCompositeVerticalDir.Top2Bottom; | |
@property([Prefab]) | |
itemPrefs: Prefab[] = []; | |
@property(CCFloat) | |
spacing: number = 0; | |
// 预制是否需要重置节点宽高 | |
@property(CCBoolean) | |
isNeedResize: boolean = false; | |
public OnRefreshItem: (item: Node, nodeKey: string, idx: number) => void; | |
public OnResizeItem: (item: Node, nodeKey: string, idx: number) => void; | |
// 比遮罩高多少的预生成区域 | |
public OverHeight: number = 0; | |
public get StartIdx(): number { | |
return this._startIdx; | |
} | |
public get EndIdx(): number { | |
return this._endIdx; | |
} | |
private _nodePoolMap: Map<string, ObjectPool<UICompositeScrollViewItemGetter>> = new Map<string, ObjectPool<UICompositeScrollViewItemGetter>>(); | |
//item 高度 Map,key=id ,value=height | |
private _itemSizeMap: Map<number, number> = new Map<number, number>(); | |
// 预制体默认大小 | |
private _prefabSizeMap: Map<string, math.Size> = new Map<string, math.Size>(); | |
//item 名字列表 | |
private _itemNameList: string[] = []; | |
// 位置数据列表 | |
private _itemPosList: UICompositeScrollViewPos[] = []; | |
// 当前显示的预制列表 | |
private _curNodeList: UICompositeScrollViewItemGetter[] = []; | |
private _scrollView: ScrollView; | |
private _contentTrans: UITransform; | |
private _maskSize: math.Size; | |
private _preScrollY: number; | |
private _startIdx: number; | |
private _endIdx: number; | |
private _curContentHeight: number; | |
private set curContentHeight(height: number) { | |
this._curContentHeight = height; | |
this._contentTrans.setContentSize(new math.Size(this._maskSize.width, height)); | |
} | |
public init() { | |
this._scrollView = this.getComponent(ScrollView); | |
this._maskSize = this.getComponentInChildren(Mask).getComponent(UITransform).contentSize; | |
this._contentTrans = this.getComponent(ScrollView).content.getComponent(UITransform); | |
var isTop2Bottom = this.dir == eCompositeVerticalDir.Top2Bottom; | |
this._contentTrans.anchorY = isTop2Bottom ? 1 : 0; | |
this.curContentHeight = 0; | |
this.recycleAll(); | |
this.itemPrefs.forEach(prefab => { | |
var key = prefab.name; | |
// 记录 prefab 初始大小,可能会动态改变 | |
this._prefabSizeMap.set(key, prefab.data.getComponent(UITransform).contentSize); | |
// 对象池初始化 | |
if (!this._nodePoolMap.has(key)) { | |
let pool = new ObjectPool<UICompositeScrollViewItemGetter>(() => { | |
let node = instantiate(prefab); | |
node.setParent(this._contentTrans.node); | |
node.getComponent(UITransform).setAnchorPoint(0.5, 1); | |
node.setRotationFromEuler(0, 0, 0); | |
node.setScale(1, 1, 1); | |
var getter = node.getComponent(UICompositeScrollViewItemGetter) ?? node.addComponent(UICompositeScrollViewItemGetter); | |
node.active = false; | |
return getter; | |
}, getter => getter.hide(), getter => getter.clear(), 2); | |
this._nodePoolMap.set(key, pool); | |
} | |
}); | |
} | |
// 根据头尾索引,刷新当前区域显示 | |
public refreshCurShow() { | |
// 回收 | |
for (let i = 0; i < this._curNodeList.length; i++) { | |
var getter = this._curNodeList[i]; | |
this.recycleNode(getter); | |
} | |
this._curNodeList.length = 0; | |
if (this._startIdx <= 0) { | |
this._startIdx = this._endIdx = -1; | |
} else { | |
this._endIdx = this._startIdx - 1; | |
} | |
this.refreshNewShowItems(); | |
} | |
// 从尾端插入 | |
public pushList(namesList: string[]) { | |
var fromIdx = this._itemNameList.length; | |
this._itemNameList = this._itemNameList.concat(namesList); | |
this._preScrollY = this._scrollView.getScrollOffset().y; | |
this.pushDefaultPosList(fromIdx); | |
this.refreshNewShowItems(); | |
} | |
// 从头部插入 | |
public unshiftList(namesList: string[]) { | |
var endIdx = namesList.length - 1; | |
this._itemNameList = namesList.concat(this._itemNameList); | |
this._preScrollY = this._scrollView.getScrollOffset().y; | |
this.unshiftDefaultPosList(endIdx); | |
this.refreshCurShow(); | |
} | |
// 根据预制体名称列表,全部刷新 | |
public resetList(namesList: string[]) { | |
this.recycleAll(); | |
this._itemNameList = namesList; | |
// 同步位置列表,设置默认 contentsize | |
this._itemSizeMap.clear(); | |
this._itemPosList.length = 0; | |
this.pushDefaultPosList(0); | |
this.refreshNewShowItems(); | |
this._contentTrans.node.position = new Vec3(0, -this._maskSize.height / 2 * this.getScrollDir(), 0); | |
this._preScrollY = this._scrollView.getScrollOffset().y; | |
this.node.on(ScrollView.EventType.SCROLLING, this.onScrolling, this); | |
this.node.on(ScrollView.EventType.SCROLL_ENDED, this.onScrollEnded, this); | |
} | |
private recycleAll() { | |
this._nodePoolMap.forEach(pool => { | |
pool?.recycleAll(); | |
}) | |
this._startIdx = this._endIdx = -1; | |
this._preScrollY = 0; | |
this._itemNameList.length = 0; | |
this._curNodeList.length = 0; | |
this._itemPosList.length = 0; | |
this.curContentHeight = 0; | |
this._itemSizeMap.clear(); | |
this._scrollView.stopAutoScroll(); | |
this.node.off(ScrollView.EventType.SCROLLING, this.onScrolling, this); | |
this.node.off(ScrollView.EventType.SCROLL_ENDED, this.onScrollEnded, this); | |
} | |
private onScrolling(scrollView: ScrollView) { | |
var scrollY = scrollView.getScrollOffset().y; | |
this.refreshItemsByScrollY(scrollY); | |
} | |
private onScrollEnded(scrollView: ScrollView) { | |
this._preScrollY = this._scrollView.getScrollOffset().y; | |
} | |
private refreshItemsByScrollY(scrollY: number) { | |
// 判断上滑还是下滑 | |
if (scrollY > this._preScrollY) { | |
//console.log ("上拉 ↑"); | |
if (this.isTop2Bottom()) { | |
if (this._endIdx >= this._itemNameList.length - 1) | |
return; | |
// 回收 | |
this.recycleItemsWithBack2FrontSearch(scrollY, true, true); | |
// 新增 | |
this.refreshNewShowItems(); | |
} else { | |
if (this._startIdx <= 0) | |
return; | |
// 回收 | |
var calY = this._curContentHeight - scrollY; | |
this.recycleItemsWithFront2BackSearch(calY, true, false); | |
// 新增 | |
this.refreshBackShowItems(); | |
} | |
} else { | |
//console.log ("下拉↓"); | |
if (this.isTop2Bottom()) { | |
if (this._startIdx <= 0) | |
return; | |
// 回收 | |
this.recycleItemsWithFront2BackSearch(scrollY, false, true); | |
// 新增 | |
this.refreshBackShowItems(); | |
} else { | |
if (this._endIdx >= this._itemNameList.length - 1) | |
return; | |
// 回收 | |
var calY = this._curContentHeight - scrollY; | |
this.recycleItemsWithBack2FrontSearch(calY, false, false); | |
// 新增 | |
this.refreshNewShowItems(); | |
} | |
} | |
this._preScrollY = scrollY; | |
} | |
private unshiftDefaultPosList(endIdx: number) { | |
var totalHeight = this._curContentHeight; | |
var dir = this.getScrollDir(); | |
// 以默认高度添加位置 | |
var newHeight = 0; | |
var newPosList = []; | |
for (let i = 0; i <= endIdx; i++) { | |
var name = this._itemNameList[i]; | |
var height = this.getDefaultPrefabSize(name).height; | |
var posData = <UICompositeScrollViewPos>{ | |
pos: this.getNodePos(newHeight, height), | |
height: height, | |
resized: true, | |
isAsyncPos: true, | |
} | |
newPosList.push(posData); | |
newHeight += height + this.spacing; | |
this._itemSizeMap.set(i, height); | |
} | |
this._itemPosList = newPosList.concat(this._itemPosList); | |
if (this.isNeedResize) { | |
// 需要知道实际的高度 | |
var getters = []; | |
for (let i = 0; i <= endIdx; i++) { | |
var key = this._itemNameList[i]; | |
let getter = this.allocateByName(key); | |
getter.Index = i; | |
this.resizeItemNode(getter.node, getter.Index); | |
// 放到屏幕外,保证看不见 | |
getter.node.position = new Vec3(100000, 100000); | |
getter.show(); | |
getters.push(getter); | |
} | |
// 新增的位置刷新重排 | |
newHeight = 0; | |
for (let i = 0; i < getters.length; i++) { | |
let getter = getters[i]; | |
var data = this._itemPosList[i]; | |
var realHeight = getter.getSize().height; | |
data.pos = this.getNodePos(newHeight, realHeight); | |
data.height = realHeight; | |
data.resized = true; | |
data.isAsyncPos = true; | |
newHeight += realHeight + this.spacing; | |
this._itemSizeMap.set(i, realHeight); | |
// 用完要回收 | |
this.recycleNode(getter); | |
} | |
} | |
// 旧的要重算位置 | |
var addHeightY = newHeight * dir; | |
for (let i = endIdx + 1; i < this._itemPosList.length; i++) { | |
var data = this._itemPosList[i]; | |
data.pos = new Vec3(data.pos.x, data.pos.y + addHeightY); | |
this._itemSizeMap.set(i, data.height); | |
} | |
this.curContentHeight = Math.max(this._maskSize.height, totalHeight + newHeight); | |
} | |
private pushDefaultPosList(fromIdx: number) { | |
var totalHeight = this._curContentHeight; | |
for (let i = fromIdx; i < this._itemNameList.length; i++) { | |
var name = this._itemNameList[i]; | |
var height = this.getDefaultPrefabSize(name).height; | |
var posData = <UICompositeScrollViewPos>{ | |
pos: this.getNodePos(totalHeight, height), | |
height: height, | |
resized: false, | |
} | |
this._itemPosList.push(posData); | |
totalHeight += height + this.spacing; | |
this._itemSizeMap.set(i, height); | |
} | |
this.curContentHeight = Math.max(this._maskSize.height, totalHeight); | |
} | |
private getNodePos(totalHeight: number, height: number): Vec3 { | |
if (this.isTop2Bottom()) { | |
return new Vec3(0, -totalHeight); | |
} else { | |
return new Vec3(0, totalHeight + height); | |
} | |
} | |
private getScrollDir(): number { | |
return this.isTop2Bottom() ? -1 : 1; | |
} | |
private isTop2Bottom(): boolean { | |
return this.dir == eCompositeVerticalDir.Top2Bottom; | |
} | |
private getDefaultPrefabSize(name: string): math.Size { | |
if (!this._prefabSizeMap.has(name)) { | |
console.error(`不存在该类型的预制体池子:${name}`) | |
return new math.Size(0, 0); | |
} | |
return this._prefabSizeMap.get(name); | |
} | |
private allocateByName(name: string): UICompositeScrollViewItemGetter { | |
if (!this._nodePoolMap.has(name)) { | |
console.error(`不存在该类型的预制体池子:${name}`) | |
return null | |
} | |
var getter = this._nodePoolMap.get(name).allocate(); | |
return getter; | |
} | |
private recycleNode(getter: UICompositeScrollViewItemGetter) { | |
var key = getter.node.name; | |
if (!this._nodePoolMap.has(key)) { | |
console.error(`不存在该类型的预制体池子:${key}`) | |
return null | |
} | |
this._nodePoolMap.get(key).recycle(getter); | |
} | |
private getCurHeight(idx: number): number { | |
return this._itemSizeMap.get(idx); | |
} | |
private refreshItemNode(node: Node, idx: number) { | |
node.setPosition(this._itemPosList[idx].pos); | |
//console.log (`刷新节点:${idx} 位置为:${this._itemPosList [idx].pos} 高度为:${this._curContentHeight}`); | |
this.OnRefreshItem && this.OnRefreshItem(node, node.name, idx); | |
} | |
private resizeItemNode(node: Node, idx: number) { | |
var posData = this._itemPosList[idx]; | |
posData.resized = true; | |
posData.isAsyncPos = false; | |
this.OnResizeItem && this.OnResizeItem(node, node.name, idx); | |
} | |
// 刷新初次显示 | |
private refreshNewShowItems() { | |
// 生成刚好超过遮罩高度的预制体 | |
// 当前显示的高度 | |
var curHeight: number; | |
if (this._startIdx == this._endIdx && this._startIdx < 0) { | |
// 初始化 | |
curHeight = 0; | |
this._startIdx = 0; | |
} else { | |
if (this._startIdx < 0 || this._endIdx >= this._itemNameList.length - 1) { | |
return; | |
} | |
curHeight = Math.abs(this._itemPosList[this._endIdx].pos.y - this._itemPosList[this._startIdx].pos.y); | |
} | |
if (curHeight >= this.getShowHeight()) { | |
return; | |
} | |
//console.log (`开始:${this._startIdx}, 结束:${this._endIdx}`); | |
//console.log (`NewShowItems 开始添加理论新增`); | |
var preEndIdx = Math.max(0, this._endIdx); | |
while (this._endIdx < this._itemNameList.length - 1 && curHeight < this.getShowHeight()) { | |
this._endIdx++; | |
var getter = this.allocateAndShowGetter(this._endIdx); | |
this._curNodeList.push(getter); | |
curHeight += this.getCurHeight(getter.Index); | |
} | |
//console.log (`开始:${this._startIdx}, 结束:${this._endIdx}`); | |
// 预制节点是否需要刷新,如果需要则要重新布局 | |
if (this.isNeedResize && preEndIdx != this._endIdx) { | |
this.asyncShowListPos(preEndIdx, this._endIdx); | |
this.refreshNewShowItems(); | |
} | |
} | |
// 刷新已经初始化过后的显示 | |
private refreshBackShowItems() { | |
// 当前显示的高度 | |
var curHeight: number; | |
if (this._startIdx == this._endIdx && this._endIdx < 0) { | |
// 初始化 | |
curHeight = 0; | |
this._startIdx = 0; | |
} else { | |
if (this._startIdx < 0 || this._endIdx >= this._itemNameList.length - 1) | |
return; | |
curHeight = Math.abs(this._itemPosList[this._endIdx].pos.y - this._itemPosList[this._startIdx].pos.y); | |
} | |
if (curHeight >= this.getShowHeight()) | |
return; | |
//console.log (`开始:${this._startIdx}, 结束:${this._endIdx}`); | |
//console.log (`BackShowItems 开始添加理论新增`); | |
while (this._startIdx > 0 && curHeight < this.getShowHeight()) { | |
this._startIdx--; | |
var getter = this.allocateAndShowGetter(this._startIdx); | |
this._curNodeList.unshift(getter); | |
curHeight += this.getCurHeight(getter.Index); | |
} | |
//console.log (`开始:${this._startIdx}, 结束:${this._endIdx}`); | |
} | |
private getShowHeight(): number { | |
return this._maskSize.height + this.OverHeight; | |
} | |
private asyncShowListPos(startIdx: number, endIdx: number) { | |
//console.log (`开始位置矫正 ${startIdx}--${endIdx}`) | |
var startPosY: number; | |
var diffValue = 0; | |
// 理论的初始位置 | |
var startIdx = startIdx - this._startIdx; | |
var endIdx = endIdx - this._startIdx; | |
for (let i = startIdx; i <= endIdx; i++) { | |
var getter = this._curNodeList[i]; | |
if (!getter) | |
continue; | |
var posData = this._itemPosList[getter.Index]; | |
if (posData.isAsyncPos) | |
continue; | |
// 理论高度 | |
var defaultHeight = this.getCurHeight(getter.Index); | |
// 实际高度 | |
var realHeight = getter.getSize().height; | |
var preRealHeight = 0; | |
// 当前显示的节点需要同步位置 | |
if (getter.Index - 1 < 0) { | |
startPosY = 0; | |
} | |
else { | |
var prePosData = this._itemPosList[getter.Index - 1]; | |
//console.log (`==============> 基类索引为 ${getter.Index - 1} 位置为:${prePosData.pos} 高度为:${prePosData.height}`) | |
startPosY = prePosData.pos.y + this.spacing * this.getScrollDir(); | |
preRealHeight = prePosData.height; | |
} | |
// 后续位置根据初始位置重新计算 | |
var calHeight = this.isTop2Bottom() ? preRealHeight : realHeight; | |
var newPos: Vec3 = new Vec3(0, startPosY + calHeight * this.getScrollDir()); | |
//console.log (`索引 ${getter.Index} 理论高度 ${defaultHeight}---- 实际高度 ${realHeight},位置从 ${this._itemPosList [getter.Index].pos}---->${newPos}`); | |
posData.pos = newPos; | |
posData.height = realHeight; | |
posData.isAsyncPos = true; | |
diffValue += defaultHeight - realHeight; | |
this._itemSizeMap.set(getter.Index, realHeight); | |
getter.node.setPosition(newPos); | |
} | |
//console.log ("结束位置矫正") | |
//contentSize 大小 +- | |
var newHeight = this._curContentHeight - diffValue; | |
//console.log (`content size 从 ${this._curContentHeight} 变为 ${newHeight}`); | |
this.curContentHeight = Math.max(this._maskSize.height, newHeight); | |
this._preScrollY -= diffValue; | |
} | |
private allocateAndShowGetter(idx: number): UICompositeScrollViewItemGetter { | |
//console.log (`显示新增:${idx}`); | |
var key = this._itemNameList[idx]; | |
var getter = this.allocateByName(key); | |
// 先放到屏幕外去 | |
getter.node.position = new Vec3(100000, 100000); | |
getter.MaskSize = new math.Size(this._maskSize.width, this.getShowHeight()); | |
getter.Index = idx; | |
var posData = this._itemPosList[getter.Index]; | |
if (!posData.resized) | |
this.resizeItemNode(getter.node, getter.Index); | |
this.refreshItemNode(getter.node, getter.Index); | |
getter.show(); | |
return getter; | |
} | |
private recycleItemsWithFront2BackSearch(offsetY: number, isTopBorder: boolean, isUp2BottomCheck: boolean) { | |
// 从前往后找,找到的第一个说明这个之后的也已经出界了 | |
for (let i = 0; i < this._curNodeList.length; i++) { | |
var getter = this._curNodeList[i]; | |
var res = isUp2BottomCheck ? getter.outSizeCheckWithUp2Bottom(isTopBorder, offsetY) : getter.outSizeCheckWithBottom2Up(isTopBorder, offsetY); | |
if (res) { | |
this._endIdx = getter.Index - 1; | |
// 回收 | |
for (let j = i; j < this._curNodeList.length; j++) { | |
var recycleGetter = this._curNodeList[j]; | |
//console.log ("回收", recycleGetter.Index); | |
this.recycleNode(recycleGetter); | |
} | |
this._curNodeList = this._curNodeList.slice(0, i); | |
break; | |
} | |
} | |
} | |
private recycleItemsWithBack2FrontSearch(offsetY: number, isTopBorder: boolean, isUp2BottomCheck: boolean) { | |
// 从后往前找,找到的第一个说明这个之前的也已经出界了 | |
for (let i = this._curNodeList.length - 1; i >= 0; i--) { | |
var getter = this._curNodeList[i]; | |
var res = isUp2BottomCheck ? getter.outSizeCheckWithUp2Bottom(isTopBorder, offsetY) : getter.outSizeCheckWithBottom2Up(isTopBorder, offsetY); | |
if (res) { | |
// 回收 | |
this._startIdx = getter.Index + 1; | |
for (let j = 0; j <= i; j++) { | |
var recycleGetter = this._curNodeList[j]; | |
//console.log ("回收", recycleGetter.Index); | |
this.recycleNode(recycleGetter); | |
} | |
this._curNodeList = this._curNodeList.slice(i + 1, this._curNodeList.length); | |
break; | |
} | |
} | |
} | |
} |
注意,这里在刷新显示区域内预制个数时采用的算法是头尾索引间的位置差是否大于遮罩的高度,当某个预制节点的高度过大时,会导致无法正确刷新出新的预制体的问题,但由于该组件目前作用于聊天组件,聊天存在字数限制,因此预制体的高度并不会导致太大的问题,但如果后续出现了这种情况,再去考虑下新的算法吧,比如位置超出头尾后会减去超出部分的差值啥的... 嗯,再说吧 hhh
- 用例
随便写写?
export class xxx :Component{ | |
onLoad(){ | |
var com = this.node.getComponent(UICompositeVerticalLoopScrollViewComp); | |
com.OverHeight = 200; | |
com.OnRefreshItem = this.OnRefreshItem.bind(this); | |
com.OnResizeItem = this.OnResizeItem.bind(this); | |
this.vm.UIScrollViewUICompositeVerticalLoopScrollViewComp.init(); | |
var prefabNames = ["xxxx1","xxx2","xxx1"...] | |
com.resetList(prefabNames); | |
// 尾端插入新数据 | |
com.pushList(["xxx1","xxx2"]); | |
} | |
private OnRefreshItem(node: Node, nodeKey: string, idx: number) { | |
// 刷新节点显示函数 | |
... | |
} | |
private OnResizeItem(node: Node, nodeKey: string, idx: number) { | |
// 重置节点宽高函数 | |
... | |
} | |
} |
# 总结
唔,总的来说勉强能用了,就这样吧,有更好的做法的话欢迎指出😆