解決Unity無限滾動(dòng)復(fù)用列表的問題
無限滾動(dòng)復(fù)用列表
Demo展示


前言
游戲中有非常多的下拉滾動(dòng)菜單,比如成就列表,任務(wù)列表,以及背包倉庫之類;如果列表內(nèi)容非常豐富,會(huì)占用大量?jī)?nèi)存,這篇無限滾動(dòng)復(fù)用ScrollView就是解決這種問題;還可以用來做朋友圈,聊天等;
一般情況,ScrollView中每個(gè)Item的大小是一直的,使用ContentSizeFillter組件足夠解決大部分問題;
如果每個(gè)Item大小不一致,問題就復(fù)雜起來,需要做滾動(dòng)位置判斷,我這里做了大小適應(yīng);
設(shè)計(jì)思路
1.將數(shù)據(jù)部分和滾動(dòng)邏輯部分分離開,數(shù)據(jù)設(shè)計(jì)成泛型類;
2.在ScrollView組件上添加ScrollView腳本,控制Item的添加和刪除,分為頭部和尾部;
3.在每個(gè)Item上添加ScrollItem腳本,重寫更新數(shù)據(jù)方法,同時(shí)監(jiān)聽自身是否為頭部或者尾部;
4.如果為頭部或者尾部,且超界通過委托調(diào)用ScrollView腳本中的添加或刪除Item方法;
關(guān)鍵基類
1.ScrollData
負(fù)責(zé)整個(gè)列表的數(shù)據(jù)管理,分為總數(shù)據(jù)和現(xiàn)實(shí)數(shù)據(jù)兩個(gè)鏈表,增刪查改方法;泛型類方便復(fù)用;
這里使用LinkedList方便查找并返回頭尾節(jié)點(diǎn);
全部代碼:
public class ScrollData<T>
{
public List<T> allDatas;
public LinkedList<T> curDatas;
public ScrollData()
{
allDatas = new List<T>();
curDatas = new LinkedList<T>();
//加載數(shù)據(jù);
}
//獲取頭數(shù)據(jù)
public T GetHeadData()
if(allDatas.Count == 0)
return default(T);
if (curDatas.Count == 0)
{
T head = allDatas[0];
curDatas.AddFirst(head);
return head;
}
T t = curDatas.First.Value;
int index = allDatas.IndexOf(t);
if (index != 0)
T head = allDatas[index - 1];
return default(T);
//移出頭數(shù)據(jù)
public bool RemoveHeadData()
if (curDatas.Count == 0 || curDatas.Count == 1)
return false;
curDatas.RemoveFirst();
return true;
//獲取尾部數(shù)據(jù)
public T GetEndData()
if (allDatas.Count == 0)
T end = allDatas[0];
curDatas.AddLast(end);
return end;
T t = curDatas.Last.Value;
if (index != allDatas.Count - 1)
T end = allDatas[index + 1];
//移出尾部數(shù)據(jù)
public bool RemoveEndData()
curDatas.RemoveLast();
//添加數(shù)據(jù),通過數(shù)組
public void AddData(T[] t)
allDatas.AddRange(t);
//添加數(shù)據(jù),通過鏈表
public void AddData(List<T> t)
allDatas.AddRange(t.ToArray());
//添加單條數(shù)據(jù)
public void AddData(T t)
allDatas.Insert(0,t);
curDatas.AddFirst(t);
//情況當(dāng)前顯示節(jié)點(diǎn)
public void ClearCurData()
curDatas.Clear();
//獲取當(dāng)前顯示鏈表的第一個(gè)數(shù)據(jù)在總數(shù)據(jù)中的下標(biāo)
public int GetFirstIndex()
return allDatas.IndexOf(t);
}2.ScrollView
關(guān)鍵字段:
scrollItemGo //每個(gè)Item的預(yù)制體 content //scrollRect下的Content spacing //每個(gè)Item的間隔 isStart //是否第一次加載
方法:
GetChildItem;
1.獲取一個(gè)Item的預(yù)制體,先從content的子物體中尋找active為false的物體,如果沒有則根據(jù)scrollItemGo克隆一個(gè);
2.創(chuàng)建新Item時(shí),獲取ScrollItem組件,賦值其中的參數(shù)(四個(gè)委托),并初始化;
OnAddHead;OnRemoveHead;OnAddEnd;OnRemoveEnd;
委托方法:
1.調(diào)用ScrollData中GetHeadData方法,獲得頭數(shù)據(jù);
2.找到content中第一個(gè)節(jié)點(diǎn);
3.調(diào)用GetChildItem方法獲得item的實(shí)例;
4.SetAsFirstSibling,將實(shí)例設(shè)置為首節(jié)點(diǎn),同時(shí)調(diào)用RefreshData,刷新數(shù)據(jù);
5.根據(jù)item 的寬度做自適應(yīng)(item大小相同,只選掛載ContentSizeFitter);
全部代碼:
public class ScrollView : MonoBehaviour
{
public GameObject scrollItemGo;
private RectTransform content;
[SerializeField]
private float spacing;
private bool isStart = true;
void Start()
{
content = this.GetComponent<ScrollRect>().content;
spacing = 15;
OnAddHead();
}
private GameObject GetChildItem()
//查找是否有未回收的子節(jié)點(diǎn)
for (int i = 0; i < content.childCount; ++i)
{
GameObject tempGo = content.GetChild(i).gameObject;
if (!tempGo.activeSelf)
{
tempGo.SetActive(true);
return tempGo;
}
}
//無創(chuàng)建新的
GameObject childItem = GameObject.Instantiate<GameObject>(scrollItemGo,content.transform);
ScrollViewItem scrollItem = childItem.GetComponent<ScrollViewItem>();
if (scrollItem == null)
scrollItem = childItem.AddComponent<ScrollViewItem>();
scrollItem.onAddHead += OnAddHead;
scrollItem.onRemoveHead += OnRemoveHead;
scrollItem.onAddEnd += OnAddEnd;
scrollItem.onRemoveEnd += OnRemoveEnd;
scrollItem.Init();
childItem.GetComponent<RectTransform>().anchorMin = new Vector2(0.5f, 1);
childItem.GetComponent<RectTransform>().anchorMax = new Vector2(0.5f, 1);
childItem.GetComponent<RectTransform>().pivot = new Vector2(0, 1);
childItem.transform.localScale = Vector3.one;
childItem.transform.localPosition = Vector3.zero;
//-----設(shè)置寬高——加載數(shù)據(jù)
return childItem;
private void OnAddHead()
Data data = this.GetComponent<Test>().scrollData.GetHeadData();
if (data != null)
Transform first = FindFirst();
//----first 不為 數(shù)據(jù)頭---在data中做了
GameObject obj = GetChildItem();
obj.GetComponent<ScrollViewItem>().RefreshData(data);
obj.transform.SetAsFirstSibling();
RectTransform objRect = obj.GetComponent<RectTransform>();
float height = objRect.sizeDelta.y;
if (first != null)
obj.transform.localPosition = first.localPosition + new Vector3(0, height + spacing, 0);
if (isStart)
content.sizeDelta += new Vector2(0, height + spacing);
isStart = false;
private void OnRemoveHead()
var scrollData = this.GetComponent<Test>().scrollData;
if (scrollData.RemoveHeadData())
Transform tf = FindFirst();
if (tf != null)
tf.gameObject.SetActive(false);
private void OnAddEnd()
Data data = this.GetComponent<Test>().scrollData.GetEndData();
Transform end = FindEnd();
//----end 不為 數(shù)據(jù)尾在data中做了
obj.transform.SetAsLastSibling();
float height = end.GetComponent<RectTransform>().sizeDelta.y;
if (end != null)
obj.transform.localPosition = end.localPosition - new Vector3(0, height + spacing, 0);
//是否增加content高度
if (IsAddContentH(obj.transform))
float h = obj.GetComponent<RectTransform>().sizeDelta.y;
content.sizeDelta += new Vector2(0, h + spacing);
private void OnRemoveEnd()
if (scrollData.RemoveEndData())
Transform tf = FindEnd();
private Transform FindFirst()
if (content.GetChild(i).gameObject.activeSelf)
return content.GetChild(i);
return null;
private Transform FindEnd()
for (int i = content.childCount - 1; i >= 0; --i)
private bool IsAddContentH(Transform tf)
Vector3[] rectC = new Vector3[4];
Vector3[] contentC = new Vector3[4];
tf.GetComponent<RectTransform>().GetWorldCorners(rectC);
content.GetWorldCorners(contentC);
if (rectC[0].y < contentC[0].y)
return true;
return false;
}
3.ScrollItem
關(guān)鍵字段:四個(gè)委托
public Action onAddHead; public Action onRemoveHead; public Action onAddEnd; public Action onRemoveEnd;
關(guān)鍵方法:
OnRecyclingItem;
1.判斷自身是否為頭尾節(jié)點(diǎn);
2.判斷自身是否超界,超界需要隱藏自身;
3.判斷自身與邊界距離,是否添加節(jié)點(diǎn);

關(guān)鍵API:
RectTransform.GetWorldCorners(Vector3[4])
獲取UI對(duì)象四個(gè)頂點(diǎn)的世界坐標(biāo),下標(biāo)對(duì)應(yīng)的位置;

全部代碼:
public class ScrollViewItem : MonoBehaviour
{
private RectTransform viewRect;
private RectTransform rect;
[SerializeField]
private float viewStart;
[SerializeField]
private float viewEnd;
[SerializeField]
private Vector3[] rectCorners;
public Action onAddHead;
public Action onRemoveHead;
public Action onAddEnd;
public Action onRemoveEnd;
public Text nameT;
public Text inputT;
void Start()
{
Init();
}
public void Init()
{
viewRect = transform.parent.parent.GetComponent<RectTransform>();
rect = this.GetComponent<RectTransform>();
rectCorners = new Vector3[4];
viewRect.GetWorldCorners(rectCorners);
viewStart = rectCorners[1].y;
viewEnd = rectCorners[0].y;
}
void Update()
{
OnRecyclingItem();
}
//超界變false;
private void OnRecyclingItem()
{
rect = this.GetComponent<RectTransform>();
rectCorners = new Vector3[4];
rect.GetWorldCorners(rectCorners);
if (IsFirst())
{
if (rectCorners[0].y > viewStart)
{
//隱藏頭節(jié)點(diǎn)
if (onRemoveHead != null)
onRemoveHead();
}
if (rectCorners[1].y < viewStart)
{
//添加頭節(jié)點(diǎn)-頭節(jié)點(diǎn)不為數(shù)據(jù)起始點(diǎn)
if (onAddHead != null)
onAddHead();
}
}
if (IsLast())
{
if (rectCorners[0].y > viewEnd)
{
//添加尾節(jié)點(diǎn)-尾節(jié)點(diǎn)不為數(shù)據(jù)末尾
if (onAddEnd != null)
onAddEnd();
}
if (rectCorners[1].y < viewEnd)
{
//隱藏尾節(jié)點(diǎn)
if (onRemoveEnd != null)
onRemoveEnd();
}
}
}
private bool IsFirst()
{
for (int i = 0; i < transform.parent.childCount; ++i)
{
Transform tf = transform.parent.GetChild(i);
if (tf.gameObject.activeSelf)
{
if (tf == this.transform)
{
return true;
}
break;
}
}
return false;
}
private bool IsLast()
{
for (int i = transform.parent.childCount-1; i >= 0 ; i--)
{
Transform tf = transform.parent.GetChild(i);
if (tf.gameObject.activeSelf)
{
if (tf == this.transform)
{
return true;
}
break;
}
}
return false;
}
public bool IsInView()
{
rect = this.GetComponent<RectTransform>();
rect.GetWorldCorners(rectCorners);
if (rectCorners[1].y > viewEnd || rectCorners[0].y < viewStart)
return false;
return true;
}
public void RefreshData(Data da)
{
nameT.text = da.name;
inputT.text = da.text;
Vector2 oldSize = rect.sizeDelta;
rect.sizeDelta = new Vector2(oldSize.x, 200 + da.h);
}
}測(cè)試類
初始化數(shù)據(jù),隨機(jī)4中寬度的item;
void InitData()
{
int[] hArr = new int[4];
hArr[0] = 0;
hArr[1] = 190;
hArr[2] = 190 * 2;
hArr[3] = 190 * 3;
for (int i = 0; i < 30; ++i)
{
Data da = new Data();
da.name = "小紫蘇" + i.ToString();
da.text = "000000" + i.ToString();
int index = UnityEngine.Random.Range(0, 3);
da.h = hArr[index];
scrollData.allDatas.Add(da);
}
}添加三個(gè)按鈕,及相應(yīng)的響應(yīng)方法;
1.添加20組數(shù)據(jù)
private void AddData()
{
int[] hArr = new int[4];
hArr[0] = 0;
hArr[1] = 190;
hArr[2] = 190 * 2;
hArr[3] = 190 * 3;
Data[] newData = new Data[20];
for (int i = 0; i < 20; ++i)
{
Data da = new Data();
da.name = "小紫蘇" + i.ToString();
da.text = "000000" + i.ToString();
int index = UnityEngine.Random.Range(0, 3);
da.h = hArr[index];
newData[i] = da;
}
scrollDat回到頂部或底部需要有過程,因此需要在update中運(yùn)行,也可以用插值;
2.回到頂部
private void OnGoHead()
{
if (isGoHead)
isGoHead = false;
else
isGoHead = true;
}
private void OnGoLast()
{
if (isGoLast)
isGoLast = false;
else
isGoLast = true;
}3.回到底部
private void GoHead()
{
if (!isGoHead)
return;
float curPos = scroll.verticalNormalizedPosition;
if (curPos != 1)
{
curPos += 0.01f;
if (curPos >= 1)
{
curPos = 1;
isGoHead = false;
}
scroll.verticalNormalizedPosition = curPos;
}
}
private void GoLast()
{
if (!isGoLast)
return;
float curPos = scroll.verticalNormalizedPosition;
if (curPos != 0)
{
curPos -= 0.01f;
if (curPos <= 0)
{
curPos = 0;
isGoLast = false;
}
scroll.verticalNormalizedPosition = curPos;
}
}
坑點(diǎn)
1.ScrollView回滾設(shè)置延遲;
回滾判斷是通過verticalNormalizedPosition的API,更改這個(gè)值后需要間隔一幀才會(huì)修改,因?yàn)榭赡軐?dǎo)致判斷兩次;
解決方法,延遲調(diào)用1s——Invoke;
2.錨點(diǎn)設(shè)置;
錨點(diǎn)的設(shè)置以及UI的自適應(yīng)會(huì)直接影響項(xiàng)目回滾的方向和位置;
大部分位置出錯(cuò)都是因?yàn)殄^點(diǎn)設(shè)置錯(cuò)誤;
3.數(shù)據(jù)需要網(wǎng)絡(luò)請(qǐng)求,自適應(yīng)會(huì)失效;
網(wǎng)絡(luò)數(shù)據(jù)一般都是異步,所以判斷會(huì)做多次,因此數(shù)據(jù)上要求提前計(jì)算好item的寬度;
項(xiàng)目工程我上傳到Gitee,可自行下載學(xué)習(xí);https://gitee.com/small-perilla/scroll-view
以上是我對(duì)滾動(dòng)復(fù)用組件的總結(jié),如果有更好的意見,歡迎給作者評(píng)論留言;
到此這篇關(guān)于Unity無限滾動(dòng)復(fù)用列表的文章就介紹到這了,更多相關(guān)Unity無限滾動(dòng)列表內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#裝飾器模式(Decorator Pattern)實(shí)例教程
這篇文章主要介紹了C#裝飾器模式(Decorator Pattern),以一個(gè)完整實(shí)例形式講述了C#裝飾器模式的實(shí)現(xiàn)過程,有助于深入理解C#程序設(shè)計(jì)思想,需要的朋友可以參考下2014-09-09
C# 實(shí)現(xiàn)PPT 每一頁轉(zhuǎn)成圖片過程解析
這篇文章主要介紹了C# 實(shí)現(xiàn)PPT 每一頁轉(zhuǎn)成圖片過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-09-09
C#實(shí)現(xiàn)數(shù)字轉(zhuǎn)換漢字的示例詳解
這篇文章主要為大家詳細(xì)介紹了如何利用C#實(shí)現(xiàn)數(shù)字轉(zhuǎn)換漢字功能,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)C#有一定的幫助,感興趣的小伙伴可以跟隨小編一起了解一下2022-12-12
C#實(shí)現(xiàn)圖表中鼠標(biāo)移動(dòng)并顯示數(shù)據(jù)
這篇文章主要為大家詳細(xì)介紹了C#實(shí)現(xiàn)圖表中鼠標(biāo)移動(dòng)并顯示數(shù)據(jù),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02

