[Unity優(yōu)化] 如何優(yōu)化UGUI的ScrollRect

介紹

每個元素知道自己的序號,可以根據(jù)需要修改自己的內(nèi)容、大小等信息。

此外支持了ScrollBar,支持橫向、縱向及正反向。



在關(guān)閉Mask后可以看到,只有當(dāng)需要的時候才動態(tài)實例化元素,使用完后回收。


最原始版本的代碼是@ivomarel的InfinityScroll。我改到后來,基本和原始版沒啥相同的了。

原代碼使用了sizeDelta作為大小,但是這個在錨點不重合情況下是不成立的

支持了GridLayout

在啟動時檢查錨點和軸心,方便使用

修復(fù)了原代碼在往前拖拽會卡頓的問題

優(yōu)化代碼,提升性能

支持反向滑動

支持ScrollBar (在無盡模式下不起作用;如果元素大小不一致會出現(xiàn)滾動條瑕疵)

此外,我修改了Easy Object Pool作為池子,循環(huán)利用元素。

警告: 為了解決原始代碼回拉卡頓的問題,我直接復(fù)制了一份UGUI中的ScrollRect代碼,而沒有繼承。這是因為老的做法是在onDrag里停止并立即啟動滾動,而我通過修改兩個私有變量保證了滑動順暢。所有我的代碼都用==========LoopScrollRect==========這樣的注釋包起來,維護(hù)起來就像打patch了。

框架思路

和UGUI自帶的ScrollRect有所不同,我拆分出了LoopHorizontalScrollRect和LoopVerticalScrollRect兩個類,分別代表水平滾動條和水平滾動條。下面我們以LoopVerticalScrollRect為例,水平版本類似。

1. 判定cell大小

LoopScrollRect要解決的核心問題是:如何計算每個元素的大小。這里我使用了Content Size Fitter配合Layout Element來控制每個cell的長寬,因此對于GridLayout直接取高度,否則取Preferred Height。需要注意的是,除了元素本身的大小之外,我們還要將padding考慮進(jìn)去。

protected override float GetSize(RectTransform item)

{

? ? float size = contentSpacing;

? ? if (m_GridLayout != null)

? ? {

? ? ? ? size += m_GridLayout.cellSize.y;

? ? }

? ? else

? ? {

? ? ? ? size += LayoutUtility.GetPreferredHeight(item);

? ? }

? ? return size;

}

這個其實也是最核心的一個地方:在能夠準(zhǔn)確計算格子大小的基礎(chǔ)上,后續(xù)工作就好實現(xiàn)了。

2. 如何優(yōu)雅的增刪元素

對于每個ScrollRect,其實只需要考慮在頭部和尾部是否需要增加或者刪除元素。在這里以頭部的各種情況為例進(jìn)行解釋,因為在正向滑動情況下,必須保證在修改元素之后整個ScrollRect內(nèi)容顯示一致不跳變;這些情況比尾部處理會麻煩一些。

NewItemAtStart函數(shù)實現(xiàn)了在頭部增加一個(或一行,針對GridLayout)元素,并返回這些元素的高度;DeleteItemAtStart代表刪除頭部的一個元素。需要注意的是,在修改頭部元素之后要及時修改content的anchoredPosition,這樣才能保證整個內(nèi)容區(qū)域不會因為多了或者少了一行而產(chǎn)生跳變。

protected float NewItemAtStart()

{

? ? float size = 0;

? ? for (int i = 0; i < contentConstraintCount; i++)

? ? {

? ? ? ? // Get Element from ObjectPool

? ? }

? ? if (!reverseDirection)

? ? {

? ? ? ? // Modify content.anchoredPosition

? ? }

? ? return size;

}

protected float DeleteItemAtStart()

{

? ? float size = 0;

? ? for (int i = 0; i < contentConstraintCount; i++)

? ? {

? ? ? ? // Return Element to ObjectPool

? ? }

? ? if (!reverseDirection)

? ? {

? ? ? ? // Modify content.anchoredPosition

? ? }

? ? return size;

}

3. 何時需要增刪元素

這里需要有兩個概念viewBounds和contentBounds:前者是指ScrollRect本身的大小,一般也對應(yīng)Mask;后者是指ScrollRect里所有cell組成的內(nèi)容部分的大小。在這個基礎(chǔ)上就簡單了:如果contentBounds的最上面比viewBounds的最上面要低,那么嘗試在頂部增加元素;如果contentBounds的最上面比viewBounds的最上面高很多,那么嘗試刪除元素。

protected override bool UpdateItems(Bounds viewBounds, Bounds contentBounds)

