Unity 高效 ListView GridView

基本思想

在以 List / Grid 显示UI元素时,如果有上百,甚至上千个元素,如果全部创建出来,对内存和渲染还是有一定的压力的,因此我们希望仅创建那些在 view content 中可见的元素。

对于 ListView 基本思想是:

  • 根据元素数量,滑动方向,确定 content 尺寸
  • 根据每个元素的大小,计算元素在 content 上的坐标
  • 当 content 滑动时,计算在视口中的元素
    • 对于已经显示的元素
      • 如果当前不再显示,隐藏,并回收UI元素,放入池中
      • 如果当前依然显示,不处理
    • 对于当前不在显示状态的元素,创建并显示
      • 如果UI元素池中有,从池中申请元素
      • 如果UI元素池中没有,创建新的UI元素

对于 GridView 基本思想与 ListView 是一样的,只不过将一个元素,换成一行/列元素。

涉及到三个类:

  • UIContainerView:UI元素容器视窗,该类主要是定义了通用的 UI容器的元素类型,及显示时调用的方法
  • UIListView:主要是实现了添加元素,更新视窗,清理的接口
  • UIGridView:同 UIListView

实现

下面是三个类的代码,拷贝下来可以直接使用

cs 复制代码
using UnityEngine;

public class UIContainerView : MonoBehaviour
{
    public class ItemView : MonoBehaviour
    {
        public virtual void OnCreate(object data, bool first)
        {
            throw new System.NotImplementedException();
        }
    }
}

UIListView 实现代码

