cocos creator 滚动列表 ListView

滚动列表,这种东西在游戏中很常见.而cocos creator 中的ScrollView + Layout 只有你想不到,没有它满足不了,   各种分骚布局. 都能实现.  但是, 但是, 它还有一些场景不是很适合.例如 :

  • 千百人的排行榜(虽然,大家只会记得第一名,谁还在乎第二名,除非第二名是自己)
  • 密密麻麻的好友列表(当然有些人是没有几个朋友的,就像我,这点就不适用了)
  • 上百条的充值记录(当然,只有我这种土豪,才会充这么多给自己开发的游戏)

这些数据,都有一个特点,那就是,数据量大,结构类似 对于这种数据,我们可以使用scrollView 进行显示,其实压力也不是很大,但是,如果用 循环列表 的话,会更好. 那么,  你用还是不用呢?

进入正题.

2019.10.23 翻了以前的测试项目找到了个demo附加在这,

链接: https://pan.baidu.com/s/1ukiVbbU3SyoajHAU24Z9qg 提取码: yup6 

js版本是这篇博客用到的,后面的ts版本是项目用到的稍微改了一下,做了个小demo也都传上来(creator版本 2.2.0)


ScrollView的结构:

之前无论是 2dx 还是 2d-JS 都用过ScrollView,而且用的也非常多,但是呢,今天我想用creator 来说说scrollView的那些事:

scollView的原理就是用content做子元素的容器,用view做一个切割,显示View元素在屏幕占据的矩形面积,其子元素,只能显示在父元素范围内的信息.view 和content 其实是实现scrollView的关键

其实scrollView 组件本身起到的作用,  就是主要是 统筹的作用 ,将数据逻辑,和页面截取分开 

  • content本身不做任何处理,由scrollView 组件控制,
  • scrollview 为滑动提供动画,控制范围.联动滑动条,提供事件监听
  • content可以添加Layout组件,布局元素的内容

滚动列表 ListView的实现

以前的公司,为了实现ListView 在将content向上,或者向下滚动的时候,当某个元素完全滚出view显示的范围一定距离后,会将content回滚,然后将之前显示的content中的其他item设置到,回滚之前,相对于屏幕的位置.将超出范围的那个元素放置到队列最后,这样操作在一帧内完成,玩家不会意识到页面回滚了.屏幕可以接着滚动.这样会使content保存在一个固定的大小.在需要显示滚动条的时候就比较麻烦.

这算是一种实现方法.但是呢,还不是最好的解决办法.

而我今天要说的就是第二种方式:

这种方式,在创建多个类表项的时候,只需要创建超过屏幕范围数量的Item,然后,只需要增加content的长度,到所有item的长度之后,就可以了,在滑动的时候,按照上面的逻辑处理,就可以实现循环滚动.


代码实现ListView:

属性配置:

 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._deadZone){
           return ;
        }
        var direction =cc.v2();
        if(this.direction == Direction.VERTICAL ){
            direction.y= this._lastContentPos.y> 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布局的,总结.欢迎吐槽.

吐槽是一种年轻的沟通方式!

2021.1.28 优化

优化后的链接

  • 7
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
Unity 无限滚动列表是一种常见的UI设计模式,用于在有大量数据的情况下,动态加载和显示列表项,以实现更好的性能和用户体验。 实现无限滚动列表需要以下步骤: 1. 创建一个包含UI元素(例如图标、文本块等)的列表项模板。 2. 创建一个容器对象,用于容纳列表项。这个容器对象可以是 ScrollView 或者使用 Unity 的 UI 布局组件。 3. 在启动时,初始化列表。通常会加载一部分列表项并显示在容器中,同时记录当前显示的第一个和最后一个列表项的索引。 4. 监听滚动事件。当滚动发生时,根据滚动的方向(向上或向下),动态地加载和显示新的列表项。 5. 当滚动到顶部时,加载并显示上一批新的列表项,并删除容器中最后一个列表项。 6. 当滚动到底部时,加载并显示下一批新的列表项,并删除容器中第一个列表项。 7. 重复第4至第6步,直到所有列表项都被加载和显示。 在实现无限滚动列表时,可以使用对象池来优化性能。对象池可以缓存已经创建的列表项对象,而不是每次滚动时都创建新的对象。这样可以避免频繁的对象创建和销毁,提高性能。 此外,还可以将无限滚动列表与数据源绑定,以便在滚动时动态从数据源中加载数据。这样可以实现更高效地处理大量数据的功能。 总而言之,Unity 无限滚动列表是一种非常有用的UI设计模式,可以在处理大量数据时提高性能和用户体验。通过合理的实现和优化,可以实现一个流畅且高效的无限滚动列表
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值