列表对应的是GList.在FairyGUI中,列表的本质就是一个组件,GList也是从GComponent派生来的,所以你可以用GComponent的API直接访问列表能容,例如可以用GetChild或者GetChildAt访问列表内的项目;也可以用AddChild添加一个item。
当你对列表增删改后,列表是自动排列和刷新的,不需要调用任何API。自动排列时会根据列表的布局设置item的坐标、大小和深度,所以不要自行设置item的位置,也不要设置sortingOrder尝试去控制item的深度。除了一个例外,垂直布局的列表只会自动设置item的y坐标,如果你需要item有一个水平位移的效果,你仍然可以修改item的x值。水平布局的也是一样道理。
这个排列和刷新发生在本帧绘制之前,如果你希望立刻访问item的正确坐标,那么可以调用EnsureBoundsCorrect
通知GList立刻重排。EnsureBoundsCorrect是一个友好的函数,你不用担心重复调用会有额外性能消耗。
在实际应用中,列表的内容通常被频繁的更新。典型的用法就是当接收到后台数据时,将列表清空,然后再重新添加所有项目。如果每次都创建和销毁UI对象,将消耗很大的CPU和内存。因此,GList内建了对象池。
使用对象池后的显示列表管理方法:
当应用到池时,我们就应该非常小心,一个不停增长的池那将是游戏的灾难,但如果不使用池,对游戏性能也会有影响。
错误示例1:
GObject obj = UIPackage.CreateObject(...);
aList.AddChild(obj);
aList.RemoveChildrenToPool();
添加对象时不使用池,但最后清除列表时却放到池里。这段代码持续运行,对象池将不断增大,可能造成内存溢出。
正确的做法:应从池中创建对象。将AddChild改成AddItemFromPool。
错误示例2:
for(int i=0;i<10;i++)
aList.AddItemFromPool();
aList.RemoveChildren();
这里添加了10个item,但移除时并没有保存他们的引用,也没有放回到池里,这样就造成了内存泄漏。将aList.RemoveChildren改成aList.RemoveChildrenToPool();
移除和销毁是两回事。当你把item从列表移除时,如果以后不再使用,那么还应该销毁;如果还需要用,那么请保存它的引用。但如果放入了池,切勿再销毁item。
当添加大量item时,除了用循环方式AddChild或AddItemFromPool外,还可以使用另一种回调的方式。首先为列表定义一个回调函数,例如
void RenderListItem(int index,GObject obj)
{
GButton button = ob.asButton;
button.title = "+index";
}
然后设置这个函数为列表的渲染函数:
//Unity/Cry
aList.itemRenderer = RenderListItem;
//AS3
aList.itemRenderer = RenderListItem;
//Egret
aList.itemRenderer = RenderListItem;
aList.callbackThisObj = this;
//Laya。(注意,最后一个参数必须为false!)
aList.itemRenderer = Handler.create(this, this.RenderListItem, null, false);
//Cocos2dx
aList->itemRenderer = CC_CALLBACK_2(AClass::renderListItem, this);
最后直接设置列表中的项目总数,这样列表就会调整当前列表容器的对象数量,然后调用回调函数渲染item。
//创建100个对象,注意这里不能使用numChildren,numChildren是只读的。
aList.numItems = 100;
如果新设置的项目数小于当前的项目数,那么多出来的item将放回池里。
使用这种方式生成的列表,如果你需要更新某个item,自行调用RenderListItem(索引,GetChildAt(索引))就可以了。
这是列表的一个选项,如果为true,当点击某个item时,如果这个item处于部分显示状态,那么列表将会自动滚动到整个item显示完整。
默认值是true。如果你的列表有超过列表视口大小的item,建议设置为false。
这是列表的一个选项,如果为true,但某个item不可见时(visible=false),列表不会为他留位置,也就是排版时会忽略这个item;如果为false,在列表会为这个item保留位置,显示效果就是一个空白的占位。默认值是false。
严格来说,列表没有自动大小的功能。但GList提供了API根据item的数量设置列表的大小。当你填充完列表的数据后,可以调用GList.ResizeToFit,这样列表的大小就会修改为最适合的大小,容纳指定的item数量。如果不指定item数量,则列表扩展大小至显示所有item。
点击列表内的某一个item触发事件:
//Unity/Cry, EventContext.data就是当前被点击的item对象
list.onClickItem.Add(onClickItem);
//AS3, ItemEvent.itemObject就是当前被点击的对象
list.addEventListener(ItemEvent.CLICK, onClickItem);
//Egret,ItemEvent.itemObject就是当前被点击的对象
list.addEventListener(ItemEvent.CLICK, this.onClickItem, this);
//Laya, onClickItem方法的第一个参数就是当前被点击的对象
list.on(fairygui.Events.CLICK_ITEM, this, this.onClickItem);
//Cocos2dx,EventContext.getData()就是当前被点击的item对象
list->addEventListener(UIEventType::ClickItem, CC_CALLBACK_1(AClass::onClickItem, this));
从上面的代码可以看出,事件回调里都可以方便的获得当前点击的对象。如果要获得索引,那么可以使用GetChildIndex。
如果列表的item数量特别多时,例如几百上千,为每一条项目创建实体的显示对象将非常消耗时间和资源。FairyGUI的列表内置了虚拟机制,也就是它只为显示范围内的item创建实体对象,并通过动态设置数据的方式实现大容量列表。
启用虚拟列表有几个条件:
满足条件后可以开启列表的虚拟功能:
aList.SetVirtual();
提示:虚拟功能只能开启,不能关闭。
虚拟列表的性能和itemRenderer的处理逻辑密切相关,你应该尽量简化这里面的逻辑,协程、IO、高密度计算这类操作不应该在这里出现,否者会出现卡顿。如果需要在itemRenderer里发起异步操作,切勿让异步操作保存ITEM实例,并且在回调中直接写该ITEM实例,正确的做法是让异步操作保存ITEM的索引,异步操作完成后,查询这个索引的ITEM是否有对应的显示对象,有则更新,如果没有,放弃更新
另外,itemRenderer里也不应该有new等会产生GC的操作,因为在滚动的过程中,itemRenderer调用频率会非常高。
在虚拟列表里,ITEM是服用的,当一个ITEM需要被刷新时,itemRenderer就会被调用,你无需关心这个调用的时机,也不能依赖这个时机。如果在itemRenderer你使用Add进行事件的侦听操作,绝不可以使用临时函数或者lamba表达式。
void EventCallback()
{
}
EventCallback0 callback = EventCallback;
void OnRenderItem(int index,GObect obj)
{
GButton btn = obj.asCom.GetChild("btn").asButton;
//错误!,临时函数会造成添加多次回调。Lua里使用“function() end”类似。
btn.onClick.Add(()=>{});
//可以,同一个方法只会添加一次。但直接使用方法名会生成几十B的GC。
btn.onClick.Add(EventCallback);
//正确,callback是缓存的代理实例,不会产生GC。
btn.onClick.Add(callback);
//正确,使用Set设置可以保证不会重复添加。
btn.onClick.Set(callback);
//错误!,不能对ITEM使用onClick.Set,你需要用GList.onClickItem
obj.onClick.Set(EventCallback);
}
AS3/Starling/Egret/Laya参考:
//
private function EventCallback(evt:Event):void
{
}
private function onRenderItem(index:int, obj:GObject):void
{
var btn:GButton = obj.asCom.getChild("btn").asButton;
//错误,这里不应该使用临时函数
btn.addClickListener(function():void {});
//正确,同一个方法只会添加一次
btn.addClickListener(EventCallback);
}
在虚拟列表中,显示对象和item的数量在数量上和顺序上是不一致的,item的数量可以通过numItems获得,而显示对象的数量可以由组件的API numChildren获得。
在虚拟列表中,需要注意item索引和显示对象索引的区分。通过selectedIndex获得的值是item的索引,而非显示对象的索引。AddSeledtion/RemoveSelection等API同样需要的是item的索引。项目索引和对象索引的转换可以通过以下两种方法完成:
//转换项目索引为显示对象索引。
int childIndex = aList.ItemIndexToChildIndex(1);
//转换显示对象索引为项目索引。
int itemIndex = aList.ChildIndexToItemIndex(1);
使用虚拟列表时,我们很少会需要访问屏外对象。如果你确实需要获得列表中指定索引的某一个项目的显示对象,例如第500个,因为当前这个item是不在视口的,对于虚拟列表,不在视口的对象是没有对应的显示对象的,那么你需要先让列表滚动到目标位置。例如:
//这里要注意,因为我们要立即访问新滚动位置的对象,所以第二个参数scrollItToView不能为true,即不使用动画效果
aList.ScrollToView(500);
//转换到显示对象索引
int index = aList.ItemIndexToChildIndex(500);
//这就是你要的第500个对象
GObject obj = aList.GetChildAt(index);
虚拟列表的本质是数据和渲染分离,经常有人问怎样删除、或者修改虚拟列表的项目,答案就是先修改你的数据,然后刷新列表就可以了,不需要获得某个item对象来处理。
刷新虚拟列表的方式有两种:
不允许使用AddChild或RemoveChild对虚拟列表增删对象。如果要清空列表,必须要通过设置numItems=0,而不是RemoveChildren。
虚拟列表支持可变大小的item,可以通过两种方式动态改变item的大小:
除这两种方式外,不可以通过其他方式改变item大小,否则虚拟列表排列会错乱。
虚拟列表支持不同类型的item混合。首先为列表定义一个回调函数,例如
//根据索引的不同,返回不同的资源URL
string GetListItemResource(int index)
{
Message msg = _messages[index];
if (msg.fromMe)
return "ui://Emoji/chatRight";
else
return "ui://Emoji/chatLeft";
}
然后设置这个函数为列表的item提供者:
//Unity/Cry
aList.itemProvider = GetListItemResource;
//AS3
aList.itemProvider = GetListItemResource;
//Egret
aList.itemProvider = GetListItemResource;
aList.callbackThisObj = this;
//Laya。(注意,最后一个参数必须为false!)
aList.itemProvider = Handler.create(this, this.GetListItemResource, null, false);
//Cocos2dx
aList->itemProvider = CC_CALLBACK_1(AClass::getListItemResource, this);
对于横向流动、竖向流动和分页的列表,与非虚拟列表具有流动特性不同,虚拟列表每行或每列的item个数都是固定的。列表在初始化时会创建一个默认的item用于测算这个数量。
如果你仍然需要每行或每列不等item数量的排版,且必须使用虚拟化,那么可以插入一些用于占位的空组件或者空图形,并根据实际需要设置他们的宽度,从而实现那种排版效果。
循环列表是指首尾相连的列表,循环列表必须是虚拟列表。启用循环列表的方法为:
aList.SetVirtualAndLoop()。
循环列表只支持单行或者单列的布局,不支持流动布局和分页布局。
因为循环列表是首尾相连的,指定一个item索引可能出现在不同的位置,所以需要指定滚定位置时,尽量避免使用item索引。例如,如果需要循环列表左/上滚一格或者右/下滚一格,最好的办法就是调用ScrollPane的API:ScrollLeft/ScrollRight/ScrollUp/ScrollDown
循环列表的特性与虚拟列表一致,在此不再赘述。