cs 复制代码
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UIListView : UIContainerView
{
    public enum ESlideDirection
    {
        Vertical,
        Horizontal
    }

    public ESlideDirection slideDirection = ESlideDirection.Vertical;
    public float margin = 0f;
    public float spacing = 0f;
    public GameObject itemPrefab;

    RectTransform _rectContent;
    bool _isVertical = true;
   
    List<object> _datalist = new List<object>();  
    List<Vector2> _dataPos = new List<Vector2>();
    Queue<GameObject> _itemPool = new Queue<GameObject>();

    struct VisibleItem
    {
        public int dataIndex;
        public GameObject item;
    }
    List<VisibleItem> _visibleItemList = new List<VisibleItem>();

    float _viewHeight;
    float _itemHeight;

    int _firstIndex;
    int _lastIndex;
    float _lastContentY = 0;

    bool _ready = false;
    bool _waitRefresh = false;

    void Start()
    {
        ScrollRect scrollRect = GetComponentInParent<ScrollRect>();
        _rectContent = scrollRect.content;
        _isVertical = slideDirection == ESlideDirection.Vertical;
        RectTransform rectItem = itemPrefab.GetComponent<RectTransform>();
        if (_isVertical)
        {
            scrollRect.horizontal = false;
            scrollRect.vertical = true;
            _rectContent.anchorMin = new Vector2(0f, 1f);
            _rectContent.anchorMax = new Vector2(1f, 1f);
            itemPrefab.GetComponent<RectTransform>().pivot = new Vector2(0.5f, 1f);
        }
        else
        {
            scrollRect.horizontal = true;
            scrollRect.vertical = false;
            _rectContent.anchorMin = new Vector2(0f, 0f);
            _rectContent.anchorMax = new Vector2(0f, 1f);
            itemPrefab.GetComponent<RectTransform>().pivot = new Vector2(0f, 0.5f);
        }

        rectItem.anchorMin = new Vector2(0f, 1f);
        rectItem.anchorMax = new Vector2(0f, 1f);

        itemPrefab.SetActive(false);

        _ready = true;
    }


    void Update()
    {
        if(_waitRefresh)
        {
            _waitRefresh = false;
            RefreshListView();
        }

        if (_datalist.Count == 0)
            return;

        float contentY = _isVertical ? _rectContent.anchoredPosition.y : Mathf.Abs(_rectContent.anchoredPosition.x);
        if (Mathf.Abs(contentY - _lastContentY) < 0.1f)
            return;
        _lastContentY = contentY;


        int firstIndex = Mathf.FloorToInt(contentY / _itemHeight);
        int lastIndex = Mathf.CeilToInt((contentY + _viewHeight) / _itemHeight);
        firstIndex = Mathf.Max(0, firstIndex);
        lastIndex = Mathf.Min(_datalist.Count - 1, lastIndex);

        if(_firstIndex == firstIndex && _lastIndex == lastIndex)
        {
            return;
        }
        _firstIndex = firstIndex;
        _lastIndex = lastIndex;

        //Debug.Log($"firstIndex:{firstIndex}, lastIndex:{lastIndex}");

        if(_visibleItemList.Count == 0)
        {
            for (int i = firstIndex; i <= lastIndex; i++)
            {
                ShowItem(i);
            }
        }
        else
        {
            HashSet<int> newVisibleDataIndex = new HashSet<int>();
            for (int i = firstIndex; i <= lastIndex; i++)
                newVisibleDataIndex.Add(i);

            HashSet<int> currentVisibleDataIndex = new HashSet<int>();
            for (int i = 0; i < _visibleItemList.Count; i++)
            {
                int index = _visibleItemList[i].dataIndex;
                if (!newVisibleDataIndex.Contains(index))
                {
                    // 回收item
                    _itemPool.Enqueue(_visibleItemList[i].item);
                    _visibleItemList[i].item.SetActive(false);
                    _visibleItemList.RemoveAt(i);
                    i--;
                }
                else
                {
                    currentVisibleDataIndex.Add(index);
                }
            }

            for (int i = firstIndex; i <= lastIndex; i++)
            {
                if (!currentVisibleDataIndex.Contains(i))
                {
                    ShowItem(i);
                }
            }
        }
    }

    HashSet<int> _visibleRecords = new HashSet<int>();

    void ShowItem(int i)
    {
        GameObject item;
        if (_itemPool.Count > 0)
        {
            item = _itemPool.Dequeue();
            item.SetActive(true);
        }
        else
        {
            item = Instantiate(itemPrefab);
            item.SetActive(true);
            item.transform.SetParent(_rectContent);
            item.transform.localScale = Vector3.one;
        }
        item.GetComponent<RectTransform>().anchoredPosition = _dataPos[i];
        item.gameObject.name = "Item_" + i;
        item.GetComponent<ItemView>()?.OnCreate(_datalist[i], !_visibleRecords.Contains(i));
        _visibleRecords.Add(i);
        _visibleItemList.Add(new VisibleItem { dataIndex = i, item = item });
    }

    public void SetDatas(List<object> datalist)
    {
        _datalist = datalist;
    }

    public void AddItem(object data)
    {
        _datalist.Add(data);
    }

    public void ClearItems()
    {
        _datalist.Clear();
        foreach(var visibleItem in _visibleItemList)
        {
            Destroy(visibleItem.item);
        }
        _visibleItemList.Clear();
        while(_itemPool.Count > 0)
        {
            var item = _itemPool.Dequeue();
            Destroy(item);
        }
        _visibleRecords.Clear();
    }

    public void RefreshListView()
    {
        _visibleRecords.Clear();

        if (!_ready)
        {
            _waitRefresh = true;
            return;
        }

        _lastContentY = 99999999f;

        RectTransform rectView = (RectTransform)_rectContent.parent;

        if(_isVertical)
        {
            _viewHeight = rectView.rect.height;
            _itemHeight = itemPrefab.GetComponent<RectTransform>().rect.height + spacing;
        }
        else
        {
            _viewHeight = rectView.rect.width;
            _itemHeight = itemPrefab.GetComponent<RectTransform>().rect.width + spacing;
        }

        // 计算每个Item的位置
        Vector2 offset = _isVertical ?
                            new Vector2(rectView.rect.width * 0.5f, -margin) :
                            new Vector2(margin, -rectView.rect.height * 0.5f);
        _dataPos.Clear();
        for (int i = 0; i < _datalist.Count; i++)
        {
            _dataPos.Add(offset);
            if(_isVertical)
                offset.y -= _itemHeight;
            else
                offset.x += _itemHeight;
        }

        // 设置Content的高度
        if(_isVertical)
            _rectContent.sizeDelta = new Vector2(_rectContent.sizeDelta.x, _itemHeight * _datalist.Count + margin*2f);
        else
            _rectContent.sizeDelta = new Vector2(_itemHeight * _datalist.Count + margin*2f, _rectContent.sizeDelta.y);

        _firstIndex = -1;
        _lastIndex = -1;
    }
}

