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