介紹
每個元素知道自己的序號,可以根據(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)。