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();
    }
}
相关推荐
天人合一peng34 分钟前
unity 生成标记根据背景色变色为明显的颜色
unity·游戏引擎
魔士于安1 小时前
Unity 超市总动员 超市收银台 超市货架 超市购物手推车 超市常见商品
游戏·unity·游戏引擎·贴图·模型
CandyU21 小时前
Unity —— 数据持久化
unity·游戏引擎
zh路西法1 小时前
【Unity实现Oneshot胶卷显形】游戏窗口化与Win32API的使用
游戏·unity·游戏引擎
凡情6 小时前
android隐私合规检测
android·unity
小贺儿开发6 小时前
Unity3D 本地 Stable Diffusion 文生图效果演示
人工智能·unity·stable diffusion·文生图·ai绘画·本地化
mxwin1 天前
Unity Shader 半透明物体为什么不能写入深度缓冲?
unity·游戏引擎·shader
晚枫歌F1 天前
三层时间轮的实现
网络·unity·游戏引擎
咸鱼永不翻身1 天前
Lua脚本事件检查工具
unity·lua·工具
leo__5201 天前
单载波中继系统资源分配算法MATLAB仿真程序
算法·matlab·unity