添加一个自定义ListView 实现, 高效复用机制以及便捷的使用接口

This commit is contained in:
leo
2018-10-19 23:19:03 +08:00
parent 094c3a08da
commit 69513dbea4
32 changed files with 26051 additions and 0 deletions

67
ListViewJsDemo/.gitignore vendored Normal file
View File

@@ -0,0 +1,67 @@
#/////////////////////////////////////////////////////////////////////////////
# Fireball Projects
#/////////////////////////////////////////////////////////////////////////////
library/
temp/
local/
build/
#/////////////////////////////////////////////////////////////////////////////
# Logs and databases
#/////////////////////////////////////////////////////////////////////////////
*.log
*.sql
*.sqlite
#/////////////////////////////////////////////////////////////////////////////
# files for debugger
#/////////////////////////////////////////////////////////////////////////////
*.sln
*.csproj
*.pidb
*.unityproj
*.suo
#/////////////////////////////////////////////////////////////////////////////
# OS generated files
#/////////////////////////////////////////////////////////////////////////////
.DS_Store
ehthumbs.db
Thumbs.db
#/////////////////////////////////////////////////////////////////////////////
# exvim files
#/////////////////////////////////////////////////////////////////////////////
*UnityVS.meta
*.err
*.err.meta
*.exvim
*.exvim.meta
*.vimentry
*.vimentry.meta
*.vimproject
*.vimproject.meta
.vimfiles.*/
.exvim.*/
quick_gen_project_*_autogen.bat
quick_gen_project_*_autogen.bat.meta
quick_gen_project_*_autogen.sh
quick_gen_project_*_autogen.sh.meta
.exvim.app
#/////////////////////////////////////////////////////////////////////////////
# webstorm files
#/////////////////////////////////////////////////////////////////////////////
.idea/
#//////////////////////////
# VS Code
#//////////////////////////
.vscode/

3
ListViewJsDemo/README.md Normal file
View File

@@ -0,0 +1,3 @@
# ListView Demo
一个自定义ListView 实现, 高效复用机制以及便捷的使用接口
链接http://forum.cocos.com/t/listview/67396

View File

@@ -0,0 +1,232 @@
[
{
"__type__": "cc.Prefab",
"_name": "",
"_objFlags": 0,
"_native": "",
"data": {
"__id__": 1
},
"optimizationPolicy": 0,
"asyncLoadAssets": false
},
{
"__type__": "cc.Node",
"_name": "ListItem",
"_objFlags": 0,
"_parent": null,
"_children": [
{
"__id__": 2
}
],
"_active": true,
"_level": 1,
"_components": [
{
"__id__": 5
},
{
"__id__": 6
}
],
"_prefab": {
"__id__": 7
},
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 119,
"g": 119,
"b": 119,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 500,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_position": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_scale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_rotationX": 0,
"_rotationY": 0,
"_quat": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_skewX": 0,
"_skewY": 0,
"_zIndex": 0,
"groupIndex": 0,
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "New Label",
"_objFlags": 0,
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_level": 3,
"_components": [
{
"__id__": 3
}
],
"_prefab": {
"__id__": 4
},
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 22.25,
"height": 40
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_position": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_scale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_rotationX": 0,
"_rotationY": 0,
"_quat": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_skewX": 0,
"_skewY": 0,
"_zIndex": 0,
"groupIndex": 0,
"_id": ""
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 2
},
"_enabled": true,
"_srcBlendFactor": 1,
"_dstBlendFactor": 771,
"_useOriginalSize": false,
"_string": "1",
"_N$string": "1",
"_fontSize": 40,
"_lineHeight": 40,
"_enableWrapText": true,
"_N$file": null,
"_isSystemFontUsed": true,
"_spacingX": 0,
"_N$horizontalAlign": 1,
"_N$verticalAlign": 1,
"_N$fontFamily": "Arial",
"_N$overflow": 0,
"_id": "0fTgkeyUtAlbUEzXQuVmWC"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__uuid__": "ee8a03a3-1f23-464b-abf1-1800df851e7e"
},
"fileId": "e4vYmRcZxFKJbFCJ7l482h",
"sync": false
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 1
},
"_enabled": true,
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_spriteFrame": {
"__uuid__": "a23235d1-15db-4b95-8439-a2e005bfff91"
},
"_type": 0,
"_sizeMode": 0,
"_fillType": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_state": 0,
"_atlas": null,
"_id": "denu30qbJDfI60nITKiTWW"
},
{
"__type__": "f139d5DxZVMbZa4VmNhI06c",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 1
},
"_enabled": true,
"label": {
"__id__": 3
},
"_id": "71I0xMGeFKrrdvrFDaEv0A"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__uuid__": "ee8a03a3-1f23-464b-abf1-1800df851e7e"
},
"fileId": "2fpXAiOq1H+b1tE/oO5pnV",
"sync": false
}
]

