列表

列表是组件的一种特殊扩展。点击侧工具栏的按钮生成一个列表。

列表属性

在舞台上点中一个列表,右边属性栏显示的是列表的属性:

点击后弹出二级界面:

点击“编辑列表”后显示对话框:

点击增加,将增加一个由“项目资源”指定的item,如果你的列表需要多种资源混合,可以先新增一个item,再从库中拖动组件到“资源”栏中替换默认的资源。

表格内可编辑item的标题、图标和名称属性,其它属性则可以选中item后,在右侧的检查器内修改。

“发布时自动清空”表示这里编辑的列表数据在最终发布时不会包含到发布结果中,也就是列表数据仅作编辑器预览用途。

GList

管理列表内容

列表对应的类型是GList。在FairyGUI中,列表的本质就是一个组件,GList也是从GComponent派生来的,所以你可以用GComponent的API直接访问列表内的内容,例如可以用GetChild或者GetChildAt访问列表内的项目;也可以用AddChild添加一个item。这部分的API可以参考GComponent的显示列表管理

当你对列表增删改后,列表是自动排列和刷新的,不需要调用任何API。自动排列时会根据列表的布局设置item的坐标、大小和深度,所以不要自行设置item的位置,也不要设置sortingOrder尝试去控制item的深度。除了一个例外,垂直布局的列表只会自动设置item的y坐标,如果你需要item有一个水平位移的效果,你仍然可以修改item的x值。水平布局的也是一样道理。

这个排列和刷新发生在本帧绘制之前,如果你希望立刻访问item的正确坐标,那么可以调用EnsureBoundsCorrect通知GList立刻重排。EnsureBoundsCorrect是一个友好的函数,你不用担心重复调用会有额外性能消耗。

在实际应用中,列表的内容通常被频繁的更新。典型的用法就是当接收到后台数据时,将列表清空,然后再重新添加所有项目。如果每次都创建和销毁UI对象,将消耗很大的CPU和内存。因此,GList内建了对象池。

使用对象池后的显示列表管理方法:

聪明的你应该能知道,AddItemFromPool = GetFromPool + AddChild , RemoveChildToPool = RemoveChild + ReturnToPool。

当应用到池时,我们就应该非常小心,一个不停增长的池那将是游戏的灾难,但如果不使用池,对游戏性能也会有影响。
以下是几种错误用法的举例:

错误示例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 = obj.asButton;
button.title = ""+index;
}

然后设置这个函数为列表的渲染函数:

//Unity/Cry/MonoGame
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);

//CocosCreator
aList.itemRenderer = this.renderListItem.bind(this);

最后直接设置列表中的项目总数,这样列表就会调整当前列表容器的对象数量,然后调用回调函数渲染item。

//创建100个对象,注意这里不能使用numChildren,numChildren是只读的。

aList.numItems = 10;

如果新设置的项目数小于当前的项目数,那么多出来的item将放回池里。

使用这种方式生成的列表,如果你需要更新某个item,自行调用RenderListItem(索引,GetChildAt(索引))就可以了。

列表自动大小

严格来说,列表没有自动大小的功能。但GList提供了API根据item的数量设置列表大小。当你填充完列表的数据后,可以调用GList.ResizeToFit,这样列表的大小就会修改为最适合的大小,容纳指定的item数量。如果不指定item数量,则列表扩展大小至显示所有item。

事件

点击列表内的某一个item触发事件:

//Unity/Cry/MonoGame, 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));

//CocosCreator, onClickItem的第一个参数就是当前被点击的对象,可选的第二个对象是fgui.Event。
list.on(fgui.Event.CLICK_ITEM, this.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表达式。下面举例子说明一下。

C#参考:

void EventCallback()
{
}

EventCallback0 callback = EventCallback;

void OnRenderItem(int index, GObject 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的索引,而非显示对象的索引。AddSelection/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的大小:

除这两种方式外,不可以通过其他在itemRenderer外的方式改变item大小,否则虚拟列表排列会错乱。但你可以通过调用RefreshVirtualList强制触发itemRenderer。

虚拟列表支持不同类型的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);

//CocosCreator
aList.itemProvider = this.getListItemResource.bind(this);

对于横向流动、竖向流动和分页的列表,与非虚拟列表具有流动特性不同,虚拟列表每行或每列的item个数都是固定的。列表在初始化时会创建一个默认的item用于测算这个数量。
如果你仍然需要每行或每列不等item数量的排版,且必须使用虚拟化,那么可以插入一些用于占位的空组件或者空图形,并根据实际需要设置他们的宽度,从而实现那种排版效果。

循环列表

循环列表是指首尾相连的列表,循环列表必须是虚拟列表。启用循环列表的方法为:

aList.SetVirtualAndLoop()。

循环列表只支持单行或者单列的布局,不支持流动布局和分页布局。
因为循环列表是首尾相连的,指定一个item索引可能出现在不同的位置,所以需要指定滚定位置时,尽量避免使用item索引。例如,如果需要循环列表左/上滚一格或者右/下滚一格,最好的办法就是调用ScrollPane的API:ScrollLeft/ScrollRight/ScrollUp/ScrollDown
循环列表的特性与虚拟列表一致,在此不再赘述。