基本思想
在以 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();
}
}