網(wǎng)站首頁 編程語言 正文
無限滾動復用列表
Demo展示
前言
游戲中有非常多的下拉滾動菜單,比如成就列表,任務列表,以及背包倉庫之類;如果列表內容非常豐富,會占用大量內存,這篇無限滾動復用ScrollView就是解決這種問題;還可以用來做朋友圈,聊天等;
一般情況,ScrollView中每個Item的大小是一直的,使用ContentSizeFillter組件足夠解決大部分問題;
如果每個Item大小不一致,問題就復雜起來,需要做滾動位置判斷,我這里做了大小適應;
設計思路
1.將數(shù)據(jù)部分和滾動邏輯部分分離開,數(shù)據(jù)設計成泛型類;
2.在ScrollView組件上添加ScrollView腳本,控制Item的添加和刪除,分為頭部和尾部;
3.在每個Item上添加ScrollItem腳本,重寫更新數(shù)據(jù)方法,同時監(jiān)聽自身是否為頭部或者尾部;
4.如果為頭部或者尾部,且超界通過委托調用ScrollView腳本中的添加或刪除Item方法;
關鍵基類
1.ScrollData
負責整個列表的數(shù)據(jù)管理,分為總數(shù)據(jù)和現(xiàn)實數(shù)據(jù)兩個鏈表,增刪查改方法;泛型類方便復用;
這里使用LinkedList方便查找并返回頭尾節(jié)點;
全部代碼:
public class ScrollData{ public List allDatas; public LinkedList curDatas; public ScrollData() { allDatas = new List (); curDatas = new LinkedList (); //加載數(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) allDatas.AddRange(t.ToArray()); //添加單條數(shù)據(jù) public void AddData(T t) allDatas.Insert(0,t); curDatas.AddFirst(t); //情況當前顯示節(jié)點 public void ClearCurData() curDatas.Clear(); //獲取當前顯示鏈表的第一個數(shù)據(jù)在總數(shù)據(jù)中的下標 public int GetFirstIndex() return allDatas.IndexOf(t); }
2.ScrollView
關鍵字段:
scrollItemGo //每個Item的預制體 content //scrollRect下的Content spacing //每個Item的間隔 isStart //是否第一次加載
方法:
GetChildItem;
1.獲取一個Item的預制體,先從content的子物體中尋找active為false的物體,如果沒有則根據(jù)scrollItemGo克隆一個;
2.創(chuàng)建新Item時,獲取ScrollItem組件,賦值其中的參數(shù)(四個委托),并初始化;
OnAddHead;OnRemoveHead;OnAddEnd;OnRemoveEnd;
委托方法:
1.調用ScrollData中GetHeadData方法,獲得頭數(shù)據(jù);
2.找到content中第一個節(jié)點;
3.調用GetChildItem方法獲得item的實例;
4.SetAsFirstSibling,將實例設置為首節(jié)點,同時調用RefreshData,刷新數(shù)據(jù);
5.根據(jù)item 的寬度做自適應(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().content; spacing = 15; OnAddHead(); } private GameObject GetChildItem() //查找是否有未回收的子節(jié)點 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 (scrollItemGo,content.transform); ScrollViewItem scrollItem = childItem.GetComponent (); if (scrollItem == null) scrollItem = childItem.AddComponent (); scrollItem.onAddHead += OnAddHead; scrollItem.onRemoveHead += OnRemoveHead; scrollItem.onAddEnd += OnAddEnd; scrollItem.onRemoveEnd += OnRemoveEnd; scrollItem.Init(); childItem.GetComponent ().anchorMin = new Vector2(0.5f, 1); childItem.GetComponent ().anchorMax = new Vector2(0.5f, 1); childItem.GetComponent ().pivot = new Vector2(0, 1); childItem.transform.localScale = Vector3.one; childItem.transform.localPosition = Vector3.zero; //-----設置寬高——加載數(shù)據(jù) return childItem; private void OnAddHead() Data data = this.GetComponent ().scrollData.GetHeadData(); if (data != null) Transform first = FindFirst(); //----first 不為 數(shù)據(jù)頭---在data中做了 GameObject obj = GetChildItem(); obj.GetComponent ().RefreshData(data); obj.transform.SetAsFirstSibling(); RectTransform objRect = obj.GetComponent (); 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 ().scrollData; if (scrollData.RemoveHeadData()) Transform tf = FindFirst(); if (tf != null) tf.gameObject.SetActive(false); private void OnAddEnd() Data data = this.GetComponent ().scrollData.GetEndData(); Transform end = FindEnd(); //----end 不為 數(shù)據(jù)尾在data中做了 obj.transform.SetAsLastSibling(); float height = end.GetComponent ().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 ().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 ().GetWorldCorners(rectC); content.GetWorldCorners(contentC); if (rectC[0].y < contentC[0].y) return true; return false; }
3.ScrollItem
關鍵字段:四個委托
public Action onAddHead; public Action onRemoveHead; public Action onAddEnd; public Action onRemoveEnd;
關鍵方法:
OnRecyclingItem;
1.判斷自身是否為頭尾節(jié)點;
2.判斷自身是否超界,超界需要隱藏自身;
3.判斷自身與邊界距離,是否添加節(jié)點;
關鍵API:
RectTransform.GetWorldCorners(Vector3[4])
獲取UI對象四個頂點的世界坐標,下標對應的位置;
全部代碼:
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(); rect = this.GetComponent (); 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 (); rectCorners = new Vector3[4]; rect.GetWorldCorners(rectCorners); if (IsFirst()) { if (rectCorners[0].y > viewStart) { //隱藏頭節(jié)點 if (onRemoveHead != null) onRemoveHead(); } if (rectCorners[1].y < viewStart) { //添加頭節(jié)點-頭節(jié)點不為數(shù)據(jù)起始點 if (onAddHead != null) onAddHead(); } } if (IsLast()) { if (rectCorners[0].y > viewEnd) { //添加尾節(jié)點-尾節(jié)點不為數(shù)據(jù)末尾 if (onAddEnd != null) onAddEnd(); } if (rectCorners[1].y < viewEnd) { //隱藏尾節(jié)點 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 (); 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); } }
測試類
初始化數(shù)據(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); } }
添加三個按鈕,及相應的響應方法;
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中運行,也可以用插值;
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; } }
坑點
1.ScrollView回滾設置延遲;
回滾判斷是通過verticalNormalizedPosition的API,更改這個值后需要間隔一幀才會修改,因為可能導致判斷兩次;
解決方法,延遲調用1s——Invoke;
2.錨點設置;
錨點的設置以及UI的自適應會直接影響項目回滾的方向和位置;
大部分位置出錯都是因為錨點設置錯誤;
3.數(shù)據(jù)需要網(wǎng)絡請求,自適應會失效;
網(wǎng)絡數(shù)據(jù)一般都是異步,所以判斷會做多次,因此數(shù)據(jù)上要求提前計算好item的寬度;
項目工程我上傳到Gitee,可自行下載學習;https://gitee.com/small-perilla/scroll-view
以上是我對滾動復用組件的總結,如果有更好的意見,歡迎給作者評論留言;
原文鏈接:https://www.cnblogs.com/littleperilla/p/15348538.html
相關推薦
- 2023-07-29 iview的表格實現(xiàn)單元格行編輯功能
- 2022-12-12 Python實現(xiàn)打印九九乘法表的不同方法總結_python
- 2022-10-03 正則表達式中^和$的含義與實例代碼_正則表達式
- 2022-06-16 .Net?Core解決WebAPI中返回時間格式帶T的問題_實用技巧
- 2023-08-30 Linux下查找和刪除7天以前的文件
- 2022-06-19 C#中的checksum計算公式_C#教程
- 2022-11-29 ASP.NET?MVC把數(shù)據(jù)庫中枚舉項的數(shù)字轉換成文字_基礎應用
- 2022-09-26 利用C++實現(xiàn)?然連接操作算法_C 語言
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細win安裝深度學習環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結構-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支