UIGridView 实现代码

cs 复制代码
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UIGridView : UIContainerView
{
    public enum ESlideDirection
    {
        Vertical,
        Horizontal
    }

    public ESlideDirection slideDirection = ESlideDirection.Vertical;
    public float margin = 0f;
    public float spacing = 0f;
    public GameObject itemPrefab;

    RectTransform _rectContent;
    bool _isVertical = true;

    List<object> _datalist = new List<object>();
    Queue<GameObject> _itemPool = new Queue<GameObject>();

    class Row
    {
        public int rowIndex = 0;
        public int fistItemIndex = 0;
        public List<object> items = new List<object>();
        public List<Vector2> itemPositions = new List<Vector2>();

        public List<GameObject> itemViews = new List<GameObject>();
    }
    List<Row> _rows = new List<Row>();
    List<Row> _visibleRows = new List<Row>();

    float _viewHeight;
    float _itemHeight;

    int _firstIndex;
    int _lastIndex;
    float _lastContentY = 0;

    bool _ready = false;
    bool _waitRefresh = false;

    void Start()
    {
        ScrollRect scrollRect = GetComponentInParent<ScrollRect>();
        _rectContent = scrollRect.content;
        _isVertical = slideDirection == ESlideDirection.Vertical;
        RectTransform rectItem = itemPrefab.GetComponent<RectTransform>();
        if (_isVertical)
        {
            scrollRect.horizontal = false;
            scrollRect.vertical = true;
            _rectContent.anchorMin = new Vector2(0f, 1f);
            _rectContent.anchorMax = new Vector2(1f, 1f);
        }
        else
        {
            scrollRect.horizontal = true;
            scrollRect.vertical = false;
            _rectContent.anchorMin = new Vector2(0f, 0f);
            _rectContent.anchorMax = new Vector2(0f, 1f);
        }

        rectItem.pivot = new Vector2(0f, 1f);
        rectItem.anchorMin = new Vector2(0f, 1f);
        rectItem.anchorMax = new Vector2(0f, 1f);

        itemPrefab.SetActive(false);

        _ready = true;
    }


    void Update()
    {
        if (_waitRefresh)
        {
            _waitRefresh = false;
            RefreshListView();
        }

        if (_datalist.Count == 0)
            return;

        float contentY = _isVertical ? _rectContent.anchoredPosition.y : Mathf.Abs(_rectContent.anchoredPosition.x);
        if (Mathf.Abs(contentY - _lastContentY) < 0.1f)
            return;
        _lastContentY = contentY;


        int firstIndex = Mathf.FloorToInt(contentY / _itemHeight);
        int lastIndex = Mathf.FloorToInt((contentY + _viewHeight) / _itemHeight);
        firstIndex = Mathf.Max(0, firstIndex);
        lastIndex = Mathf.Min(_rows.Count - 1, lastIndex);

        if (_firstIndex == firstIndex && _lastIndex == lastIndex)
        {
            return;
        }
        _firstIndex = firstIndex;
        _lastIndex = lastIndex;

        //Debug.Log($"firstIndex:{firstIndex}, lastIndex:{lastIndex}");

        if (_visibleRows.Count == 0)
        {
            for (int i = firstIndex; i <= lastIndex; i++)
            {
                ShowRow(i);
            }
        }
        else
        {
            HashSet<int> newVisibleDataIndex = new HashSet<int>();
            for (int i = firstIndex; i <= lastIndex; i++)
                newVisibleDataIndex.Add(i);

            HashSet<int> currentVisibleDataIndex = new HashSet<int>();
            for (int i = 0; i < _visibleRows.Count; i++)
            {
                int index = _visibleRows[i].rowIndex;
                if (!newVisibleDataIndex.Contains(index))
                {
                    // 回收item
                    for (int c = 0; c < _visibleRows[i].itemViews.Count; c++)
                    {
                        _itemPool.Enqueue(_visibleRows[i].itemViews[c]);
                        _visibleRows[i].itemViews[c].SetActive(false);
                        _visibleRows[i].itemViews[c] = null;
                    }
                    _visibleRows.RemoveAt(i);
                    i--;
                }
                else
                {
                    currentVisibleDataIndex.Add(index);
                }
            }

            for (int i = firstIndex; i <= lastIndex; i++)
            {
                if (!currentVisibleDataIndex.Contains(i))
                {
                    ShowRow(i);
                }
            }
        }
    }

    HashSet<int> _visibleRecords = new HashSet<int>();

    void ShowRow(int i)
    {
        Row curRow = _rows[i];
        bool first = !_visibleRecords.Contains(i);
        for (int c = 0; c < curRow.items.Count; c++)
        {
            GameObject item;
            if (_itemPool.Count > 0)
            {
                item = _itemPool.Dequeue();
                item.SetActive(true);
            }
            else
            {
                item = Instantiate(itemPrefab);
                item.SetActive(true);
                item.transform.SetParent(_rectContent);
                item.transform.localScale = Vector3.one;
            }
            item.GetComponent<RectTransform>().anchoredPosition = curRow.itemPositions[c];
            item.gameObject.name = "Item_" + (curRow.fistItemIndex + c);
            item.GetComponent<ItemView>()?.OnCreate(curRow.items[c], first);
            curRow.itemViews[c] = item;
        }
        _visibleRecords.Add(i);
        _visibleRows.Add(curRow);
    }

    public void SetDatas(List<object> datalist)
    {
        _datalist = datalist;
    }

    public void AddItem(object data)
    {
        _datalist.Add(data);
    }

    public void ClearItems()
    {
        _datalist.Clear();
        for(int i = 0; i < _visibleRows.Count; i++)
        {
            for (int c = 0; c < _visibleRows[i].itemViews.Count; c++)
            {
                Destroy(_visibleRows[i].itemViews[c]);
            }
        }
        while (_itemPool.Count > 0)
        {
            var item = _itemPool.Dequeue();
            Destroy(item);
        }
        _visibleRecords.Clear();
    }

    public void RefreshListView()
    {
        _visibleRecords.Clear();

        if (!_ready)
        {
            _waitRefresh = true;
            return;
        }

        _lastContentY = 99999999f;
        int countPerRow = 0;

        RectTransform rectView = (RectTransform)_rectContent.parent;
        RectTransform rectItem = itemPrefab.GetComponent<RectTransform>();
        float rowWidth = 0f;
        float itemWidth = 0f;
        if (_isVertical)
        {
            _viewHeight = rectView.rect.height;
            _itemHeight = rectItem.rect.height + spacing;
            rowWidth = rectView.rect.width - margin * 2f;
            itemWidth = rectItem.rect.width;
        }
        else
        {
            _viewHeight = rectView.rect.width;
            _itemHeight = rectItem.rect.width + spacing;
            rowWidth = rectView.rect.height - margin * 2f;
            itemWidth = rectItem.rect.height;
        }

        // 计算每行能放多少个Item
        float curWidth = 0f;
        while (true)
        {
            float inc = itemWidth + (countPerRow > 0 ? spacing : 0f);
            curWidth += inc;
            if (curWidth > rowWidth)
            {
                curWidth -= inc;
                break;
            }
            countPerRow++;
        }
        countPerRow = countPerRow > 0 ? countPerRow : 1;

        // 计算每个Item的位置
        Vector2 offset;
        if (_isVertical)
        {
            offset = new Vector2(rectView.rect.width * 0.5f, -margin);
            offset.x -= curWidth * 0.5f;
        }
        else
        {
            offset = new Vector2(margin, -rectView.rect.height * 0.5f);
            offset.y += curWidth * 0.5f;
        }
            
        int countRow = _datalist.Count / countPerRow;
        int countLastRow = _datalist.Count % countPerRow;
        countRow += countLastRow > 0 ? 1 : 0;
        //Debug.Log($"countPerRow:{countPerRow}, countRow:{countRow}, countLastRow:{countLastRow}");

        int indexData = 0;
        _rows.Clear();
        for(int r=0; r< countRow; r++)
        {
            Row row = new Row();
            row.rowIndex = r;
            row.fistItemIndex = indexData;
            int countItems = r == countRow -1 && countLastRow >0 ? countLastRow : countPerRow;
            for (int c=0; c< countItems; c++)
            {
                int dataIndex = indexData++;
                row.items.Add(_datalist[dataIndex]);

                Vector2 itemPos = offset;
                if (_isVertical)
                    itemPos.x += (rectItem.rect.width + spacing) * c;
                else 
                    itemPos.y -= (rectItem.rect.height + spacing) * c;
                row.itemPositions.Add(itemPos);
                row.itemViews.Add(null);
            }
            _rows.Add(row);

            // 累加行偏移
            // 每行从头开始计算位置
            if (_isVertical)
            {
                offset.y -= _itemHeight;
            }
            else
            {
                offset.x += _itemHeight;
            }
        }


        // 设置Content的高度
        if (_isVertical)
            _rectContent.sizeDelta = new Vector2(_rectContent.sizeDelta.x, _itemHeight * _rows.Count + margin * 2f);
        else
            _rectContent.sizeDelta = new Vector2(_itemHeight * _rows.Count + margin * 2f, _rectContent.sizeDelta.y);

        _firstIndex = -1;
        _lastIndex = -1;
    }
}

