FairyGUI笔记:列表(十九)

  • GList

列表对应的是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内建了对象池。

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

  • AddItemFromPool从池中取出(如果有)或者新建一个对象,添加到列表中。如果不使用参数,则使用列表的"项目资源"的设置;也可以指定一个URL,创建指定的对象。
  • GetFromPool从池中取出(如果有)或者新建一个对象。
  • RetrurnToPool将对象返回池里。
  • RemoveChildToPool删除一个item,并将对象返回池里。
  • RemoveChildrenToPool删除一个范围内的item,或者全部删除,并将删除的对象都返回池里

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

错误示例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(索引))就可以了。

  • scrollItemToViewOnClick

这是列表的一个选项,如果为true,当点击某个item时,如果这个item处于部分显示状态,那么列表将会自动滚动到整个item显示完整。
默认值是true。如果你的列表有超过列表视口大小的item,建议设置为false。

  • foldInvisibleItem

这是列表的一个选项,如果为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创建实体对象,并通过动态设置数据的方式实现大容量列表。

启用虚拟列表有几个条件:

  • 需要定义itemRenderer
  • 需要开启滚动。溢出处理不是滚动的列表不能开启虚拟。
  • 需要设置好列表的"项目资源"。可以在编辑器内设置,也可以调用GList.defaultItem设置。

满足条件后可以开启列表的虚拟功能:

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对象来处理。
刷新虚拟列表的方式有两种:

  • 使用numItems重新设置数量
  • GList.RefreshVirtualList。

不允许使用AddChild或RemoveChild对虚拟列表增删对象。如果要清空列表,必须要通过设置numItems=0,而不是RemoveChildren。

虚拟列表支持可变大小的item,可以通过两种方式动态改变item的大小:

  • 在itemRenderer的内部使用width、height或SetSize改变item的大小。
  • item建立对内部元件的关联,例如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
循环列表的特性与虚拟列表一致,在此不再赘述。

  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值