滚动列表,这种东西在游戏中很常见.而cocos creator 中的ScrollView + Layout 只有你想不到,没有它满足不了, 各种分骚布局. 都能实现. 但是, 但是, 它还有一些场景不是很适合.例如 :
这些数据,都有一个特点,那就是,数据量大,结构类似 对于这种数据,我们可以使用scrollView 进行显示,其实压力也不是很大,但是,如果用 循环列表 的话,会更好. 那么, 你用还是不用呢?
进入正题.
2019.10.23 翻了以前的测试项目找到了个demo附加在这,
链接: https://pan.baidu.com/s/1ukiVbbU3SyoajHAU24Z9qg 提取码: yup6
js版本是这篇博客用到的,后面的ts版本是项目用到的稍微改了一下,做了个小demo也都传上来(creator版本 2.2.0)
之前无论是 2dx 还是 2d-JS 都用过ScrollView,而且用的也非常多,但是呢,今天我想用creator 来说说scrollView的那些事:
scollView的原理就是用content做子元素的容器,用view做一个切割,显示View元素在屏幕占据的矩形面积,其子元素,只能显示在父元素范围内的信息.view 和content 其实是实现scrollView的关键
其实scrollView 组件本身起到的作用, 就是主要是 统筹的作用 ,将数据逻辑,和页面截取分开
以前的公司,为了实现ListView 在将content向上,或者向下滚动的时候,当某个元素完全滚出view显示的范围一定距离后,会将content回滚,然后将之前显示的content中的其他item设置到,回滚之前,相对于屏幕的位置.将超出范围的那个元素放置到队列最后,这样操作在一帧内完成,玩家不会意识到页面回滚了.屏幕可以接着滚动.这样会使content保存在一个固定的大小.在需要显示滚动条的时候就比较麻烦.
这算是一种实现方法.但是呢,还不是最好的解决办法.
而我今天要说的就是第二种方式:
这种方式,在创建多个类表项的时候,只需要创建超过屏幕范围数量的Item,然后,只需要增加content的长度,到所有item的长度之后,就可以了,在滑动的时候,按照上面的逻辑处理,就可以实现循环滚动.
属性配置:
properties: {
itemTemplate: { // Item的模板
default: null,
type: cc.Node
},
scrollView: { //需要的ScrollView
default: null,
type: cc.ScrollView
},
spawnCount: 0, //循环的Item 的个数
totalCount: 0, //总共需要创建多少个Item
spacing: 0, //item 间隙
bufferZone: 0, //出了scrollView多远,开启位置调整
},
初始化:
onLoad: function () {
this.content = this.scrollView.content; //ScrollVeiw的content节点
this.items = []; //存放所有的创建的Item 其实只有循环的几个 //
this.lastContentPosY = 0; //用来保存content在Y坐标的值
//根据循环需要的数量调整content的高度
this.content.height = this.totalCount * (this.itemTemplate.height + this.spacing) + this.spacing;
//创建必要的Item
for (let i = 0; i < this.spawnCount; ++i) {
let item = cc.instantiate(this.itemTemplate);
this.content.addChild(item);
//第一个item的位置是 - (间隔高度 + 一半高度的item); 后面以此减去间隔,和一item的高度
item.setPosition(0, -item.height * (0.5 + i) - this.spacing * (i + 1));
item.getComponent('Item').updateItem(i, i);
this.items.push(item);
}
},
循环判断,更新数据:
getPositionInView: function (item) { //位置转换到ScrollVeiw的坐标系,判断是否出了可视范围
let worldPos = item.parent.convertToWorldSpaceAR(item.position);
let viewPos = this.scrollView.node.convertToNodeSpaceAR(worldPos);
return viewPos;
},
update: function(dt) {
let items = this.items;
let buffer = this.bufferZone;
let isDown = this.scrollView.content.y < this.lastContentPosY;
let offset = (this.itemTemplate.height + this.spacing) * items.length;
for (let i = 0; i < items.length; ++i) {
let viewPos = this.getPositionInView(items[i]);
if (isDown) {
// if away from buffer zone and not reaching top of content
if (viewPos.y < -buffer && items[i].y + offset < 0) {
items[i].y = items[i].y + offset;
let item = items[i].getComponent('Item');
let itemId = item.itemID - items.length; // update item id
item.updateItem(i, itemId);
}
} else {
if (viewPos.y > buffer && items[i].y - offset > -this.content.height) {
items[i].y = items[i].y - offset;
let item = items[i].getComponent('Item');
let itemId = item.itemID + items.length;
item.updateItem(i, itemId);
}
}
}
this.lastContentPosY = this.scrollView.content.y;
},
以上,便是一个垂直滚动的ListView的整个逻辑了,这里,我没有直接创建一个ListVeiw 继承ScrollView原因是,降低了耦合性,这种组合模式拆卸也方便,可以更具需要再特化这个组件. 第二,创建继承后,你还是要手动搭建一个ScrollIVew,太麻烦(主要是,我不会创建那种自定义组件,从工具页面拖出来就配置好子节点的那种.)
自己写的一个简单的ListVIew,欢迎指正:
想把资源包导出来上传,好像不行,就把这个组件贴在这了
代码进过cocos creator 2.07测试
// Learn cc.Class:
// - [Chinese] http://docs.cocos.com/creator/manual/zh/scripting/class.html
// - [English] http://www.cocos2d-x.org/docs/creator/en/scripting/class.html
// Learn Attribute:
// - [Chinese] http://docs.cocos.com/creator/manual/zh/scripting/reference/attributes.html
// - [English] http://www.cocos2d-x.org/docs/creator/en/scripting/reference/attributes.html
// Learn life-cycle callbacks:
// - [Chinese] http://docs.cocos.com/creator/manual/zh/scripting/life-cycle-callbacks.html
// - [English] http://www.cocos2d-x.org/docs/creator/en/scripting/life-cycle-callbacks.html
//测试用,可以删除
var ItemData = cc.Class({
name:"ItemData",
properties: {
id:cc.Integer,
name:cc.String,
grade:cc.Integer
},
});
const EventType=cc.ScrollView.EventType;
/**
* ListView 方向
* @enum ListView.Direction
*/
const Direction = cc.Enum({
HORIZONTAL: 0,
VERTICAL: 1
});
/**
* 循环滚动组件
* @class ListView
* @extends Component
*/
let ListView =cc.Class({
extends: cc.Component,
properties: {
template: {
default: null,
type: cc.Node,
tooltip:"跟新数据模版,可以是node节点,也可以是 prefab(手动赋值)"
},
updateComp: {
default:"",
tooltip:"跟新数据组件名"
},
datas:{
default: [],
type:ItemData,
serializable:true
},
scrollView: {
default: null,
type: cc.ScrollView
},
direction:{
default: Direction.VERTICAL,
type: Direction,
},
lblPostion:{
default: null,
type: cc.Label
},
spawnCount: 0, //用来循环的item的数量
spacing: 0, //每个Item的之间的间隙
bufferZone: 0, //离StrollView中心多远之后开启循环
_items:[], //缓存包装后的Item
_updateTimer:0, //记录上次更新的时间
_updateInterval:0.2, //记录跟新的间隙
_content:null, //ScrollView的容器
_deadZone:2, //死区,滑动在这个范围内,不更新
_lastContentPos:cc.v2(0,0), //和死区配合使用
_offset:0, //
_onFastRefresh:false, //在设置固定位置时,要快速的滚动到相应的位置
},
statics: {
Direction:Direction,
},
onLoad: function () {
this._content = this.scrollView.content;
this._lastContentPos = this._content.position;
//测试获取数据
cc.loader.loadRes("data",function(err,data){
this.bindData(data.json);
}.bind(this))
},
//用于手动初始化数据
initialize: function (scrollView,direction,template,updateComp,datas,spawnCount,bufferZone) {
this.scrollView = scrollView;
this.direction = direction;
this.template = template;
this.updateComp = updateComp;
this._content = scrollView.content;
if(this.direction == Direction.VERTICAL ){
this._content.height = this.datas.length * (this.template.height + this.spacing) + this.spacing;
this.spawnCount = spawnCount? spawnCount : Math.ceil(this.scrollView.height*2/ this.template.height);
this.bufferZone = bufferZone ? bufferZone : this.scrollView.height/2+this.template.height;
}else {
this._content.width = this.datas.length * (this.template.width + this.spacing) + this.spacing;
this.spawnCount = spawnCount? spawnCount: Math.ceil(this.scrollView.width*2/ this.template.width);
this.bufferZone = bufferZone ? bufferZone : this.scrollView.width/2+this.template.width;
}
//可以重复使用
this._items = [];
this.datas = [];
this._content.removeAllChildren(true);
this.bindData(datas);
},
bindData:function(datas){
cc.assert(datas != null,"数据类型错误");
for (let item in datas){
this.addItem(datas[item])
}
},
addItem:function(data){
//如果数量没有达到循环需要的数量,就要添加新的item
if(this._items.length < this.spawnCount){
let item = cc.instantiate(this.template);
this._content .addChild(item);
let index = this._items.length;
if(this.direction == Direction.VERTICAL ){
item.setPosition(0, -item.height * (0.5 + index) - this.spacing * (index + 1));
}
else {
item.setPosition(item.width * (0.5 + index) + this.spacing * (index + 1),0);
}
item.getComponent(this.updateComp).updateItem(data);
//包装一下保存index
this._items.push({_item:item,_index:index});
}
this.datas.push(data);
//添加元素后,重新计算一下content的高度
if(this.direction == Direction.VERTICAL ){
this._content.height = this.datas.length * (this.template.height + this.spacing) + this.spacing;
}else {
this._content.width = this.datas.length * (this.template.width + this.spacing) + this.spacing;
}
},
getPositionInView: function (item) {
let worldPos = item.parent.convertToWorldSpaceAR(item.position);
let viewPos = this.scrollView.node.convertToNodeSpaceAR(worldPos);
return viewPos;
},
update: function(dt) {
if(this._items.length < this.spawnCount){
return;
}
this.lblPostion.string= this._content.position.toString();
//可以启用这里的跟新,也可以是onScroll函数
return;
this._updateTimer += dt;
if(this._updateTimer < this._updateInterval ) return;
this._updateListView();
this._updateTimer = 0;
this._lastContentPos = this._content.position;
},
_updateListView:function(){
// 和 onScroll 函数开启一个就够了
let items = this._items;
for (let i = 0; i < items.length; ++i) {
var item =items[i];
let _item = item._item;
let _index = item._index;
let viewPos = this.getPositionInView(_item);
let buffer = this.bufferZone;
let offset =0;
if( this.direction == Direction.VERTICAL){
offset=(this.template.height + this.spacing) * this._items.length;
if(this._content.y > this._lastContentPos.y){
if (viewPos.y > buffer && _item.y - offset > -this._content.height) {
_item.y = _item.y - offset;
item._index = _index + this._items.length;
let itemComponent = _item.getComponent(this.updateComp);
itemComponent.updateItem(this.datas[item._index]);
}
}else{
if (viewPos.y < -buffer && _item.y + offset < 0) {
_item.y = _item.y + offset;
item._index = _index - this._items.length;
let itemComponent = _item.getComponent(this.updateComp);
itemComponent.updateItem(this.datas[item._index]);
}
}
}else{
offset=(this.template.width + this.spacing) * this._items.length;
if(this._content.x > this._lastContentPos.x){
if (viewPos.x > buffer && _item.x - offset > 0) {
_item.x= _item.x - offset;
item._index = _index - this._items.length;
let itemComponent = _item.getComponent(this.updateComp);
itemComponent.updateItem(this.datas[item._index]);
}
}else{
if (viewPos.x < -buffer && _item.x + offset < this._content.width) {
_item.x = _item.x + offset;
item._index = _index + this._items.length;
let itemComponent = _item.getComponent(this.updateComp);
itemComponent.updateItem(this.datas[item._index]);
}
}
}
}
},
/**
* @method scrollEvent
* @param {cc.ScrollView}
* @param {cc.ScrollView.EventType}
*/
scrollEvent: function(sender, event) {
switch(event) {
case EventType.SCROLLING:
this.onScroll();
break;
case EventType.SCROLL_ENDED:
cc.log( "Auto scroll ended");
break;
}
},
onScroll:function(direction){
//和 _updateListView 选择开启一个就够了
//本来是为了简便,不想做太多的判断,反倒是写的更加麻烦了
var diff = this._lastContentPos.sub(this._content.position).mag();
if(diff this._content.position.y ? -1 : 1;
}else {
direction.x= this._lastContentPos.x> this._content.position.x ? -1:1;
}
for (let i = 0; i < this._items.length; ++i) {
let item = this._items[i];
let _item = item._item;
let _index = item._index;
let buffer = this.bufferZone;
let offsetPos = cc.v3(
(this.template.width + this.spacing) * this._items.length * -direction.x ,
(this.template.height + this.spacing) * this._items.length * -direction.y,
0
);
let targetPos = _item.position.add(offsetPos);
let rect = this._content.getBoundingBoxToWorld()
let viewPos = this.getPositionInView(_item);
let distance = viewPos.mag();
//距离在边框之外 并且 转换后的坐标在盒子空间内
if(distance > buffer && viewPos.normalize().equals(direction) && rect.contains(this._content.convertToWorldSpaceAR(targetPos)) ){
_item.position = targetPos;
item._index = _index + this._items.length * ( direction.x == -1 || direction.y == 1 ? 1 :-1);
let itemComponent = _item.getComponent(this.updateComp);
itemComponent.updateItem(this.datas[item._index]);
}
}
this._lastContentPos = this._content.position;
},
scrollToFixedPosition: function (pos) {
this.scrollView.scrollToOffset(pos, 2);
},
scrollTo500: function () {
this.scrollToFixedPosition(cc.v2(500, 500));
},
scrollToBottom: function () {
this.scrollToFixedPosition(cc.v2(0, this._content.height));
},
scrollToTop: function () {
this.scrollToFixedPosition(cc.v2(0, 0));
},
scrollToLeft: function () {
this.scrollToFixedPosition(cc.v2(0, 0));
},
scrollToRight: function () {
this.scrollToFixedPosition(cc.v2(this._content.width, 0));
}
});
结束了, 各位再会!, 后面,会写一篇ScrollView + Layout布局的,总结.欢迎吐槽.
吐槽是一种年轻的沟通方式!