UIListView 的测试/使用示例代码

cs 复制代码
using UnityEngine;
using UnityEngine.UI;

public class UIListViewItemExample : UIContainerView.ItemView
{
    public Text text;

    public override void OnCreate(object data, bool first)
    {
        text.text = data.ToString();
        if (first)
        {
            text.color = Color.green;
        }
        else
        {
            text.color = Color.blue;
        }
    }
}
cs 复制代码
using UnityEngine;

public class UIListViewExample : MonoBehaviour
{
    public UIListView listViewVertical;
    public UIListView listViewHorizontal;

    // Start is called before the first frame update
    void Start()
    {
        for (int i = 0; i < 30; i++)
        {
            listViewVertical.AddItem("Item " + i);
            listViewHorizontal.AddItem(i);

        }
        listViewVertical.RefreshListView();
        listViewHorizontal.RefreshListView();
    }
}

UIGridView 测试/使用示例代码

cs 复制代码
using UnityEngine;
using UnityEngine.UI;

public class UIGridViewItemExample : UIContainerView.ItemView
{
    public Text text;

    public override void OnCreate(object data, bool first)
    {
        text.text = data.ToString();
        if (first)
        {
            text.color = Color.green;
        }
        else
        {
            text.color = Color.blue;
        }
    }
}
cs 复制代码
using UnityEngine;

