1. 程式人生 > >FairyGUI筆記:列表(十九)

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
迴圈列表的特性與虛擬列表一致,在此不再贅述。