在游戏开发过程中,排行榜、背包之类的需求是很常见的,一般使用TableView组件来做成可上下左右滑动的界面。如果数据量不大,直接创建子项对象既可。如果数据量比较大,初始化时要创建N个子项对象,就悲剧了。一方面,由于数量过多导致初始化等待时间过长,降低了用户体验;另一方面,子项对象过多,导致drawcall飙升。所以针对这种数据量较多的需求,需要创建一种可重用TableView组件。
如下图所示,这是一个类似排行榜的界面,绿色模块为一条条排行榜数据,支持上下滑动查看排行榜信息。
在一开始,根据可见区域高度,创建固定数目的子项对象,比如下图就只创建了五个子项对象,无论总共有多少条数据,界面上只有5个子项对象。当我们往上滑动排行榜时,所有子项对象都往上走,当第一条数据离开可见区域时,将这条数据位置调整到第五条数据的后面,以此类推就可以一直重用子项对象了。向下,左、右滑动时都是一样的道理。
创建界面
- 在画布下创建空的对象,命名为VerticalTableView,添加Image,Mask,Scroll Rect等组件,设置好相关属性,如下图所示
- 由于是做只支持上下活动的TableView,所以在ScrollRect属性中需勾选Vertical,Horizontal属性不勾选
- Mask组件是一个遮罩,它可以遮住超出当前面板的区域,因为一般数据都会比我们当前可看到的要多,如果没有加此组件,向上滑动时,就会出现下图的情况
- 创建内容面板,创建空对象既可,命名为Content,作为VerticalTableView的子节点,用来存放子项对象;
创建子项对象,命名为Item,作为Content的子节点,在此对象下面创建一个对象并添加Text组件,设置为不可见,后面会重复创建此对象,可做成prefab;
- 至此界面创建完毕,目录结构如下图所示
编写TableView脚本
创建TableView脚本,并添加到VerticalTableView对象上
创建相关变量
///
/// 子项对象
///
public GameObject m_cell;
///
/// 面板总尺寸(宽或高)
///
private float m_totalViewSize;
///
/// 可见面板尺寸(宽或高)
///
private float m_visibleViewSize;
///
/// 子项尺寸(宽或高)
///
private float m_cellSize;
///
/// 子项间隔
///
private float m_cellInterval;
///
/// 可滑动距离
///
private float m_totalScrollDistance;
///
/// 子项总数量
///
private int m_totalCellCount;
///
/// 内容面板尺寸是否大于可见面板
///
private bool m_isSizeEnough = true;
///
/// 开始下标
///
private int m_startIndex;
///
/// 结束下标
///
private int m_endIndex;
///
/// 已滑动距离
///
private Vector2 m_contentOffset;
///
/// 可见子项集合
///
private Dictionary<int, GameObject> m_cells;
///
/// 可重用子项集合
///
private List m_reUseCellList;
///
/// 当前滑动方向
///
private ViewDirection m_currentDir;
///
/// 上次滑动系数
///
private Vector2 m_lastScrollFactor;
///
/// ScrollRect组件
///
private ScrollRect m_scrollRect;
///
/// RectTransform组件
///
private RectTransform m_rectTransform;
///
/// 内容面板RectTransform组件
///
private RectTransform m_contentRectTransform;
public enum ViewDirection
{
Horizontal,//横向滑动
Vertical//纵向滑动
}
初始化组件
///
/// 初始化组件
///
private void InitComponent()
{
m_scrollRect = this.GetComponent();
m_rectTransform = this.GetComponent();
m_contentRectTransform = m_scrollRect.content;
}
初始化变量
///
/// 初始化变量
///
private void InitFields()
{
m_cells = new Dictionary<int, GameObject>();
m_reUseCellList = new List();
m_lastScrollFactor = Vector2.one;
m_contentOffset = Vector2.zero;
m_totalCellCount = 10;//总子项数量
m_cellInterval = 10f;//子项间隔
if (m_scrollRect.horizontal == true)//根据ScrollRect组件属性设置滑动方向
{
m_currentDir = ViewDirection.Horizontal;
}
if (m_scrollRect.vertical == true)//根据ScrollRect组件属性设置滑动方向
{
m_currentDir = ViewDirection.Vertical;
}
if (m_currentDir == ViewDirection.Vertical)//获取可见面板高度,子项对象高度
{
m_visibleViewSize = m_rectTransform.sizeDelta.y;
m_cellSize = m_cell.GetComponent().sizeDelta.y;
}
else//获取可见面板宽度,子项对象宽度
{
m_visibleViewSize = m_rectTransform.sizeDelta.x;
m_cellSize = m_cell.GetComponent().sizeDelta.x;
}
m_totalViewSize = (m_cellSize + m_cellInterval) * m_totalCellCount;
m_totalScrollDistance = m_totalViewSize - m_visibleViewSize;
}
初始化面板
///
/// 初始化面板
///
private void InitView()
{
Vector2 contentSize = m_contentRectTransform.sizeDelta;
if (m_currentDir == ViewDirection.Vertical)//设置内容面板锚点,对齐方式,纵向滑动为向上对齐
{
contentSize.y = m_totalViewSize;
m_contentRectTransform.anchorMin = new Vector2(0.5f, 1f);
m_contentRectTransform.anchorMax = new Vector2(0.5f, 1f);
m_contentRectTransform.pivot = new Vector2(0.5f, 1f);
}
else//设置内容面板锚点,对齐方式,横向滑动为向左对齐
{
contentSize.x = m_totalViewSize;
m_contentRectTransform.anchorMin = new Vector2(0f, 0.5f);
m_contentRectTransform.anchorMax = new Vector2(0f, 0.5f);
m_contentRectTransform.pivot = new Vector2(0f, 0.5f);
}
//设置内容面板尺寸
m_contentRectTransform.sizeDelta = contentSize;
//设置内容面板坐标
m_contentRectTransform.anchoredPosition = Vector2.zero;
int count = 0;
float usefulSize = 0f;
if (m_visibleViewSize > m_totalViewSize)//可见面板大于所有子项所占尺寸时,不重用子项对象
{
usefulSize = m_totalViewSize;
count = (int)(usefulSize / (m_cellSize + m_cellInterval));
m_isSizeEnough = false;
}
else
{
usefulSize = m_visibleViewSize;
count = (int)(usefulSize / (m_cellSize + m_cellInterval)) + 1;
float tempSize = m_visibleViewSize + (m_cellSize + m_cellInterval);
float allCellSize = (m_cellSize + m_cellInterval) * count;
if (allCellSize < tempSize)
{
count++;
}
}
for (int i =0; i< count; i++)
{
OnCellCreateAtIndex(i);
}
}
重写ScrollRect组件OnValueChanged方法
如图所示,将此方法拖拉到ScrollRect组件上
///
/// 重写ScrollRect OnValueChanged方法,此方法在每次滑动时都会被调用
///
///
public void OnScrollValueChanged(Vector2 offset)
{
OnCellScrolling(offset);
}
书写滑动逻辑
//滑动区域计算
private void OnCellScrolling(Vector2 offset)
{
if (m_isSizeEnough == false)
{
return;
}
//offset的x和y都为0~1的浮点数,分别代表横向滑出可见区域的宽度百分比和纵向划出可见区域的高度百分比
m_contentOffset.x = m_totalScrollDistance * offset.x;//滑出可见区域宽度
m_contentOffset.y = m_totalScrollDistance * (1 - offset.y);//滑出可见区域高度
CalCellIndex();
}
//计算可见区域子项对象开始跟结束下标
private void CalCellIndex()
{
float startOffset = 0f;
float endOffset = 0f;
if (m_currentDir == ViewDirection.Vertical)//纵向滑动
{
startOffset = m_contentOffset.y;//当前可见区域起始y坐标
endOffset = m_contentOffset.y + m_visibleViewSize;//当前可见区域结束y坐标
}
else
{
startOffset = m_contentOffset.x;//当前可见区域起始x坐标
endOffset = m_contentOffset.x + m_visibleViewSize;//当前可见区域结束y坐标
}
endOffset = endOffset > m_totalViewSize ? m_totalViewSize : endOffset;
m_startIndex = (int)(startOffset / (m_cellSize + m_cellInterval));//子项对象开始下标
m_startIndex = m_startIndex < 0 ? 0 : m_startIndex;
m_endIndex = (int)(endOffset / (m_cellSize + m_cellInterval));//子项对象结束下标
m_endIndex = m_endIndex > (m_totalCellCount - 1) ? (m_totalCellCount - 1) : m_endIndex;
UpdateCells();
}
//管理子项对象集合
private void UpdateCells()
{
List<int> delList = new List<int>();
foreach (KeyValuePair<int, GameObject> pair in m_cells)
{
if (pair.Key < m_startIndex || pair.Key > m_endIndex)//回收超出可见范围的子项对象
{
delList.Add(pair.Key);
m_reUseCellList.Add(pair.Value);
}
}
//移除超出可见范围的子项对象
foreach (int index in delList)
{
m_cells.Remove(index);
}
//根据开始跟结束下标,重新生成子项对象
for (int i = m_startIndex; i <= m_endIndex; i++)
{
if (m_cells.ContainsKey(i))
{
continue;
}
OnCellCreateAtIndex(i);
}
}
//创建子项对象
public void OnCellCreateAtIndex(int index)
{
GameObject cell = null;
if (m_reUseCellList.Count > 0)//有可重用子项对象时,复用之
{
cell = m_reUseCellList[0];
m_reUseCellList.RemoveAt(0);
}
else//没有可重用子项对象则创建
{
cell = GameObject.Instantiate(m_cell) as GameObject;
}
cell.transform.SetParent(m_contentRectTransform);
cell.transform.localScale = Vector3.one;
RectTransform cellRectTrans = cell.GetComponent();
if (m_currentDir == ViewDirection.Vertical)
{
cellRectTrans.anchorMin = new Vector2(0.5f, 1f);
cellRectTrans.anchorMax = new Vector2(0.5f, 1f);
cellRectTrans.pivot = new Vector2(0.5f, 1f);
}
else
{
cellRectTrans.anchorMin = new Vector2(0f, 0.5f);
cellRectTrans.anchorMax = new Vector2(0f, 0.5f);
cellRectTrans.pivot = new Vector2(0f, 0.5f);
}
//设置子项对象位置
if (m_currentDir == ViewDirection.Vertical)
{
float posY = index * m_cellSize + (index + 1) * m_cellInterval;
if (posY > 0)
{
posY = -posY;
}
cellRectTrans.anchoredPosition3D = new Vector3(0, posY, 0);
}
else
{
float posX = index * m_cellSize + (index + 1) * m_cellInterval;
cellRectTrans.anchoredPosition3D = new Vector3(posX, 0, 0);
}
cell.transform.Find("Text").GetComponent().text = "这是第 " + (index + 1) + " 数据";
cell.SetActive(true);
cell.transform.SetAsLastSibling();
m_cells.Add(index, cell);
}
最后,如下图所示,无论数据量有多大,存在的子项对象都只有这么多,只是在滑动过程中不停地变换位置而已。