public class UIGridViewExample : MonoBehaviour
{
    public UIGridView viewVertical;
    public UIGridView viewHorizontal;

    void Start()
    {
        for (int i = 0; i < 100; i++)
        {
            if(viewVertical != null)
                viewVertical.AddItem("Item " + i);
            if(viewHorizontal != null)
                viewHorizontal.AddItem(i);

        }

        if (viewVertical != null)
            viewVertical.RefreshListView();

        if (viewHorizontal != null)
            viewHorizontal.RefreshListView();
    }
}
相关推荐
avi911120 小时前
发现一个宝藏Unity开源AVG框架,视觉小说的脚手架
unity·开源·框架·插件·tolua·avg
沉默金鱼2 天前
Unity实用技能-格式化format文字
ui·unity·游戏引擎
jyy_992 天前
通过网页地址打开unity的exe程序,并传参
unity
qq_205279052 天前
Unity TileMap 使用经验
unity·游戏引擎
心灵宝贝2 天前
Mac Unity 2018.dmg游戏工具 安装步骤 简单易懂教程(附安装包)
macos·unity·游戏引擎
TO_ZRG3 天前
Unity SDK 通过 Registry 分发及第三方依赖处理指南
unity·游戏引擎
龙智DevSecOps解决方案3 天前
Perforce《2025游戏技术现状报告》Part 1:游戏引擎技术的广泛影响以及生成式AI的成熟之路
人工智能·unity·游戏引擎·游戏开发·perforce
WarPigs4 天前
Unity编辑器开发笔记
unity·编辑器·excel
霜绛4 天前
Unity:lua热更新(三)——Lua语法(续)
unity·游戏引擎·lua
世洋Blog4 天前
更好的利用ChatGPT进行项目的开发
人工智能·unity·chatgpt