View File

@@ -0,0 +1,7 @@
{
"ver": "1.0.0",
"uuid": "ee8a03a3-1f23-464b-abf1-1800df851e7e",
"optimizationPolicy": "AUTO",
"asyncLoadAssets": false,
"subMetas": {}
}

View File

@@ -0,0 +1,6 @@
{
"ver": "1.0.1",
"uuid": "29f52784-2fca-467b-92e7-8fd9ef8c57b7",
"isGroup": false,
"subMetas": {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
{
"ver": "1.0.0",
"uuid": "2d2f792f-a40c-49bb-a189-ed176a246e49",
"asyncLoadAssets": false,
"autoReleaseAssets": false,
"subMetas": {}
}

View File

@@ -0,0 +1,6 @@
{
"ver": "1.0.1",
"uuid": "4734c20c-0db8-4eb2-92ea-e692f4d70934",
"isGroup": false,
"subMetas": {}
}

View File

@@ -0,0 +1,24 @@
import ListView, {AbsAdapter} from "./ListView";
const ListAdapter = require('./ListAdapter');
cc.Class({
extends: cc.Component,
properties: {
listView: {
default: null,
type: ListView
},
tipLabel: {
type: cc.Label,
default: null
}
},
start() {
const adapter = new ListAdapter();
adapter.setDataSet([1, 2, 3, 4, 5, 6, 7, 8, 89, 9, 12, 1243, 45, 564, 6756, 876, 7988, 789, 78987, 978, 45, 6732, 423, 42]);
this.listView.setAdapter(adapter);
}
});

View File

@@ -0,0 +1,9 @@
{
"ver": "1.0.5",
"uuid": "280c3aec-6492-4a9d-9f51-a9b00b570b4a",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}

View File

@@ -0,0 +1,13 @@
import {AbsAdapter} from "./ListView";
const ListItem = require('./ListItem');
cc.Class({
extends: AbsAdapter,
updateView(item, posIndex) {
let comp = item.getComponent(ListItem);
if (comp) {
comp.setData(this.getItem(posIndex));
}
}
})

View File

@@ -0,0 +1,9 @@
{
"ver": "1.0.5",
"uuid": "9ec2acad-d240-4e35-9106-069a09c2f73d",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}

View File

@@ -0,0 +1,13 @@
cc.Class({
extends: cc.Component,
properties: {
label: {
default: null,
type: cc.Label
}
},
setData(data) {
this.label.string = `${data}`;
}
});

View File

@@ -0,0 +1,9 @@
{
"ver": "1.0.5",
"uuid": "f139de43-c595-4c6d-96b8-566361234e9c",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}

View File

@@ -0,0 +1,321 @@
const {ccclass, property} = cc._decorator;
@ccclass
export default class ListView extends cc.Component {
@property(cc.Prefab)
private itemTemplate: cc.Prefab = null;
@property
private spacing: number = 1;
// 比可见元素多缓存3个, 缓存越多,快速滑动越流畅,但同时初始化越慢.
@property
private spawnCount: number = 2;
@property(cc.ScrollView)
private scrollView: cc.ScrollView = null;
private content: cc.Node = null;
private adapter: AbsAdapter = null;
private readonly _items: cc.NodePool = new cc.NodePool();
// 记录当前填充在树上的索引. 用来快速查找哪些位置缺少item了.
private readonly _filledIds: { [key: number]: number } = {};
private horizontal: boolean = false;
// 初始时即计算item的高度.因为布局时要用到.
private _itemHeight: number = 1;
private _itemWidth: number = 1;
private _itemsVisible: number = 1;
private lastStartIndex: number = -1;
private scrollTopNotifyed: boolean = false;
private scrollBottomNotifyed: boolean = false;
private pullDownCallback: () => void = null;
private pullUpCallback: () => void = null;
public onLoad() {
if (this.scrollView) {
this.content = this.scrollView.content;
this.horizontal = this.scrollView.horizontal;
if (this.horizontal) {
this.scrollView.vertical = false
this.content.anchorX = 0;
this.content.x = this.content.parent.width * this.content.parent.anchorX;
} else {
this.scrollView.vertical = true;
this.content.anchorY = 1;
this.content.y = this.content.parent.height * this.content.parent.anchorY;
}
} else {
console.error("ListView need a scrollView for showing.")
}
let itemOne = this._items.get() || cc.instantiate(this.itemTemplate);
this._items.put(itemOne);
this._itemHeight = itemOne.height || 10;
this._itemWidth = itemOne.width || 10;
if (this.horizontal) {
this._itemsVisible = Math.ceil(this.content.parent.width / this._itemWidth);
} else {
this._itemsVisible = Math.ceil(this.content.parent.height / this._itemHeight);
}
console.log("可见区域的item数量为:", this._itemsVisible);
this.adjustEvent();
}
public async setAdapter(adapter: AbsAdapter) {
this.adapter = adapter;
if (this.adapter == null) {
console.warn("adapter 为空.")
return
}
if (this.itemTemplate == null) {
console.error("Listview 未设置待显示的Item模板.");
return;
}
this.notifyUpdate();
}
public getItemIndex(height: number): number {
return Math.floor(Math.abs(height / ((this._itemHeight + this.spacing))));
}
public getPositionInView(item: cc.Node) {
let worldPos = item.parent.convertToWorldSpaceAR(item.position);
let viewPos = this.scrollView.node.convertToNodeSpaceAR(worldPos);
return viewPos;
}
// 数据变更了需要进行更新UI显示, 可只更新某一条.
public notifyUpdate(updateIndex?: number[]) {
if (this.adapter == null) {
return;
}
if (updateIndex && updateIndex.length > 0) {
updateIndex.forEach(i => {
if (this._filledIds.hasOwnProperty(i)) {
delete this._filledIds[i];
}
})
} else {
Object.keys(this._filledIds).forEach(key => {
delete this._filledIds[key];
})
}
this.lastStartIndex = -1;
if (this.horizontal) {
this.content.width = this.adapter.getCount() * (this._itemWidth + this.spacing) + this.spacing;
} else {
this.content.height = this.adapter.getCount() * (this._itemHeight + this.spacing) + this.spacing; // get total content height
}
this.scrollView.scrollToTop()
}
public scrollToTop(anim: boolean = false) {
this.scrollView.scrollToTop(anim ? 1 : 0);
}
public scrollToBottom(anim: boolean = false) {
this.scrollView.scrollToBottom(anim ? 1 : 0);
}
public scrollToLeft(anim: boolean = false) {
this.scrollView.scrollToLeft(anim ? 1 : 0);
}
public scrollToRight(anim: boolean = false) {
this.scrollView.scrollToRight(anim ? 1 : 0);
}
// 下拉事件.
public pullDown(callback: () => void, this$: any) {
this.pullDownCallback = callback.bind(this$);
}
// 上拉事件.
public pullUp(callback: () => void, this$: any) {
this.pullUpCallback = callback.bind(this$);
}
protected update(dt) {
const startIndex = this.checkNeedUpdate();
if (startIndex >= 0) {
this.updateView(startIndex);
}
}
// 向某位置添加一个item.
private _layoutVertical(child: cc.Node, posIndex: number) {
this.content.addChild(child);
// 增加一个tag 属性用来存储child的位置索引.
child["_tag"] = posIndex;
this._filledIds[posIndex] = posIndex;
child.setPosition(0, -child.height * (0.5 + posIndex) - this.spacing * (posIndex + 1));
}
// 向某位置添加一个item.
private _layoutHorizontal(child: cc.Node, posIndex: number) {
this.content.addChild(child);
// 增加一个tag 属性用来存储child的位置索引.
child["_tag"] = posIndex;
this._filledIds[posIndex] = posIndex;
child.setPosition(-child.width * (0.5 + posIndex) - this.spacing * (posIndex + 1), 0);
}
// 获取可回收item
private getRecycleItems(beginIndex: number, endIndex: number): cc.Node[] {
const children = this.content.children;
const recycles = []
children.forEach(item => {
if (item["_tag"] < beginIndex || item["_tag"] > endIndex) {
recycles.push(item);
delete this._filledIds[item["_tag"]];
}
})
return recycles;
}
// 填充View.
private updateView(startIndex) {
let itemStartIndex = startIndex;
// 比实际元素多3个.
let itemEndIndex = itemStartIndex + this._itemsVisible + (this.spawnCount || 2);
const totalCount = this.adapter.getCount();
if (itemStartIndex >= totalCount) {
return;
}
if (itemEndIndex > totalCount) {
itemEndIndex = totalCount;
if (!this.scrollBottomNotifyed) {
this.notifyScrollToBottom()
this.scrollBottomNotifyed = true;
}
} else {
this.scrollBottomNotifyed = false;
}
// 回收需要回收的元素位置.向上少收一个.向下少收2两.
const recyles = this.getRecycleItems(itemStartIndex - (this.spawnCount || 2), itemEndIndex);
recyles.forEach(item => {
this._items.put(item);
})
// 查找需要更新的元素位置.
const updates = this.findUpdateIndex(itemStartIndex, itemEndIndex)
// 更新相应位置.
for (let index of updates) {
let child = this.adapter._getView(this._items.get() || cc.instantiate(this.itemTemplate), index);
this.horizontal ?
this._layoutHorizontal(child, index) :
this._layoutVertical(child, index);
}
}
// 检测是否需要更新UI.
private checkNeedUpdate(): number {
if (this.adapter == null) {
return -1
}
let scroll = this.horizontal ? (this.content.x - this.content.parent.width * this.content.parent.anchorX)
: (this.content.y - this.content.parent.height * this.content.parent.anchorY);
let itemStartIndex = Math.floor(scroll / ((this.horizontal ? this._itemWidth : this._itemHeight) + this.spacing));
if (itemStartIndex < 0 && !this.scrollTopNotifyed) {
this.notifyScrollToTop();
this.scrollTopNotifyed = true;
return itemStartIndex;
}
// 防止重复触发topNotify.仅当首item不可见后才能再次触发
if (itemStartIndex > 0) {
this.scrollTopNotifyed = false;
}
if (this.lastStartIndex != itemStartIndex) {
this.lastStartIndex = itemStartIndex;
return itemStartIndex;
}
return -1;
}
// 查找需要补充的元素索引.
private findUpdateIndex(itemStartIndex: number, itemEndIndex: number): number[] {
const d = [];
for (let i = itemStartIndex; i < itemEndIndex; i++) {
if (this._filledIds.hasOwnProperty(i)) {
continue;
}
d.push(i);
}
return d;
}
private notifyScrollToTop() {
if (!this.adapter || this.adapter.getCount() <= 0) {
return;
}
if (this.pullDownCallback) {
this.pullDownCallback();
}
}
private notifyScrollToBottom() {
if (!this.adapter || this.adapter.getCount() <= 0) {
return;
}
if (this.pullUpCallback) {
this.pullUpCallback();
}
}
private adjustEvent() {
this.content.on(this.isMobile() ? cc.Node.EventType.TOUCH_END : cc.Node.EventType.MOUSE_UP, () => {
this.scrollTopNotifyed = false;
this.scrollBottomNotifyed = false;
}, this)
this.content.on(this.isMobile() ? cc.Node.EventType.TOUCH_CANCEL : cc.Node.EventType.MOUSE_LEAVE, () => {
this.scrollTopNotifyed = false;
this.scrollBottomNotifyed = false;
}, this);
}
private isMobile(): boolean {
return (cc.sys.isMobile || cc.sys.platform === cc.sys.WECHAT_GAME || cc.sys.platform === cc.sys.QQ_PLAY)
}
}
// 数据绑定的辅助适配器
export abstract class AbsAdapter {
private dataSet: any[] = [];
public setDataSet(data: any[]) {
this.dataSet = data;
}
public getCount(): number {
return this.dataSet.length;
}
public getItem(posIndex: number): any {
return this.dataSet[posIndex];
}
public _getView(item: cc.Node, posIndex: number): cc.Node {
this.updateView(item, posIndex);
return item;
}
public abstract updateView(item: cc.Node, posIndex: number);
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.0.5",
"uuid": "8a20b952-8dfd-4538-aba8-d30cc2a55f00",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}

View File

@@ -0,0 +1,6 @@
{
"ver": "1.0.1",
"uuid": "7b81d4e8-ec84-4716-968d-500ac1d78a54",
"isGroup": false,
"subMetas": {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -0,0 +1,31 @@
{
"ver": "2.2.0",
"uuid": "6aa0aa6a-ebee-4155-a088-a687a6aadec4",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"subMetas": {
"HelloWorld": {
"ver": "1.0.3",
"uuid": "31bc895a-c003-4566-a9f3-2e54ae1c17dc",
"rawTextureUuid": "6aa0aa6a-ebee-4155-a088-a687a6aadec4",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 195,
"height": 270,
"rawWidth": 195,
"rawHeight": 270,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,31 @@
{
"ver": "2.2.0",
"uuid": "a8027877-d8d6-4645-97a0-52d4a0123dba",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"subMetas": {
"singleColor": {
"ver": "1.0.3",
"uuid": "410fb916-8721-4663-bab8-34397391ace7",
"rawTextureUuid": "a8027877-d8d6-4645-97a0-52d4a0123dba",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 2,
"height": 2,
"rawWidth": 2,
"rawHeight": 2,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}

View File

@@ -0,0 +1,7 @@
{
"ver": "1.0.1",
"uuid": "d6a6c9a3-1d26-4236-8c6e-26cbf483984b",
"isSubpackage": false,
"subpackageName": "",
"subMetas": {}
}

View File

@@ -0,0 +1,7 @@
{
"ver": "1.0.1",
"uuid": "efe47830-b7d6-484d-9036-b37640b0d7bb",
"isSubpackage": false,
"subpackageName": "",
"subMetas": {}
}

24149
ListViewJsDemo/creator.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"experimentalDecorators": true
},
"exclude": [
"node_modules",
".vscode",
"library",
"local",
"settings",
"temp"
]
}

View File

@@ -0,0 +1,4 @@
{
"engine": "cocos2d-html5",
"packages": "packages"
}

View File

@@ -0,0 +1,13 @@
{
"excludeScenes": [],
"orientation": {
"landscapeLeft": true,
"landscapeRight": true,
"portrait": false,
"upsideDown": false
},
"packageName": "org.cocos2d.helloworld",
"startScene": "2d2f792f-a40c-49bb-a189-ed176a246e49",
"title": "hello_world",
"webOrientation": "auto"
}

View File

@@ -0,0 +1,7 @@
{
"excludeScenes": [],
"packageName": "org.cocos2d.helloworld",
"platform": "web-mobile",
"startScene": "2d2f792f-a40c-49bb-a189-ed176a246e49",
"title": "HelloWorld"
}

View File

@@ -0,0 +1,4 @@
{
"languages": [],
"default_language": ""
}

View File

@@ -0,0 +1,28 @@
{
"collision-matrix": [
[
true
]
],
"excluded-modules": [],
"group-list": [
"default"
],
"start-scene": "current",
"design-resolution-width": 960,
"design-resolution-height": 640,
"fit-width": false,
"fit-height": true,
"use-project-simulator-setting": false,
"simulator-orientation": false,
"use-customize-simulator": false,
"simulator-resolution": {
"width": 960,
"height": 640
},
"cocos-analytics": {
"enable": false,
"appID": "13798",
"appSecret": "959b3ac0037d0f3c2fdce94f8421a9b2"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,5 @@
{
"name": "TEMPLATES.helloworld.name",
"desc": "TEMPLATES.helloworld.desc",
"banner": "template-banner.png"
}