众所周知,cocos的ListView控件并不好用,他最明显的缺点有两个:
1.加载大量item时会有明显卡顿
2.不支持2维列表
·关于listview中item的加载listview中的用法,大约看起来是这样的:
1.首先有一个item,并且调用setItemModel
2.调用pushBackDefaultItem插入列表,然后从列表取出item并初始化
(通过阅读代码可知,这个其实就是1中的item调用clone函数,生成插入item,然后addChild,是一样的)
相关代码:
1 void ListView::addChild(cocos2d::Node *child, int zOrder, int tag)
2 {
3 ScrollView::addChild(child, zOrder, tag);
4
5 Widget* widget = dynamic_cast<Widget*>(child);
6 //这里可以看出,不是Widget 的话,并不会有布局的功能……
7 if (nullptr != widget)
8 {
9 _items.pushBack(widget);
10 onItemListChanged();
11 }
12 }
1 void ListView::pushBackDefaultItem()
2 {
3 if (nullptr == _model)
4 {
5 return;
6 }
7 Widget* newItem = _model->clone();
8 remedyLayoutParameter(newItem);//布局排位
9 addChild(newItem);
10 requestDoLayout();//设置重新渲染的标记
11 }
可以看出,listview的item只支持继承自widget的控件,或者说他的本意如此。
基类不是widget的类型,也没有clone方法,那么setItemModel和
pushBackDefaultItem这套体系就形同虚设然而……cocostudio的新建文件,根本就没有一个父节点是继承自widget的
开发过程中,一般一个item就是一个单独的csd,如果想要使用clone,则要每个item的父节点都必须使用一个的panel类型(即“基础容器”,实际上只是一个ccui.layout类型……)来替代node节点,这个panel的大小影响布局,而如果item中有嵌套csd的场合,情况则会变得更加麻烦,因为node是无法复制的。
综上所述,基本可以得出结论,为了节省工作量,clone这套机制,不如不用。直接create一个csd,再addChild的方案,更加可靠,当然,为了避免上述的布局问题,即使create了csd,还是要先加在一个panel上……
·关于clone方法的效率
那么直接使用create的效率比之clone又如何呢?
就直接用代码看看呗。
cocostudio是可以直接把csb文件导出成lua的,而lua本身又可以再一次压缩luac,这看起来效率还可以
试着测试这样一个简单结构的item,创建100个这东东:
local a = require("testItem").create().root:getChildByName("panel_base");
local osClock1 = os.clock();
for i = 1, 100 do
a:clone();
end
print("time:" .. os.clock() - osClock1);
local osClock2 = os.clock();
for i = 1, 100 do
local a = require("testItem").create().root;
end
print("time:" .. os.clock() - osClock2);
输出结果为:
好吧,虽然create慢了点,但还是可以接受的
如果item里面的东西复杂,比如我把btn_test(一个默认的按钮控件),在cocostudio中复制50个,则:
后来又测试了几个以往项目用的比较复杂的item,基本都是直接create更快了,可见这cocos的clone也是很挫的方法。
去瞧瞧c++里的代码,发现他各种属性都要遍历一遍赋值,即是说他没有方法找出重复的值,所以所有的值都要设置一次……上面的使用,想必是因为有很多重复赋值的缘故,才导致慢吧 - -
//clone方法的核心代码,一坨属性的赋值
void Widget::copyProperties(Widget *widget)
{
setEnabled(widget->isEnabled());
setVisible(widget->isVisible());
setBright(widget->isBright());
setTouchEnabled(widget->isTouchEnabled());
setLocalZOrder(widget->getLocalZOrder());
setTag(widget->getTag());
setName(widget->getName());
setActionTag(widget->getActionTag());
_ignoreSize = widget->_ignoreSize;
this->setContentSize(widget->_contentSize);
_customSize = widget->_customSize;
_sizeType = widget->getSizeType();
_sizePercent = widget->_sizePercent;
_positionType = widget->_positionType;
_positionPercent = widget->_positionPercent;
setPosition(widget->getPosition());
setAnchorPoint(widget->getAnchorPoint());
setScaleX(widget->getScaleX());
setScaleY(widget->getScaleY());
setRotation(widget->getRotation());
setRotationSkewX(widget->getRotationSkewX());
setRotationSkewY(widget->getRotationSkewY());
setFlippedX(widget->isFlippedX());
setFlippedY(widget->isFlippedY());
setColor(widget->getColor());
setOpacity(widget->getOpacity());
_touchEventCallback = widget->_touchEventCallback;
_touchEventListener = widget->_touchEventListener;
_touchEventSelector = widget->_touchEventSelector;
_clickEventListener = widget->_clickEventListener;
_focused = widget->_focused;
_focusEnabled = widget->_focusEnabled;
_propagateTouchEvents = widget->_propagateTouchEvents;
copySpecialProperties(widget);
Map<int, LayoutParameter*>& layoutParameterDic = widget->_layoutParameterDictionary;
for (auto iter = layoutParameterDic.begin(); iter != layoutParameterDic.end(); ++iter)
{
setLayoutParameter(iter->second->clone());
}
}
·二维布局以及加载优化
这个单从原理上来说,很简单,大致说说我的思路
假设listview为竖着滑动的,则通过item的宽度,以及listview的宽度,计算出每一行可以摆的下多少个item,并且得到这个行的高度,以此宽高创建一个panel,并把item都算好坐标addChild到这个panel之中,
而这个panel则会被addChild到listview中,这样就实现了二维。
然而,加载的问题还是没有解决,listview就是这样的,你的列表有多少个,就给你加载多少个,一次搞定,cocos又没有做到很好的资源剪裁,于是item一多,就卡到爆炸。
能想到的解决方案:
1.分帧加载
2.随滚动加载
3.创建固定数量的item(即用户可见的最大数量item),监听滑动事件,根据滑动位置来改变各个item位置,并且根据需要重新初始化item中的内容
虽然已经没办法给出具体代码,但是方法1和方法2,甚至方法1和方法2的结合我都有实际在以前项目中使用过。
说实话体验比较一般,特别是item复杂的情况,还是会有很明显的卡顿
……总之多番试验之后,发现方法3才是最好的。
那么怎么用listview去实现呢?listview每添加一个widget类就会自动布局,自己设置滑动范围,对于3的想法来说,实在太不便利,而且我只想在lua中完成这个事情,于是pass。
再看看类似的控件怎么做吧,比如cocos2d-iphone时代就有的控件的tableView……
这个控件虽然使用起来比较麻烦,但是我认为其性能和扩展性都还可以……该说不愧是元祖鬼佬的cocos2d控件吗,反正比国人设计的这个根本不能商用的玩意要好
listview的item在tableview中的名字叫cell,tableview是动态创建item的,他只会创建冒头显示的cell,而且创建的只是一个空的cell,里面的实际内容可以自己定义。
tableview也是默认不支持二维数组的,如要需要用上述类似方法自己封装一下,至于动态加载方法,使用一个缓存item的对象池,配合滑动监听和cell初始化的回调方法来实现应该就可以了。
……好吧,说了这么多,我最终还是没有选择tableview,而是选择了更底层的scrollview,理由比较单纯,因为cocostudio里面的控件有滑动容器(scrollview),没有tableview。这样可以节省一定的ui工作量……
·实现细节
既然决定了,就创建一个叫CCListViewDynamic的class
首先来总结一下,一般初始化一个滑动列表需要的东东:
一个lua table,里面存储了一组数据结构(比如排行版的玩家列表)
一个scrollView控件,一个item的资源文件,用于重复create
一个初始化item的函数指针(比如 通过读取玩家数据,来设置 排名啊 玩家头像 玩家名字之类 )
这些都作为必要的初始化参数传入到CCListViewDynamic中
大致流程:(以下是假设滑动方向为垂直,如果是水平也是类似思路)
1.通过scrollView的宽高获取应初始化item的个数(可见的行数+1 * 每行的个数)
2.创建这些item,并且都addChild到list中,设置坐标为 -width,-height,让其不可见
3.遍历一遍lua table,算出每行由哪些item构成,并且算出每个item的坐标,每一行的显示范围并记录(即scrollview:getInnerContainer():getPositionY()在哪个区间时,这行的item依然显示),这个计算只是预计算数据,是没有做渲染相关工作的
4.根据上面的遍历,也算出了scrollview的滑动范围。
5.根据当前显示范围,通过3中的数据,计算要显示哪些行,如果没有变化,则不继续往下走,否则,和上次显示的范围做比较,计算出今次不显示和新显示的item行数,不显示的item要走一遍2中设置坐标为 -width,-height,让其不可见的流程,新显示的item则要跑一次初始化item的函数指针,并且传入对应的lua table数据
6.监听滑动事件,跑一次5的流程(需要注意的是,这个事件在c++中的枚举值是ScrollView::EventType.CONTAINER_MOVED,不知道什么时候添加的,cocos2d-x3.5的版本是没有这个事件的,如果没有就要自己整一个了……)
上面这里,我试着创建了1000个item,顶点数量和fps还算正常 总体都可以接受……就是初始化速度有点慢,要0.8s,这个还有一些细节有待优化吧。
目前只是粗略的做法,还有一些缺点:
1.目前还没实现插入自定义item,但是如果要做的话,其实思路也是类似的
2.数据和页面耦合太深,如果想做分离式的排序会变得不容易……(比如一个listview里面有两套排序规则的数据)……这个,如果真要有这种变态需求的话,还是有不少地方要改的,但是大体思路依然不变
最后附上代码:
/Files/WhiteDummy/mycode/CCListViewDynamic.txt