原文鏈接:https://www.cnblogs.com/jingjiangtao/p/15827823.html
問題
Unity的ScrollView可以用滾動視圖的形式顯示串列,但是當串列中的資料非常多的時候,用ScrollView一次顯示出來就會卡頓,并且生成串列的速度也會變慢,
要解決這個問題,可以只顯示能看到的資料項,看不到的資料項就不加載,滑動串列時實時更新資料項,這樣就只需要創建和更新能看到的資料項,加載和滑動都不會卡頓,
本文只實作垂直滾動的串列,其它形式的串列實作方式類似,
實作方法
要實作一個通用的滾動視圖串列,有兩種代碼結構可供選擇:介面和委托,其中介面的實作方法比較傳統,委托的實作方法比較靈活,
本文選擇用介面實作,如果要用委托,可以把所有介面中定義的方法在對應的位置改成委托,
搭建UI
新建ScrollView
首先需要搭建ScrollView的UI用來測驗,新建一個場景,如果場景中沒有Canvas就新建一個,然后在Canvas下新建一個Scroll View,

接下來要對串列做一些重要設定:
1. 選中ScrollRect組件所在的物體,取消勾選Horizontal以禁用水平滾動,將Scroll Sensitivity設為50使得滑鼠滾輪滑動更靈敏,

2. 選中Content物體,先將Content物體的錨點和大小設定為和Viewport一樣大,具體操作是點擊Content物體的RectTransform組件的錨點設定工具,按住Alt,選擇圖中的布局:

這樣,Content的大小就和Viewport一樣大了
3. 再次選中Content物體,點擊Content物體的RectTransform組件錨點設定工具,按住Shift,選擇圖中的布局:

這樣就可以通過Content的PosY值獲取Content的位置了,最后放一張Content的RectTransform組件值的圖:

新建串列項
接下來需要創建串列項來顯示串列中的資料,簡單起見,串列項只包含一個表示串列項編號的文本和一個洗掉按鈕,
1. 選中Content,在Content上點擊右鍵,新建空物體,重命名為TextItem,并將空物體調整到合適的大小:

2. 對串列項進行一些重要設定:選中TextItem,點擊RectTransform組件的錨點工具,按住Shift,選擇圖中的布局:

這是設定完成的RectTransform組件值:

3. 在TextItem中添加文本和按鈕:

4. 在Assets目錄下新建Prefabs目錄,把TextItem物體拖到這個目錄中,做成一個預制體,然后刪掉Content下的TextItem:

5. 在Canvas下新建一個按鈕,命名為AddBtn,位置如圖,用來實作添加串列項的功能:

至此,用于測驗的UI搭建完畢,
代碼實作
存盤滾動視圖串列項的類 LoopScrollItem.cs
這個類用于保存單獨串列項中的資料,需要用串列項的GameObject初始化,在這個例子中,串列項的資料只有它自身的RectTransform和GameObject,如果要加新的資料,可以直接在這個類中暴露出來,方便訪問,
namespace LoopScrollTest.LoopScroll { public class LoopScrollItem { public RectTransform RectTransform => _transform; private RectTransform _transform; private GameObject _gameObject; public LoopScrollItem(GameObject gameObject) { _gameObject = gameObject; _transform = _gameObject.transform as RectTransform; } } }
用于滾動視圖腳本呼叫的介面 ILoopScrollView.cs
滾動串列的介面,包含幾種對串列的操作,如果有其它操作,可以在介面中宣告,并在LoopScrollView中呼叫,
namespace LoopScrollTest.LoopScroll { public interface ILoopScrollView { /// <summary> /// 用給定索引處的資料更新給定的串列項 /// </summary> void UpdateItemContent(int index, LoopScrollItem item); /// <summary> /// 用給定索引處的資料生成串列項并回傳 /// </summary> GameObject InitScrollViewList(int index); /// <summary> /// 生成一個新的串列項并回傳 /// </summary> GameObject InitOneItem(); /// <summary> /// 洗掉指定的串列項 /// </summary> void DeleteOneItem(Transform item); /// <summary> /// 洗掉一個串列項之后處理其它所有的串列項 /// </summary> void OtherItemAfterDeleteOne(LoopScrollItem item); } }
控制滾動視圖的腳本 LoopScrollView.cs
滾動視圖的主要類,繼承了MonoBehaviour,并且需要和ScrollRect組件掛在同一個物體上,并將ScrollRect組件拖到參考槽中,這個類處理了對滾動視圖的所有操作,并呼叫了介面中的方法,
namespace LoopScrollTest.LoopScroll { [RequireComponent(typeof(ScrollRect))] public class LoopScrollView : MonoBehaviour { public ScrollRect scrollRect; // 串列項陣列 protected List<LoopScrollItem> _items = new List<LoopScrollItem>(); protected float _itemHeight; protected int _visibleCount; protected float _visibleHeight; protected int _sourceListCount; // 串列操作介面型別的實體,需要在初始化時賦值 protected ILoopScrollView _scrollViewOperate; protected virtual void Update() { RefreshGestureScrollView(); } /// <summary> /// 初始化回圈串列的數值和參考 /// </summary> public virtual void InitScrollView(float itemHeight, ILoopScrollView scrollViewOperate) { _itemHeight = itemHeight; _visibleHeight = (scrollRect.transform as RectTransform).rect.height; _visibleCount = (int)(_visibleHeight / _itemHeight) + 1; _scrollViewOperate = scrollViewOperate; } /// <summary> /// 初始化回圈串列的資料源 /// </summary> public virtual void InitScrollViewList(int sourceListCount) { _sourceListCount = sourceListCount; int generateCount = ResizeContent(); scrollRect.content.anchoredPosition = Vector2.zero; _items.Clear(); for (int i = 0; i < generateCount; i++) { GameObject itemGameObject = _scrollViewOperate.InitScrollViewList(i); LoopScrollItem item = new LoopScrollItem(itemGameObject); float itemY = -i * _itemHeight; item.RectTransform.anchoredPosition = new Vector2(scrollRect.content.anchoredPosition.x, itemY); _items.Add(item); } } /// <summary> /// 將指定索引的項對齊到串列界面的頂部 /// </summary> public virtual void MoveIndexToTop(int index) { float contentY = index * _itemHeight; scrollRect.content.anchoredPosition = new Vector2(scrollRect.content.anchoredPosition.x, contentY); RefreshGestureScrollView(); } /// <summary> /// 將指定索引的項對齊到串列界面的底部 /// </summary> public virtual void MoveIndexToBottom(int index) { float contentY = (index + 1) * _itemHeight - _visibleHeight; contentY = contentY < 0 ? 0f : contentY; scrollRect.content.anchoredPosition = new Vector2(scrollRect.content.anchoredPosition.x, contentY); RefreshGestureScrollView(); } /// <summary> /// 判斷指定的索引是否需要聚焦到底部,如果需要就對齊 /// </summary> public virtual void MoveToBottomIfNeeded(int index) { float itemY = -(index + 1) * _itemHeight; float bottomY = -(scrollRect.content.anchoredPosition.y + _visibleHeight); if (itemY < bottomY) { MoveIndexToBottom(index); } } /// <summary> /// 添加一條新項到串列中 /// </summary> public virtual void AddOneItem() { _sourceListCount++; int generateCount = ResizeContent(); if (_items.Count < generateCount) { GameObject itemGameObject = _scrollViewOperate.InitOneItem(); LoopScrollItem item = new LoopScrollItem(itemGameObject); _items.Add(item); } RefreshGestureScrollView(); } /// <summary> /// 洗掉一條串列項 /// </summary> public virtual void DeleteOneItem() { _sourceListCount--; int generateCount = ResizeContent(); if (generateCount < _items.Count) { int lastIndex = _items.Count - 1; _scrollViewOperate.DeleteOneItem(_items[lastIndex].RectTransform); _items.RemoveAt(lastIndex); } RefreshGestureScrollView(); foreach (LoopScrollItem item in _items) { _scrollViewOperate.OtherItemAfterDeleteOne(item); } } /// <summary> /// 根據當前手勢項的數量重新調整內容的高度 /// </summary> protected virtual int ResizeContent() { int generateCount = Mathf.Min(_visibleCount, _sourceListCount); float contentHeight = _sourceListCount * _itemHeight; scrollRect.content.sizeDelta = new Vector2(scrollRect.content.sizeDelta.x, contentHeight); return generateCount; } /// <summary> /// 重繪串列內容 /// </summary> protected virtual void RefreshGestureScrollView() { float contentY = scrollRect.content.anchoredPosition.y; int skipCount = (int)(contentY / _itemHeight); for (int i = 0; i < _items.Count; i++) { if (skipCount >= 0 && skipCount < _sourceListCount) { _scrollViewOperate.UpdateItemContent(skipCount, _items[i]); float itemY = -skipCount * _itemHeight; _items[i].RectTransform.anchoredPosition = new Vector2(scrollRect.content.anchoredPosition.x, itemY); skipCount++; } } } } }

以上的腳本就是滾動視圖的所有代碼,使用時掛載需要的腳本,并在其它類中實作介面即可使用,
以下是呼叫回圈串列的示例:
控制單獨項的腳本 TextItem.cs
腳本繼承了MonoBehaviour,需要掛在TextItem預制體上,并將Text和Button拖到參考欄位中,用來于控制單獨串列項的行為,
namespace LoopScrollTest { public class TextItem : MonoBehaviour { public Text text; public Button deleteBtn; public SourceData Data =https://www.cnblogs.com/jingjiangtao/p/> _sourceData; private Action<TextItem> _deleteItemAction; private SourceData _sourceData; private void Awake() { deleteBtn.onClick.AddListener(OnClickDelete); } /// <summary> /// 初始化資料和按鈕點擊的委托 /// </summary> public void Init(SourceData data, Action<TextItem> deleteItemAction) { _sourceData = data; _deleteItemAction = deleteItemAction; text.text = _sourceData.text; } /// <summary> /// 更新參考的源資料 /// </summary> public void UpdateSourceData(SourceData data) { _sourceData = data; text.text = _sourceData.text; } /// <summary> /// 洗掉按鈕的點擊事件 /// </summary> private void OnClickDelete() { _deleteItemAction?.Invoke(this); } } }

資料源物體類
namespace LoopScrollTest { public class SourceData { // 資料內容 public string text; } }
呼叫其它腳本用于測驗的腳本 TestLoopScrollView.cs
用于控制UI和呼叫回圈串列的腳本,需要在場景中新建GameObject物體,并將對應的參考拖到參考槽中以賦值,
namespace LoopScrollTest { public class TestLoopScrollView : MonoBehaviour, ILoopScrollView { [Tooltip("源資料串列的個數,修改后需要重新運行才能生效")] public uint count = 300; // 對滾動串列的參考 public LoopScrollView loopScrollView; // 對添加按鈕的參考 public Button addBtn; // 對ScrollView的Content物體的參考 public RectTransform content; // 對單個串列項預制體的參考 public TextItem textItemPrefab; // 源資料串列 private List<SourceData> _sourceList = new List<SourceData>(); private int nextId = 1; private void Awake() { addBtn.onClick.AddListener(OnClickAddItem); // 獲取單個串列項的高度 float itemHeight = (textItemPrefab.transform as RectTransform).rect.height; // 初始化滾動串列中的參考 loopScrollView.InitScrollView(itemHeight, this); } private void Start() { // 初始化源資料串列,作為串列的資料源 for (int i = 0; i < count; i++) { SourceData data = new SourceData { text = nextId.ToString() }; _sourceList.Add(data); nextId++; } // 初始化滾動串列的顯示 loopScrollView.InitScrollViewList(_sourceList.Count); } /// <summary> /// 在串列中追加新項 /// </summary> private void OnClickAddItem() { SourceData data = new SourceData { text = nextId.ToString(), }; _sourceList.Add(data); nextId++; loopScrollView.AddOneItem(); loopScrollView.MoveIndexToBottom(_sourceList.Count - 1); } /// <summary> /// 根據給定的源資料生成單個串列項 /// </summary> private TextItem InitTextItem(SourceData sourceData) { TextItem item = Instantiate(textItemPrefab, content); item.Init(sourceData, DeleteItemAction); return item; } /// <summary> /// 點擊單個串列項的洗掉按鈕的回呼,洗掉單個串列項 /// </summary> private void DeleteItemAction(TextItem item) { _sourceList.Remove(item.Data); loopScrollView.DeleteOneItem(); } #region 實作的介面方法 public virtual void DeleteOneItem(Transform item) { DestroyImmediate(item.gameObject); } public virtual GameObject InitOneItem() { TextItem item = InitTextItem(_sourceList[_sourceList.Count - 1]); return item.gameObject; } public virtual GameObject InitScrollViewList(int index) { TextItem item = InitTextItem(_sourceList[index]); return item.gameObject; } public virtual void OtherItemAfterDeleteOne(LoopScrollItem item) { } public virtual void UpdateItemContent(int index, LoopScrollItem item) { // 更新串列項參考的源資料 TextItem textItem = item.RectTransform.GetComponent<TextItem>(); textItem.UpdateSourceData(_sourceList[index]); } #endregion } }

效果

完整專案
https://github.com/jingjiangtao/LoopScrollView
參考
https://blog.csdn.net/lxt610/article/details/90036080
如有錯誤,歡迎指正,謝謝!轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/444377.html
標籤:其他
上一篇:資料結構 - 散串列