{

? ? bool changed = false;

? ? // cases for NewItemAtEnd/DeleteItemAtEnd

? ? if (viewBounds.max.y > contentBounds.max.y - 1)

? ? {

? ? ? ? float size = NewItemAtStart();

? ? ? ? if (size > 0)

? ? ? ? {

? ? ? ? ? ? changed = true;

? ? ? ? }

? ? }

? ? else if (viewBounds.max.y < contentBounds.max.y - threshold)

? ? {

? ? ? ? float size = DeleteItemAtStart();

? ? ? ? if (size > 0)

? ? ? ? {

? ? ? ? ? ? changed = true;

? ? ? ? }

? ? }

? ? return changed;

}

4. 對象池交互

在新建cell和銷毀cell的時候,使用對象池來避免內(nèi)存碎片;同時這里使用了SendMessage來向每個cell發(fā)送必須的信息,保證數(shù)據(jù)的正確性。

private void SendMessageToNewObject(Transform go, int idx)

{

? ? go.SendMessage("ScrollCellIndex", idx);

}

private void ReturnObjectAndSendMessage(Transform go)

{

? ? go.SendMessage("ScrollCellReturn", SendMessageOptions.DontRequireReceiver);

? ? prefabPool.ReturnObjectToPool(go.gameObject);

}

private RectTransform InstantiateNextItem(int itemIdx)

{

? ? RectTransform nextItem = prefabPool.GetObjectFromPool(prefabPoolName).GetComponent<RectTransform>();

? ? nextItem.transform.SetParent(content, false);

? ? nextItem.gameObject.SetActive(true);

? ? SendMessageToNewObject(nextItem, itemIdx);

? ? return nextItem;

}

5. 滾動條相關(guān)

這塊我其實是估算的,根據(jù)當(dāng)前的長度和當(dāng)前元素個數(shù)/總個數(shù)按照比例縮放,這個在所有cell大小一致的情況下是沒有問題的;但是如果大小不一致我就無法得到精確結(jié)果,所以會產(chǎn)生一定抖動。我暫時沒有更好辦法,因為得到的信息就是不夠用。

6. 其他細(xì)節(jié)

我主要遇到了兩個坑:

增加或者刪除元素之后,有時候需要強(qiáng)行調(diào)用Canvas.ForceUpdateCanvases()刷新下。

注意不要在Build Canvas過程中再次修改元素,從而再次觸發(fā)Build Canvas。

使用示例

以豎直滾動條為例,介紹一下步驟。如果覺得麻煩的話,直接打開DemoScene復(fù)制粘貼就好。當(dāng)然你也可以干掉EasyObjPool,自己控制生成和銷毀。

1. 準(zhǔn)備好Prefabs

每個物體上需要貼上Layout Element并指定preferred width/height。

貼上一個腳本接受void ScrollCellIndex (int idx) 消息,從而對每個位置的元素根據(jù)需要靈活修改。


2. 在Hierarchy里右鍵,選擇UI/Loop Horizontal Scroll Rect或UI/Loop Vertical Scroll Rect即可。使用Component菜單里的也是一樣的。

Init in Start:?啟動時自動調(diào)用Refill cells初始化

Prefab Pool:?EasyObjPool物體

Prefab Pool Name:?第二步中對應(yīng)的Cell Prefab名字

Total Count:?總共能有多少物體,范圍0 ~ TotalCount-1

Threshold:?兩端預(yù)留出來的緩存量(像素數(shù))

ReverseDirection:?如果是從下往上或者從右往左拖動,就打開這里

Clear Cells:?清除已有元素,恢復(fù)到未初始化狀態(tài)

Refill Cells:?初始化并填充元素


如果是正向滑動,就設(shè)置pivot為1;否則設(shè)為0并打開ReverseDirection。我強(qiáng)烈建議你試試在播放狀態(tài)下修改這些參數(shù)。

無盡模式

如果需要無限滾動模式,將totalCount設(shè)為負(fù)數(shù)即可。

其他參考

后來搜了下,發(fā)現(xiàn)網(wǎng)上也有人提到過UGUI ScrollRect 優(yōu)化(http://blog.csdn.net/subsystemp/article/details/46912479),不過他的策略是監(jiān)聽ScrollRect的value,然后禁用范圍外的cell。最后作者也提到改成動態(tài)加載策略。這種基于value的做法我不太確認(rèn)在在滾動前動態(tài)添加新元素的時候是否會出現(xiàn)問題。

文末,再次感謝錢康來的分享,如果您有任何獨到的見解或者發(fā)現(xiàn)也歡迎聯(lián)系我們,一起探討。(QQ群465082844)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容