介紹
每個(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)。