NGUI長列表優化利器
優化原理
NGUI3.7.x以上版本 有個新組件 UIWrapContent ,當我們的列表內容很多時,可以進行優化。它不是一次生成全部的child,而是只有固定數量的child,在滑動時更新child的內容。
當前NGUI3.6.X也有此組件,不過不完善,比如更新每一條渲染未實現,protected virtual void UpdateItem (Transform item, int index) ,還有未提供便捷的接口供外部調用。
UIWrapContent詳解
無需循環滾動
如果你需要無限滾動,那么請設置Range Limit,這個范圍是在:-最大數量+1 ~ 0。至於前面的負號,你可以去看看它的實現原理。比如你共顯示20條數據,那么范圍就是-20+1~0(-19~0)。
重疊?
如果你的內容之間會出現如下所示的重疊現象,那是Item Height的值過小
這個Item Height表示每兩個Item之間間隔,這兒不是每個item的高度,所以請設置成和UIGrid的Height一樣的值,當然如果你是水平滑動,就請和Cell Width一樣。如果沒有UIGrid,那么就設置比item的高度大一些。
名字被改了?
在運行的時候,如果是老版本的NGUI,那么很不幸的是,Item的名字會被修改,這個某些情況下還是有影響的。
如果你不想名字被修改,打開 NGUI\Scripts\Interaction\UIWrapContent.cs,在 WrapContent 方法,查找 t.name = realIndex.ToString(); 並刪除。(在ngui3.7.3中一共用四行,全刪除)
重要方法
public delegate void OnInitializeItem (GameObject go, int wrapIndex, int realIndex);
執行渲染的委托,DoRender(要渲染的對象,索引[0開始]) 真正開始渲染
private void OnInitItem(GameObject go, int wrapindex, int realindex) { var index = Mathf.Abs(realindex);// 取絕對值 CacheObject2Index[go] = index; if (CheckActive(go, index) && _hasRefresh) { DoRender(go, index); } }
在滾動時調用,更新當前滾動未尾的Item
protected virtual void UpdateItem(Transform item, int index) { if (onInitializeItem != null) { int realIndex = (mScroll.movement == UIScrollView.Movement.Vertical) ? Mathf.RoundToInt(item.localPosition.y / itemSize) : Mathf.RoundToInt(item.localPosition.x / itemSize); onInitializeItem(item.gameObject, index, realIndex); } }
UIWrapContent封裝
UI結構
使用此Help,你的UI結構可以是以下任意一種(注:如果是NGUI3.9.x建使用結構二)
下圖左UI結構: ListPanel上綁定了UIPanel、UIScrollView,UIWrapContent、UIGrid,即只有一層結構
下圖右結構:Scrollview上綁定了UIPanel,UIScrollview,WrapContent上綁定了UIGrid和UIWrapContent,分兩層結構
組件源碼
為了減少代碼量,我對UIWrapContent進行了一層封裝,代碼如下:
需要NGUI3.7.x之后的版本
update log
2015-10-25 增加可以設置順序(從左到右,從上到下)
2016-05-28
改掉foreach,減少GC,修改error
已知bug:invertOrder=true時,有莫名的表現,日后修復。
using UnityEngine; using System.Collections; using System.Collections.Generic; /// <summary> /// 對NGUI的 UIWrapContent的封裝,如果低於NGUI3.7.x,請使用高版本的UIWrapContent替換 /// 目錄結構:(NGUI3.9.7) /// -GameObject綁定 UIPanel,UIScrollView /// -GameObject綁定 UIWrapContent,[UIGrid] /// -Item (具體的滑動內容) /// 使用方法:var WrapContentHelper = UIWrapContentHelper.Create(WrapContent); /// by 趙青青 /// </summary> public class UIWrapContentHelper { public delegate void UIWrapContentRenderDelegate(GameObject obj, int index); /// <summary> ///obj:要渲染的對象; index:索引,從0開始 /// </summary> public UIWrapContentRenderDelegate OnRenderEvent; private int _count;//總數 private bool _hasRefresh = false;//是否已刷新 private UIWrapContent _wrapContent; private UIPanel _panel; private UIScrollView _scrollView; private Vector2 _initPanelClipOffset; private Vector3 _initPanelLocalPos; /// <summary> /// 緩存起來上次渲染的對象對應索引 /// </summary> private Dictionary<GameObject, int> CacheObject2Index = new Dictionary<GameObject, int>(); private UIWrapContentHelper(){} private UIWrapContentHelper(UIWrapContent uiWrapContent) { if (uiWrapContent == null) { Debug.LogError("UIWrapContentHelper 傳入了NULL"); return; } _wrapContent = uiWrapContent; //_wrapContent.hideInactive = false; _wrapContent.onInitializeItem = OnInitItem; //NOTE NGUI 3.7.x以上版本才有此功能 //NOTE UIPanel 建議掛在UIWrapContent的父級,NGUI3.9.7非父級我這兒出現異怪現象 _panel = _wrapContent.gameObject.GetComponent<UIPanel>(); var panelParent = _wrapContent.transform.parent; if (_panel == null && panelParent != null) { _panel = panelParent.GetComponent<UIPanel>(); } if (_panel == null) { Debug.LogError(uiWrapContent.name + "的父節點沒有UIPanel"); return; } _scrollView = _panel.GetComponent<UIScrollView>(); _initPanelClipOffset = _panel.clipOffset; _initPanelLocalPos = _panel.cachedTransform.localPosition; } //初始化數據,Init或Open時調用 public void ResetScroll() { if (_panel == null || _wrapContent == null || _scrollView == null) { Debug.LogWarning("panel or wrapContent , scrollView is null "); return; } _panel.clipOffset = _initPanelClipOffset; _panel.cachedTransform.localPosition = _initPanelLocalPos; // 重設組件~索引和位置 var index = 0; foreach (var oChildTransform in _wrapContent.transform) { var childTransform = (Transform)oChildTransform; // NOTE: 橫方向未測試 if (_scrollView.movement == UIScrollView.Movement.Vertical) { childTransform.SetLocalPositionY(-_wrapContent.itemSize * index); } else if (_scrollView.movement == UIScrollView.Movement.Horizontal) { childTransform.SetLocalPositionX(-_wrapContent.itemSize * index); } CacheObject2Index[childTransform.gameObject] = index; index++; } //fix soft clip panel if (_panel.clipping == UIDrawCall.Clipping.SoftClip) _panel.SetDirty(); } /// <summary> /// 設置多少項 /// </summary> /// <param name="count"></param> /// <param name="invertOrder">是否反轉</param> private void SetCount(int count, bool invertOrder = false) { if (_panel == null || _wrapContent == null) { Debug.LogWarning("panel or wrapContent is null "); return; } _count = count; //TODO: invertOrder有bug ,NGUI 3.7.x有此功能 //if (invertOrder) //{ // _wrapContent.minIndex = 0; // _wrapContent.maxIndex = count - 1; //} //else { _wrapContent.minIndex = -count + 1; _wrapContent.maxIndex = 0; } //fix: 按字母排序有bug:顯示錯亂 //_wrapContent.SortAlphabetically(); if (_scrollView != null) { var canDrag = _count >= GetActiveChilds(_wrapContent.transform).Count; if (count == 1) canDrag = false; _scrollView.restrictWithinPanel = canDrag; _scrollView.disableDragIfFits = !canDrag; // 只有一個的時候,不能滑動 } } private void OnInitItem(GameObject go, int wrapindex, int realindex) { var index = Mathf.Abs(realindex);// 取絕對值 CacheObject2Index[go] = index; if (CheckActive(go, index) && _hasRefresh) { DoRender(go, index); } } /// <summary> /// 檢查是否應該隱藏起來 /// </summary> private bool CheckActive(GameObject go, int index) { bool needActive = index <= (_count - 1);//小於總數才顯示 go.SetActive(needActive); return needActive; } //觸發渲染事件 private void DoRender(GameObject go, int index) { if (OnRenderEvent == null) { Debug.LogError("UIWrapContent必須設置RenderFunc!"); return; } OnRenderEvent(go, index); } /// <summary> /// 執行刷新,單個單個地渲染 /// </summary> /// <param name="count"></param> /// <param name="invertOrder">反轉:當有Scrollbar時才設置此值。指scrollbar的拖動方向,反轉有bug,需完善</param> public void Refresh(int count, bool invertOrder = false) { SetCount(count, invertOrder); //fix:使用GetEnumerator 替代foreach,減少GC var enumerator = CacheObject2Index.GetEnumerator(); while (enumerator.MoveNext()) { if (CheckActive(enumerator.Current.Key, enumerator.Current.Value)) { DoRender(enumerator.Current.Key, enumerator.Current.Value); } } _hasRefresh = true; } //強制設置scrollview是否可以滑動, //fix 前面在SetCount中有設此值,但判斷依據不一定 public void CanDragScrollview(bool canDrag) { if (_scrollView != null) { _scrollView.restrictWithinPanel = canDrag; _scrollView.disableDragIfFits = !canDrag; // 只有一個的時候,不能滑動 } } public static UIWrapContentHelper Create(UIWrapContent uiWrapContent) { return new UIWrapContentHelper(uiWrapContent); } // 獲取一個Transfrom下所有active=true的child public static List<GameObject> GetActiveChilds(Transform parent) { var list = new List<GameObject>(); if (parent == null) return list; var max = parent.childCount; for (int idx = 0; idx < max; idx++) { var childObj = parent.GetChild(idx).gameObject; if (childObj.activeInHierarchy) list.Add(childObj); } return list; } }
組件使用
如果需要每次打開UI時,復位UIScrollView到初始狀態,請調用 WrapContentHelper.ResetScroll();
OnRenderWrapContent 是具體的渲染邏輯
using System; using System.Collections.Generic; using Umeng; using UnityEngine; using System.Collections; public class CUIFriendList : CUINavController { private UIWrapContent WrapContent; private CUIWrapContentHelper WrapContentHelper; private List<CPartnerVo> CachePartnerVoList;//顯示的數據 //初始化 public override void OnInit() { base.OnInit(); WrapContent = GetControl<UIWrapContent>("ListPanel"); WrapContentHelper = CUIWrapContentHelper.Create(WrapContent); WrapContentHelper.RenderFunc = OnRenderWrapContent; } //界面打開前播放動畫 public override void BeforeShowTween(object[] onOpenArgs, System.Action doNext) { //NOTE 重設Scrollview的位置 WrapContentHelper.ResetScroll(); RefreshUI(); //其它的業務邏輯 base.BeforeShowTween(onOpenArgs, doNext); } //刷新列表 private void RefreshUI() { var max = CachePartnerVoList.Count;//要渲染數據 WrapContentHelper.Refresh(max); } //具體的渲染邏輯 private void OnRenderWrapContent(GameObject gameObj, int idx) { if (idx >= CachePartnerVoList.Count) { gameObj.SetActive(false); Debug.LogWarning("超出索引"); return; } var partnerVo = CachePartnerVoList[idx]; var trans = gameObj.transform; //TODO 執行具體的渲染邏輯 //eg if(trans == null) return; var NameLabel_=trans.FindChild("NameLabel"); if(NameLabel_) { NameLabel_.GetComponent<UILabel>().text=partnerVo.Name; } //....... } }