Unity ScrollView 顯示大量數據


原文鏈接: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 => _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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM