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

介紹

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

此外支持了ScrollBar文虏,支持橫向瞒瘸、縱向及正反向拒炎。



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


最原始版本的代碼是@ivomarel的InfinityScroll。我改到后來(lái)谎柄,基本和原始版沒(méi)啥相同的了丁侄。

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

支持了GridLayout

在啟動(dòng)時(shí)檢查錨點(diǎn)和軸心朝巫,方便使用

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

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

支持反向滑動(dòng)

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

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

警告: 為了解決原始代碼回拉卡頓的問(wèn)題,我直接復(fù)制了一份UGUI中的ScrollRect代碼揪荣,而沒(méi)有繼承筷黔。這是因?yàn)槔系淖龇ㄊ窃趏nDrag里停止并立即啟動(dòng)滾動(dòng),而我通過(guò)修改兩個(gè)私有變量保證了滑動(dòng)順暢仗颈。所有我的代碼都用==========LoopScrollRect==========這樣的注釋包起來(lái)佛舱,維護(hù)起來(lái)就像打patch了。

框架思路

和UGUI自帶的ScrollRect有所不同,我拆分出了LoopHorizontalScrollRect和LoopVerticalScrollRect兩個(gè)類(lèi)请祖,分別代表水平滾動(dòng)條和水平滾動(dòng)條订歪。下面我們以LoopVerticalScrollRect為例,水平版本類(lèi)似肆捕。

1. 判定cell大小

LoopScrollRect要解決的核心問(wèn)題是:如何計(jì)算每個(gè)元素的大小刷晋。這里我使用了Content Size Fitter配合Layout Element來(lái)控制每個(gè)cell的長(zhǎng)寬,因此對(duì)于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;

}

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

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

對(duì)于每個(gè)ScrollRect,其實(shí)只需要考慮在頭部和尾部是否需要增加或者刪除元素诈豌。在這里以頭部的各種情況為例進(jìn)行解釋仆救,因?yàn)樵谡蚧瑒?dòng)情況下,必須保證在修改元素之后整個(gè)ScrollRect內(nèi)容顯示一致不跳變矫渔;這些情況比尾部處理會(huì)麻煩一些彤蔽。

NewItemAtStart函數(shù)實(shí)現(xiàn)了在頭部增加一個(gè)(或一行,針對(duì)GridLayout)元素庙洼,并返回這些元素的高度顿痪;DeleteItemAtStart代表刪除頭部的一個(gè)元素。需要注意的是油够,在修改頭部元素之后要及時(shí)修改content的anchoredPosition蚁袭,這樣才能保證整個(gè)內(nèi)容區(qū)域不會(huì)因?yàn)槎嗔嘶蛘呱倭艘恍卸a(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. 何時(shí)需要增刪元素

這里需要有兩個(gè)概念viewBounds和contentBounds:前者是指ScrollRect本身的大小石咬,一般也對(duì)應(yīng)Mask揩悄;后者是指ScrollRect里所有cell組成的內(nèi)容部分的大小。在這個(gè)基礎(chǔ)上就簡(jiǎn)單了:如果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. 對(duì)象池交互

在新建cell和銷(xiāo)毀cell的時(shí)候蹬挺,使用對(duì)象池來(lái)避免內(nèi)存碎片;同時(shí)這里使用了SendMessage來(lái)向每個(gè)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. 滾動(dòng)條相關(guān)

這塊我其實(shí)是估算的,根據(jù)當(dāng)前的長(zhǎng)度和當(dāng)前元素個(gè)數(shù)/總個(gè)數(shù)按照比例縮放,這個(gè)在所有cell大小一致的情況下是沒(méi)有問(wèn)題的晰韵;但是如果大小不一致我就無(wú)法得到精確結(jié)果发乔,所以會(huì)產(chǎn)生一定抖動(dòng)。我暫時(shí)沒(méi)有更好辦法雪猪,因?yàn)榈玫降男畔⒕褪遣粔蛴谩?/p>

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

我主要遇到了兩個(gè)坑:

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

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

使用示例

以豎直滾動(dòng)條為例,介紹一下步驟官觅。如果覺(jué)得麻煩的話纵菌,直接打開(kāi)DemoScene復(fù)制粘貼就好。當(dāng)然你也可以干掉EasyObjPool休涤,自己控制生成和銷(xiāo)毀咱圆。

1. 準(zhǔn)備好Prefabs

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

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


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

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

Prefab Pool:?EasyObjPool物體

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

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

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

ReverseDirection:?如果是從下往上或者從右往左拖動(dòng)跺涤,就打開(kāi)這里

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

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


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

無(wú)盡模式

如果需要無(wú)限滾動(dòng)模式牛曹,將totalCount設(shè)為負(fù)數(shù)即可佛点。

其他參考

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末吕座,一起剝皮案震驚了整個(gè)濱河市虐译,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吴趴,老刑警劉巖漆诽,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異锣枝,居然都是意外死亡厢拭,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)撇叁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)供鸠,“玉大人,你說(shuō)我怎么就攤上這事陨闹±阄妫” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵正林,是天一觀的道長(zhǎng)泡一。 經(jīng)常有香客問(wèn)我颤殴,道長(zhǎng)觅廓,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任涵但,我火速辦了婚禮杈绸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘矮瘟。我一直安慰自己瞳脓,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布澈侠。 她就那樣靜靜地躺著劫侧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪哨啃。 梳的紋絲不亂的頭發(fā)上烧栋,一...
    開(kāi)封第一講書(shū)人閱讀 51,737評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音拳球,去河邊找鬼审姓。 笑死,一個(gè)胖子當(dāng)著我的面吹牛祝峻,可吹牛的內(nèi)容都是我干的魔吐。 我是一名探鬼主播扎筒,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼酬姆!你這毒婦竟也來(lái)了嗜桌?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤轴踱,失蹤者是張志新(化名)和其女友劉穎症脂,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體淫僻,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡诱篷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了雳灵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片棕所。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖悯辙,靈堂內(nèi)的尸體忽然破棺而出琳省,到底是詐尸還是另有隱情,我是刑警寧澤躲撰,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布针贬,位于F島的核電站,受9級(jí)特大地震影響拢蛋,放射性物質(zhì)發(fā)生泄漏桦他。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一谆棱、第九天 我趴在偏房一處隱蔽的房頂上張望快压。 院中可真熱鬧,春花似錦垃瞧、人聲如沸蔫劣。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)脉幢。三九已至,卻和暖如春嗦锐,著一層夾襖步出監(jiān)牢的瞬間嫌松,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工意推, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留豆瘫,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓菊值,卻偏偏與公主長(zhǎng)得像外驱,于是被迫代替她去往敵國(guó)和親育灸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容