CocosCreator3.x开发笔记10: 列表或者背包,用s
2023-03-02 本文已影响0人
原味蛋炒饭
用到复用滚动的空间,CocosCreator商城上的控件太贵了,然后打算自己做一个。
根据网上的一些轮子,自己改了一遍,适用于CocosCreator3.x
自用目前没什么问题,如果按我的代码使用上有什么问题,自己处理一下吧,我这个只能作为一种思路参考,大致意思都差不多就行了。刷新性能上没时间优化了,有什么思路可以评论交流一下,有空我再改哈哈哈哈哈哈。
效果图
![截屏2023-03-03 16.56.43.png](https://img.haomeiwen.com/i1803099/1251962a4e2be78c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
![截屏2023-03-03 16.56.52.png](https://img.haomeiwen.com/i1803099/1d7ac8f330058a34.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
RecyclerView
import { _decorator, Component, Node, ScrollView, game, Layout, UITransform, Vec3, EventHandler } from 'cc';
import { BaseAdapter } from './BaseAdapter';
import { BaseViewHolder } from './BaseViewHolder';
const { ccclass, property, requireComponent } = _decorator;
@ccclass('RecyclerView')
@requireComponent(ScrollView)
export class RecyclerView extends Component {
private _adapter: BaseAdapter = null;
public set adapter(v: BaseAdapter) {
this._adapter = v;
this.initLayout();
}
public get adapter(): BaseAdapter {
return this._adapter;
}
private _scrollView: ScrollView;
public get scrollView(): ScrollView {
return this._scrollView;
}
private _layout: Layout;
private _view: UITransform;
/**
* @en No layout.
* NONE = 0,
* @zh 禁用布局。
*/
/**
* @en Horizontal layout.
*HORIZONTAL = 1,
* @zh 水平布局。
*/
/**
* @en Vertical layout.
* VERTICAL = 2,
* @zh 垂直布局。
*/
/**
* @en Grid layout.
* GRID = 3
* @zh 网格布局。
*/
private _layoutType: number;
private _pdLeft: number;
private _pdRight: number;
/*上边距*/
private _pdTop: number;
/*下边距*/
private _pdBottom: number;
private _spaceX: number;
private _spaceY: number;
private _startAxis: number;
/**距离scrollView中心点的距离,超过这个距离的item会被重置,一般设置为 scrollVIew.height/2 + item.heigt/2 + space,因为这个距离item正好超出scrollView显示范围 */
private halfScrollView: number = 0;
/**上一次content的Y值,用于和现在content的Y值比较,得出是向上还是向下滚动 */
private lastContentPosY: number = 0;
/**上一次content的X值,用于和现在content的X值比较,得出是向左还是向右滚动 */
private lastContentPosX: number = 0;
//分帧创建器
private _gener: Generator
private _isActive = true;
private _pool: Map<number, Array<BaseViewHolder>> = new Map();
private _childrens: Array<BaseViewHolder> = new Array();
// /*获取停止页面的第一个item的index*/
// private lastEndStartIndex: number = 0;
/**刷新的函数 */
private updateFun: Function = function () { };
onLoad() {
this._scrollView = this.node.getComponent(ScrollView);
let layout = this._scrollView.content.getComponent(Layout);
if (!layout) {
console.error("请在content里面添加item布局方式");
}
this._layoutType = layout.type;
this._pdLeft = layout.paddingLeft;
this._pdRight = layout.paddingRight;
this._pdTop = layout.paddingTop;
this._pdBottom = layout.paddingBottom;
this._spaceX = layout.spacingX;
this._spaceY = layout.spacingY;
this._startAxis = layout.startAxis; //HORIZONTAL = 0, VERTICAL = 1
//取消布局约束
layout.type = Layout.Type.NONE;
// layout.type = Layout.Type.GRID;
this._layout = layout;
this._view = layout.getComponent(UITransform);
this.reset();
this.node.on(Node.EventType.SIZE_CHANGED, this.sizeChanged, this);
// this.scrollView.node.on(ScrollView.EventType.SCROLLING, this.onScrolling, this);
//修改:为了防止ondestroy中this.scrollView.node为null报错,这里直接用node,recyclerview只要挂在scrollview上this.scrollView.node和this.node一样的
this.node.on(ScrollView.EventType.SCROLLING, this.onScrolling, this);
//SCROLL_ENDED有时候在停止后会一直调用,这里用SCROLL_ENG_WITH_THRESHOLD
this.node.on(ScrollView.EventType.SCROLL_ENG_WITH_THRESHOLD, this.onEndWithThreshold, this);
}
onDestroy() {
this.node.off(Node.EventType.SIZE_CHANGED, this.sizeChanged, this);
// this.scrollView.node.off(ScrollView.EventType.SCROLLING, this.onScrolling, this);
//修改:this.scrollView.node会为空
this.node.off(ScrollView.EventType.SCROLLING, this.onScrolling, this);
this.node.off(ScrollView.EventType.SCROLL_ENG_WITH_THRESHOLD, this.onEndWithThreshold, this);
//把绘制的内容手动销毁
this._pool.forEach((hodel) => {
hodel.forEach((holNode) => {
if (holNode.node.isValid) {
holNode.node.destroy()
}
})
})
this._pool.clear();
//把绘制的内容手动销毁
this._childrens.forEach((hodel) => {
if (hodel.node.isValid) {
hodel.node.destroy()
}
})
this._childrens.length = 0;
//还有node残留,把子节点都销毁
let tem = this.node.children;
tem.forEach((temNode) => {
if (temNode.isValid) {
temNode.destroy()
}
})
this._adapter = null;
this._scrollView = null;
this._layout = null
this._isActive = false;
}
//重置数据
private reset() {
this._gener?.return("")//取消上一次的分帧任务(如果任务正在执行)
//把绘制的内容手动销毁
this._pool.forEach((hodel) => {
hodel.forEach((holNode) => {
if (holNode.node.isValid) {
holNode.node.destroy()
}
})
})
this._pool.clear();
//把绘制的内容手动销毁
this._childrens.forEach((hodel) => {
if (hodel.node.isValid) {
hodel.node.destroy()
}
})
this._childrens.length = 0;
}
/**数据重置后刷新页面*/
refreshView() {
this.sizeChanged()
}
/**增删数据后刷新页面*/
addDetleRefreshView() {
this.countParam()
}
/**页面内容满了之后移动到最底部*/
fullViewScrollToBottom() {
if (this._view.height > this._scrollView.view.height) {
//内容已经满了,滚动到底部
this._scrollView.scrollToBottom()
this.endScrollRefreshView()
} else {
//内容没有满,需要清空旧页面再添加
this.clearChildrensToPool()
this.startCreateItems(0);
}
}
/**页面停止移动后,刷新页面*/
endScrollRefreshView() {
// console.log("endScrollRefreshView");
//获取停止页面的第一个item的index
let startIndex = Math.ceil(this.adapter.dataA.length*(this._scrollView.getScrollOffset().y/ this._view.height))
//清空旧页面
this.clearChildrensToPool();
//从第一个item开始放置
this.startCreateItems(startIndex);
//刷新页面
this.updateFun();
}
//节点大小改变
private sizeChanged() {
if (this.adapter) {
this.reset();
this.initLayout();
}
}
//初始化面板
private initLayout() {
let layout = this._layout;
if (!layout) {
console.error("请在content里面添加item布局方式");
return;
}
this.countParam();
this.startCreateItems(0);
}
/*初始化scrollview数据*/
private countParam() {
// console.log("countParam");
let type = this._layoutType;
if (type == Layout.Type.VERTICAL) {
let bottomY = this._pdTop;
for (let index = 0; index < this.adapter.getItemCount(); index++) {
this.adapter.dataA[index].size = this.adapter.getItemSize(index)
if (index != 0) {
bottomY += this._spaceY;
}
this.adapter.dataA[index].position = new Vec3(0, -bottomY - (this.adapter.dataA[index].size.height / 2));
bottomY += this.adapter.dataA[index].size.height
}
bottomY += this._pdBottom;
this._view.height = bottomY
this.halfScrollView = this.scrollView.view.height / 2;
this.updateFun = this.updateV;
} else if (type == Layout.Type.HORIZONTAL) {
// this._view.width = this.scrollView.view.width * this.adapter.getItemCount();
// this._view.width = (this.adapter.getItemSize().width + this._spaceX) * this.adapter.getItemCount();
let viewW: number = 0
for (let index = 0; index < this.adapter.dataA.length; index++) {
viewW += this.adapter.getItemSize(index).width + this._spaceX
}
this._view.width = viewW
this.halfScrollView = this.scrollView.view.width / 2;
this.updateFun = this.updateH;
} else if (type == Layout.Type.GRID) {
let startAxis = this._startAxis;
/**
* @en The horizontal axis.
* HORIZONTAL = 0,
* @zh 进行水平方向布局。
*/
/**
* @en The vertical axis.
* VERTICAL = 1
* @zh 进行垂直方向布局。
*/
if (startAxis == 0) {
} else if (startAxis == 1) {
let bottomY = this._pdTop;
let bottomX = this._pdLeft;
let maxH = 0;
for (let index = 0; index < this.adapter.getItemCount(); index++) {
this.adapter.dataA[index].size = this.adapter.getItemSize(index)
if ((bottomX + this.adapter.getItemSize(index).width + this._pdRight) > this.scrollView.view.width) {
bottomX = this._pdLeft;
bottomY += this.adapter.getItemSize(index).height + this._spaceY;
}
this.adapter.dataA[index].position = new Vec3(bottomX + (this.adapter.dataA[index].size.width / 2) - this.scrollView.view.width/2, -bottomY - (this.adapter.dataA[index].size.height / 2));
bottomX += this.adapter.getItemSize(index).width + this._spaceX;
maxH = this.adapter.dataA[index].size.height
}
bottomY += maxH + this._pdBottom;
this._view.height = bottomY
this.halfScrollView = this.scrollView.view.height / 2;
this.updateFun = this.updateGridV;
}
}
}
/**
*
* @param startIndex 创建的起始节点
*/
private startCreateItems(startIndex: number) {
// console.log("startCreateItems");
//取消上一次的分帧任务(如果任务正在执行)
this._gener?.return("");
if (startIndex < 0) {
startIndex = 0;
}
let type = this._layoutType;
let maxNum = this.adapter.getItemCount();
let total = 0;
if (type == Layout.Type.VERTICAL) {
total = Math.abs(this._view.height);
} else if (type == Layout.Type.HORIZONTAL) {
total = Math.abs(this._view.width)
} else if (type == Layout.Type.GRID) {
if (this._startAxis == 0) {
} else if (this._startAxis == 1) {
total = Math.abs(this._view.height);
}
}
this._gener = this.getGeneratorLength(total, (i, gener) => {
if (!this._isActive) {
gener?.return("")
return false;
}
let index = startIndex + i;
if (index >= maxNum) {
//超出范围 则直接退出
gener?.return("")
return false
}
let item: BaseViewHolder;
let itemType = this.adapter.getType(index);
// console.log("startCreateItems_getItem");
item = this.getItem(itemType, index);
item.itemIndex = index;
this._layout.node.addChild(item.node);
if (type == Layout.Type.VERTICAL) {
item.node.position = new Vec3(item.node.position.x, this.adapter.dataA[index].position.y)
if (!this.isInWindow(item)) {
this._childrens.push(item);
gener?.return("")
//创建结束
return false;
}
} else if (type == Layout.Type.HORIZONTAL) {
let leftX = this._pdLeft;
let lastItem = this._childrens[this._childrens.length - 1];
if (lastItem) {
leftX = lastItem.node.position.x + lastItem.view.width / 2
+ this._spaceX;
}
item.node.position = new Vec3(leftX + item.view.width / 2,
item.node.position.y);
if (!this.isInWindow(item)) {
this._childrens.push(item);
gener?.return("")
return false;
}
if (i == maxNum - 1) {
this._view.width
= Math.abs(item.node.position.x) + item.view.width / 2 + this._pdRight;
}
} else if (type == Layout.Type.GRID) {
let startAxis = this._startAxis;
if (startAxis == 0) {
//水平
} else if (startAxis == 1) {
//垂直
item.node.position = new Vec3(this.adapter.dataA[index].position.x, this.adapter.dataA[index].position.y)
}
if (!this.isInWindow(item)) {
this._childrens.push(item);
gener?.return("")
return false;
}
}
this._childrens.push(item);
return true;
}, this._gener)
this.exeGenerator(this._gener, 4);
}
private createNextItem() {
// console.log("createNextItem");
let lastItem = this._childrens[this._childrens.length - 1];
if (!lastItem) {
return
}
let index = lastItem.itemIndex + 1;
if (index >= this.adapter.getItemCount()) {
return;
}
if (this.isInWindow(lastItem)) {
let item: BaseViewHolder;
let type = this._layoutType;
let itemType = this.adapter.getType(index);
item = this.getItem(itemType, index);
item.itemIndex = index;
this._layout.node.addChild(item.node);
if (type == Layout.Type.VERTICAL) {
item.node.position = new Vec3(item.node.position.x, this.adapter.dataA[index].position.y)
} else if (type == Layout.Type.HORIZONTAL) {
let leftX = this._pdLeft;
let lastItem = this._childrens[this._childrens.length - 1];
if (lastItem) {
leftX = lastItem.node.position.x + lastItem.view.width / 2
+ this._spaceX;
}
item.node.position = new Vec3(leftX + item.view.width / 2,
item.node.position.y);
if (index == this.adapter.getItemCount() - 1) {
this._view.width
= Math.abs(item.node.position.x) + item.view.width / 2 + this._pdRight;
}
} else if (type == Layout.Type.GRID) {
// if (this._startAxis == 0) {
// } else if (this._startAxis == 1) {
item.node.position = new Vec3(this.adapter.dataA[index].position.x, this.adapter.dataA[index].position.y)
// }
}
this._childrens.push(item);
this.createNextItem();
}
}
private createPreviousItem() {
// console.log("createPreviousItem");
let firstItem = this._childrens[0];
if (!firstItem) {
return
}
let index = firstItem.itemIndex - 1;
if (index < 0) {
return
}
if (this.isInWindow(firstItem)) {
let item: BaseViewHolder;
let type = this._layoutType;
let itemType = this.adapter.getType(index);
item = this.getItem(itemType, index);
item.itemIndex = index;
this._layout.node.addChild(item.node);
if (type == Layout.Type.VERTICAL) {
item.node.position = new Vec3(item.node.position.x, this.adapter.dataA[index].position.y);
} else if (type == Layout.Type.HORIZONTAL) {
let leftX = firstItem.node.position.x - firstItem.view.width / 2 - this._spaceX;
item.node.position = new Vec3(leftX - item.view.width / 2, item.node.position.y);
}
this._childrens.unshift(item);
this.createPreviousItem();
}
}
private createGrildPreviousItem() {
let firstItem = this._childrens[0];
if (!firstItem) {
return
}
let index = firstItem.itemIndex - 1;
if (index < 0) {
return
}
if (this.isInWindow(firstItem)) {
let item: BaseViewHolder;
let type = this._layoutType;
let itemType = this.adapter.getType(index);
item = this.getItem(itemType, index);
item.itemIndex = index;
this._layout.node.addChild(item.node);
item.node.position = new Vec3(this.adapter.dataA[index].position.x, this.adapter.dataA[index].position.y)
this._childrens.unshift(item);
this.createGrildPreviousItem();
}
}
//判断是否在窗口
private isInWindow(item: BaseViewHolder): boolean {
let point = this.getPositionInView(item);
let type = this._layoutType;
let startAxis = this._startAxis;
if (type == Layout.Type.VERTICAL) {
if (point.y - item.view.height / 2 > this.halfScrollView
|| point.y + item.view.height / 2 < -this.halfScrollView) {
return false;
}
} else if (type == Layout.Type.HORIZONTAL) {
if (point.x + item.view.width / 2 < -this.halfScrollView
|| point.x - item.view.width / 2 > this.halfScrollView) {
return false;
}
} else if (type == Layout.Type.GRID) {
if (startAxis == 0) {
if (point.x + item.view.width / 2 < -this.halfScrollView
|| point.x - item.view.width / 2 > this.halfScrollView) {
return false;
}
} else if (startAxis == 1) {
if (point.y - item.view.height / 2 > this.halfScrollView
|| point.y + item.view.height / 2 < -this.halfScrollView) {
return false;
}
}
}
return true;
}
/**获取item在scrollView的局部坐标 */
private getPositionInView(item: BaseViewHolder): Vec3 {
let worldPos = this._view.convertToWorldSpaceAR(item.node.position);
let viewPos = this.scrollView.view.convertToNodeSpaceAR(worldPos);
return viewPos;
}
/**获取一个列表项 */
private getItem(type, index) {
let child: BaseViewHolder;
let datas = this._pool.get(type);
if (datas && datas.length) {
child = datas.pop();
} else {
child = this.adapter.onCreateViewHolder(index)
}
this.adapter.onBindViewHolder(child, index);
return child;
}
/**从页面移除后,放入pool*/
private removeItem(item: BaseViewHolder) {
if (!item) { return }
item.node.removeFromParent();
let type = this.adapter.getType(item.itemIndex);
let datas = this._pool.get(type);
if (!datas) {
datas = new Array();
}
datas.push(item);
// this._pool[type] = datas;
//修改:this._pool[type] = datas 不能在没有生产键值对的情况下使用
this._pool.set(type, datas)
}
/**直接放入pool*/
private poolPushItem(item: BaseViewHolder) {
if (!item) { return }
let type = this.adapter.getType(item.itemIndex);
let datas = this._pool.get(type);
if (!datas) {
datas = new Array();
}
datas.push(item);
//修改:this._pool[type] = datas 不能在没有生产键值对的情况下使用
this._pool.set(type, datas)
}
/**
*清除页面上的item到Pool,只remove不在页面上的childrens,
*/
public clearChildrensToPool() {
let poolTemp: BaseViewHolder[] = []
this._childrens.forEach(element => {
let viewPos = this.getPositionInView(element);
//如果item超过上边界 那么就移除
if (viewPos.y - element.view.height / 2 > this.halfScrollView) {
//直接remove
this.removeItem(element);
} else {
//不直接remove
poolTemp.push(element)
}
});
poolTemp.forEach(element => {
//不直接remove pool
this.poolPushItem(element);
});
this._childrens.length = 0
}
/**清除页面上的item到Pool,并全部remove*/
public clearAllRemoveChildrensToPool() {
this._childrens.forEach(element => {
this.removeItem(element);
});
this._childrens.length = 0
}
public updateV() {
// console.log("updateV");
let isUp = this._layout.node.position.y > this.lastContentPosY;
let isDown = this._layout.node.position.y < this.lastContentPosY;
let childs = this._childrens;
for (let i = 0; i < childs.length; ++i) {
let item = childs[i];
let viewPos = this.getPositionInView(item);
if (childs.length <= 1) {
//必须要剩一个 不然就全部被删除了
break
}
if (isUp) {
//如果item超过上边界 那么就移除
if (viewPos.y - item.view.height / 2 > this.halfScrollView) {
this.removeItem(item);
childs.splice(i, 1);
i--;
}
} else if (isDown) {
if (viewPos.y + item.view.height / 2 < -this.halfScrollView) {
this.removeItem(item);
childs.splice(i, 1);
i--;
}
}
}
if (isUp) {
//创建下一个
this.createNextItem();
} else if (isDown) {
//创建上一个
this.createPreviousItem();
}
this.lastContentPosY = this._layout.node.position.y;
}
public updateH() {
let isLeft = this._layout.node.position.x < this.lastContentPosX;
let childs = this._childrens;
for (let i = 0; i < childs.length; ++i) {
let item = childs[i];
let viewPos = this.getPositionInView(item);
if (childs.length <= 1) {
break
}
if (isLeft) {
//如果item超过左边界 那么就移除
if (viewPos.x + item.view.width / 2 < -this.halfScrollView) {
this.removeItem(item);
childs.splice(i, 1);
i--;
}
} else {
if (viewPos.x - item.view.width / 2 > this.halfScrollView) {
this.removeItem(item);
childs.splice(i, 1);
i--;
}
}
}
if (isLeft) {
//创建下一个
this.createNextItem();
} else {
//创建上一个
this.createPreviousItem();
}
this.lastContentPosX = this._layout.node.position.x;
}
public updateGridV() {
// console.log("updateGridV");
// if (startAxis == 0) {
// } else if (startAxis == 1) {
// }
let isUp = this._layout.node.position.y > this.lastContentPosY;
let childs = this._childrens;
for (let i = 0; i < childs.length; ++i) {
let item = childs[i];
let viewPos = this.getPositionInView(item);
if (childs.length <= 1) {
//必须要剩一个 不然就全部被删除了
break
}
if (isUp) {
//如果item超过上边界 那么就移除
if (viewPos.y - item.view.height / 2 > this.halfScrollView) {
this.removeItem(item);
childs.splice(i, 1);
i--;
}
} else {
if (viewPos.y + item.view.height / 2 < -this.halfScrollView) {
this.removeItem(item);
childs.splice(i, 1);
i--;
}
}
}
if (isUp) {
//创建下一个
this.createNextItem();
} else {
//创建上一个
this.createGrildPreviousItem();
}
this.lastContentPosY = this._layout.node.position.y;
}
/**是否滚动容器 */
private bScrolling: boolean = false;
lateUpdate(dt) {
if (this.bScrolling == false) {
return;
}
this.bScrolling = false;
this.updateFun();
}
public onScrolling(ev: Event = null) {
this.bScrolling = true;
}
public onEndWithThreshold(ev: Event = null) {
//移动停止回调
this.endScrollRefreshView()
}
/** 分帧加载 */
private * getGeneratorLength(length: number, callback: Function, ...params: any): Generator {
for (let i = 0; i < length; i++) {
let result = callback(i, ...params)
if (result) {
yield
} else {
return
}
}
}
/** 分帧执行 */
private exeGenerator(generator: Generator, duration: number) {
return new Promise<void>((resolve, reject) => {
let gen = generator
let execute = () => {
let startTime = new Date().getTime()
for (let iter = gen.next(); ; iter = gen.next()) {
if (iter == null || iter.done) {
resolve()
return
}
if (new Date().getTime() - startTime > duration) {
setTimeout(() => execute(), game.deltaTime * 1000)
return
}
}
}
execute()
})
}
}
BaseViewHolder
import { _decorator, Node, UITransform, Size, Vec3 } from 'cc';
export class BaseScrollData {
data: any
size: Size
position: Vec3
constructor(data: any, size: Size, position: Vec3) {
this.data = data
this.size = size
this.position = position
}
}
export abstract class BaseViewHolder {
public node: Node;
public view: UITransform;
public itemIndex:number;
constructor(node: Node){
this.node = node;
this.view = node.getComponent(UITransform);
}
abstract onBind(data:any, index: number);
}
BaseAdapter
import { _decorator, Component, Node, Size } from 'cc';
import { BaseViewHolder, BaseScrollData } from './BaseViewHolder';
const { ccclass, property } = _decorator;
@ccclass('BaseAdapter')
export abstract class BaseAdapter extends Component {
//节点数量
abstract getItemCount(): number;
//节点宽高
abstract getItemSize(index: number): Size;
//数据源
abstract dataA: BaseScrollData[]
//创建节点
abstract onCreateViewHolder(index: number): BaseViewHolder;
//绑定节点信息
abstract onBindViewHolder(holder: BaseViewHolder, index: number);
// 对不同的数据进行分类
abstract getType(index:number):number;
private _dataDirty = false;
public get dataDirty() : boolean {
return this._dataDirty;
}
//数据刷新
public notifyDataSetChanged(){
this._dataDirty = true;
}
//数据刷新完成
public dataRefreshComplete(){
this._dataDirty = false;
}
}