文章目录
-
-
- 一、前言
- 二、原理
- 三、最终实现效果
- 四、具体使用
-
- 1、循环列表脚本:RecyclingListView
- 2、列表item脚本:RecyclingListViewItem
- 3、测试脚本
- 五、附录
-
- 1、RecyclingListView.cs
- 2、RecyclingListViewItem.cs
-
一、前言
点关注不迷路,持续输出
Unity
干货文章。嗨,大家好,我是新发。
游戏开发中,经常需要用到列表显示,比如排行榜列表、邮件列表、好友列表等等,而当列表数据很多时,我们就要考虑使用循环复用列表了,比如循环复用使用10个
item
来显示100
个数据。二、原理
原理其实就是,列表向上滑动时,当
item
超过显示区域的上边界时,把item
移动到列表底部,重复使用item
并更新item
的ui
显示,向下滑动同理,把超过显示区域底部的item
复用到顶部。为了方便大家理解,我成图,如下:
文章图片
三、最终实现效果
本文
Unity Demo
工程我已上传到CodeChina
,感兴趣的同学可自行下载学习。地址:https://codechina.csdn.net/linxinfa/UnityRecyclingListDemo
注,我使用的
Unity
版本:2020.2.7f1c1 (64-bit)
。文章图片
运行效果如下:
创建列表:
文章图片
使用
9
个item
循环复用,可以显示大量的列表项。文章图片
清空列表:
文章图片
添加行:
文章图片
删除指定行:
文章图片
移动指定行到列表的顶部、中间、底部:
文章图片
四、具体使用
1、循环列表脚本:RecyclingListView
RecyclingListView
:循环列表类,具体代码参见文章末尾。文章图片
主要接口和属性:
///
/// 列表行数,赋值时,会执行列表重新计算
///
public int RowCount///
/// 供外部调用,强制刷新整个列表,比如数据变化了,刷新一下列表
///
public virtual void Refresh()///
/// 供外部调用,强制刷新整个列表的局部item
///
/// 开始行
/// 数量
public virtual void Refresh(int rowStart, int count)///
/// 供外部调用,强制刷新整个列表的某一个item
///
public virtual void Refresh(RecyclingListViewItem item)///
/// 清空列表
///
public virtual void Clear()///
/// 供外部调用,强制滚动列表,使某一行显示在列表中
///
/// 行号
/// 目标行显示在列表的位置:顶部,中心,底部
public virtual void ScrollToRow(int row, ScrollPosType posType)
RecyclingListView
脚本需要挂在ScrollRect
所在的节点上。文章图片
有三个参数:
Child Obj
:列表项item
,Item
必须挂RecyclingListViewItem
或它的子类脚本。Row Padding
:列表item
之间的间隔。Pre Alloc Height
:预先分配列表高度,默认为0。2、列表item脚本:RecyclingListViewItem 列表
item
基类,主要接口和属性:///
/// 循环列表
///
public RecyclingListView ParentList
///
/// 行号
///
public int CurrentRowpublic RectTransform RectTransform///
/// item更新事件响应函数
///
public virtual void NotifyCurrentAssignment(RecyclingListView v, int row)
具体列表
item
需要继承RecyclingListViewItem
,在子类中实现具体更新item
逻辑,例:using UnityEngine.UI;
public class TestChildItem : RecyclingListViewItem
{
public Text titleText;
public Text rowText;
private TestChildData childData;
public TestChildData ChildData
{
get { return childData;
}
set
{
childData = https://www.it610.com/article/value;
titleText.text = childData.Title;
rowText.text = $"行号:{childData.Row}";
}
}
}public struct TestChildData
{
public string Title;
public int Row;
public TestChildData(string title, int row)
{
Title = title;
Row = row;
}
}
列表
item
挂上TestChildItem
脚本。文章图片
3、测试脚本 做个简单的测试界面:
文章图片
写个测试脚本
TestPanel
,测试一下循环列表,挂在TestPanel
节点上,赋值对应的ui
对象。文章图片
TestPanel
代码如下:using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;
using UnityEngine.UI;
public class TestPanel : MonoBehaviour
{
public RecyclingListView scrollList;
///
/// 列表数据
///
private List data = https://www.it610.com/article/new List();
public InputField createRowCntInput;
public Button createListBtn;
public Button clearListBtn;
public InputField deleteItemInput;
public Button deleteItemBtn;
public Button addItemBtn;
public InputField moveToRowInput;
public Button moveToTopBtn;
public Button moveToCenterBtn;
public Button moveToBottomBtn;
private void Start()
{
// 列表item更新回调
scrollList.ItemCallback = PopulateItem;
// 创建列表
createListBtn.onClick.AddListener(CreateList);
// 清空列表
clearListBtn.onClick.AddListener(ClearList);
// 删除某一行
deleteItemBtn.onClick.AddListener(DeleteItem);
// 添加行
addItemBtn.onClick.AddListener(AddItem);
// 将目标行移动到列表的顶部、中心、底部
moveToTopBtn.onClick.AddListener(() =>
{
MoveToRow(RecyclingListView.ScrollPosType.Top);
});
moveToCenterBtn.onClick.AddListener(() =>
{
MoveToRow(RecyclingListView.ScrollPosType.Center);
});
moveToBottomBtn.onClick.AddListener(() =>
{
MoveToRow(RecyclingListView.ScrollPosType.Bottom);
});
}///
/// item更新回调
///
/// 复用的item对象
/// 行号
private void PopulateItem(RecyclingListViewItem item, int rowIndex)
{
var child = item as TestChildItem;
child.ChildData = https://www.it610.com/article/data[rowIndex];
}private void CreateList()
{
if (string.IsNullOrEmpty(createRowCntInput.text))
{
Debug.LogError("请输入行数");
return;
}
var rowCnt = int.Parse(createRowCntInput.text);
data.Clear();
// 模拟数据
string[] randomTitles = new[] {
"黄沙百战穿金甲,不破楼兰终不还",
"且将新火试新茶,诗酒趁年华",
"苟利国家生死以,岂因祸福避趋之",
"枫叶经霜艳,梅花透雪香",
"夏虫不可语于冰",
"落花无言,人淡如菊",
"宠辱不惊,闲看庭前花开花落;去留无意,漫随天外云卷云舒",
"衣带渐宽终不悔,为伊消得人憔悴",
"从善如登,从恶如崩",
"欲穷千里目,更上一层楼",
"草木本无意,荣枯自有时",
"纸上得来终觉浅,绝知此事要躬行",
"不是一番梅彻骨,怎得梅花扑鼻香",
"青青子衿,悠悠我心",
"瓜田不纳履,李下不正冠"
};
for (int i = 0;
i < rowCnt;
++i)
{
data.Add(new TestChildData(randomTitles[Random.Range(0, randomTitles.Length)], i));
}// 设置数据,此时列表会执行更新
scrollList.RowCount = data.Count;
}private void ClearList()
{
data.Clear();
scrollList.Clear();
}private void DeleteItem()
{
if (string.IsNullOrEmpty(deleteItemInput.text))
{
Debug.LogError("请输入行号");
return;
}
var rowIndex = int.Parse(deleteItemInput.text);
data.RemoveAll(item => (item.Row == rowIndex));
scrollList.RowCount = data.Count;
}private void AddItem()
{
data.Add(new TestChildData("我是新增的行", data.Count));
scrollList.RowCount = data.Count;
}private void MoveToRow(RecyclingListView.ScrollPosType posType)
{
if (string.IsNullOrEmpty(moveToRowInput.text))
{
Debug.LogError("请输入行号");
return;
}
var rowIndex = int.Parse(moveToRowInput.text);
scrollList.ScrollToRow(rowIndex, posType);
}
}
测试效果见文章最上面。
完毕。
喜欢
Unity
的同学,不要忘记点击关注,如果有什么Unity
相关的技术难题,也欢迎留言或私信~五、附录
脚本注释我写的比较清楚了,大家应该可以看懂。如果不懂怎么用的,可以下载我的
Demo
工程进行学习。1、RecyclingListView.cs
using System;
using UnityEngine;
using UnityEngine.UI;
///
/// 循环复用列表
///
[RequireComponent(typeof(ScrollRect))]
public class RecyclingListView : MonoBehaviour
{
[Tooltip("子节点物体")]
public RecyclingListViewItem ChildObj;
[Tooltip("行间隔")]
public float RowPadding = 15f;
[Tooltip("事先预留的最小列表高度")]
public float PreAllocHeight = 0;
public enum ScrollPosType
{
Top,
Center,
Bottom,
}public float VerticalNormalizedPosition
{
get => scrollRect.verticalNormalizedPosition;
set => scrollRect.verticalNormalizedPosition = value;
}///
/// 列表行数
///
protected int rowCount;
///
/// 列表行数,赋值时,会执行列表重新计算
///
public int RowCount
{
get => rowCount;
set
{
if (rowCount != value)
{
rowCount = value;
// 先禁用滚动变化
ignoreScrollChange = true;
// 更新高度
UpdateContentHeight();
// 重新启用滚动变化
ignoreScrollChange = false;
// 重新计算item
ReorganiseContent(true);
}
}
}///
/// item更新回调函数委托
///
/// 子节点对象
/// 行数
public delegate void ItemDelegate(RecyclingListViewItem item, int rowIndex);
///
/// item更新回调函数委托
///
public ItemDelegate ItemCallback;
protected ScrollRect scrollRect;
///
/// 复用的item数组
///
protected RecyclingListViewItem[] childItems;
///
/// 循环列表中,第一个item的索引,最开始每个item都有一个原始索引,最顶部的item的原始索引就是childBufferStart
/// 由于列表是循环复用的,所以往下滑动时,childBufferStart会从0开始到n,然后又从0开始,以此往复
/// 如果是往上滑动,则是从0到-n,再从0开始,以此往复
///
protected int childBufferStart = 0;
///
/// 列表中最顶部的item的真实数据索引,比如有一百条数据,复用10个item,当前最顶部是第60条数据,那么sourceDataRowStart就是59(注意索引从0开始)
///
protected int sourceDataRowStart;
protected bool ignoreScrollChange = false;
protected float previousBuildHeight = 0;
protected const int rowsAboveBelow = 1;
protected virtual void Awake()
{
scrollRect = GetComponent>();
ChildObj.gameObject.SetActive(false);
}protected virtual void OnEnable()
{
scrollRect.onValueChanged.AddListener(OnScrollChanged);
ignoreScrollChange = false;
}protected virtual void OnDisable()
{
scrollRect.onValueChanged.RemoveListener(OnScrollChanged);
}///
/// 供外部调用,强制刷新整个列表,比如数据变化了,刷新一下列表
///
public virtual void Refresh()
{
ReorganiseContent(true);
}///
/// 供外部调用,强制刷新整个列表的局部item
///
/// 开始行
/// 数量
public virtual void Refresh(int rowStart, int count)
{
int sourceDataLimit = sourceDataRowStart + childItems.Length;
for (int i = 0;
i < count;
++i)
{
int row = rowStart + i;
if (row < sourceDataRowStart || row >= sourceDataLimit)
continue;
int bufIdx = WrapChildIndex(childBufferStart + row - sourceDataRowStart);
if (childItems[bufIdx] != null)
{
UpdateChild(childItems[bufIdx], row);
}
}
}///
/// 供外部调用,强制刷新整个列表的某一个item
///
public virtual void Refresh(RecyclingListViewItem item)
{for (int i = 0;
i < childItems.Length;
++i)
{
int idx = WrapChildIndex(childBufferStart + i);
if (childItems[idx] != null && childItems[idx] == item)
{
UpdateChild(childItems[i], sourceDataRowStart + i);
break;
}
}
}///
/// 清空列表
///
public virtual void Clear()
{
RowCount = 0;
}///
/// 供外部调用,强制滚动列表,使某一行显示在列表中
///
/// 行号
/// 目标行显示在列表的位置:顶部,中心,底部
public virtual void ScrollToRow(int row, ScrollPosType posType)
{
scrollRect.verticalNormalizedPosition = GetRowScrollPosition(row, posType);
}///
/// 获得归一化的滚动位置,该位置将给定的行在视图中居中
///
/// 行号
///
public float GetRowScrollPosition(int row, ScrollPosType posType)
{
// 视图高
float vpHeight = ViewportHeight();
float rowHeight = RowHeight();
// 将目标行滚动到列表目标位置时,列表顶部的位置
float vpTop = 0;
switch (posType)
{
case ScrollPosType.Top:
{
vpTop = row * rowHeight;
}
break;
case ScrollPosType.Center:
{
// 目标行的中心位置与列表顶部的距离
float rowCentre = (row + 0.5f) * rowHeight;
// 视口中心位置
float halfVpHeight = vpHeight * 0.5f;
vpTop = Mathf.Max(0, rowCentre - halfVpHeight);
}
break;
case ScrollPosType.Bottom:
{
vpTop = (row+1) * rowHeight - vpHeight;
}
break;
}// 滚动后,列表底部的位置
float vpBottom = vpTop + vpHeight;
// 列表内容总高度
float contentHeight = scrollRect.content.sizeDelta.y;
// 如果滚动后,列表底部的位置已经超过了列表总高度,则调整列表顶部的位置
if (vpBottom > contentHeight)
vpTop = Mathf.Max(0, vpTop - (vpBottom - contentHeight));
// 反插值,计算两个值之间的Lerp参数。也就是value在from和to之间的比例值
return Mathf.InverseLerp(contentHeight - vpHeight, 0, vpTop);
}///
/// 根据行号获取复用的item对象
///
/// 行号
protected RecyclingListViewItem GetRowItem(int row)
{
if (childItems != null &&
row >= sourceDataRowStart && row < sourceDataRowStart + childItems.Length &&
row < rowCount)
{
// 注意这里要根据行号计算复用的item原始索引
return childItems[WrapChildIndex(childBufferStart + row - sourceDataRowStart)];
}return null;
}protected virtual bool CheckChildItems()
{
// 列表视口高度
float vpHeight = ViewportHeight();
float buildHeight = Mathf.Max(vpHeight, PreAllocHeight);
bool rebuild = childItems == null || buildHeight > previousBuildHeight;
if (rebuild)
{int childCount = Mathf.RoundToInt(0.5f + buildHeight / RowHeight());
childCount += rowsAboveBelow * 2;
if (childItems == null)
childItems = new RecyclingListViewItem[childCount];
else if (childCount > childItems.Length)
Array.Resize(ref childItems, childCount);
// 创建item
for (int i = 0;
i < childItems.Length;
++i)
{
if (childItems[i] == null)
{
var item = Instantiate(ChildObj);
childItems[i] = item;
}
childItems[i].RectTransform.SetParent(scrollRect.content, false);
childItems[i].gameObject.SetActive(false);
}previousBuildHeight = buildHeight;
}return rebuild;
}///
/// 列表滚动时,会回调此函数
///
/// 归一化的位置
protected virtual void OnScrollChanged(Vector2 normalisedPos)
{
if (!ignoreScrollChange)
{
ReorganiseContent(false);
}
}///
/// 重新计算列表内容
///
/// 【Unity3D|【游戏开发实战】Unity UGUI实现循环复用列表,显示巨量列表信息,含Demo工程源码】是否要清空列表重新计算
protected virtual void ReorganiseContent(bool clearContents)
{if (clearContents)
{
scrollRect.StopMovement();
scrollRect.verticalNormalizedPosition = 1;
}bool childrenChanged = CheckChildItems();
// 是否要更新整个列表
bool populateAll = childrenChanged || clearContents;
float ymin = scrollRect.content.localPosition.y;
// 第一个可见item的索引
int firstVisibleIndex = (int)(ymin / RowHeight());
int newRowStart = firstVisibleIndex - rowsAboveBelow;
// 滚动变化量
int diff = newRowStart - sourceDataRowStart;
if (populateAll || Mathf.Abs(diff) >= childItems.Length)
{sourceDataRowStart = newRowStart;
childBufferStart = 0;
int rowIdx = newRowStart;
foreach (var item in childItems)
{
UpdateChild(item, rowIdx++);
}}
else if (diff != 0)
{
int newBufferStart = (childBufferStart + diff) % childItems.Length;
if (diff < 0)
{
// 向前滑动
for (int i = 1;
i <= -diff;
++i)
{
// 得到复用item的索引
int wrapIndex = WrapChildIndex(childBufferStart - i);
int rowIdx = sourceDataRowStart - i;
UpdateChild(childItems[wrapIndex], rowIdx);
}
}
else
{
// 向后滑动
int prevLastBufIdx = childBufferStart + childItems.Length - 1;
int prevLastRowIdx = sourceDataRowStart + childItems.Length - 1;
for (int i = 1;
i <= diff;
++i)
{
int wrapIndex = WrapChildIndex(prevLastBufIdx + i);
int rowIdx = prevLastRowIdx + i;
UpdateChild(childItems[wrapIndex], rowIdx);
}
}sourceDataRowStart = newRowStart;
childBufferStart = newBufferStart;
}
}private int WrapChildIndex(int idx)
{
while (idx < 0)
idx += childItems.Length;
return idx % childItems.Length;
}///
/// 获取一行的高度,注意要加上RowPadding
///
private float RowHeight()
{
return RowPadding + ChildObj.RectTransform.rect.height;
}///
/// 获取列表视口的高度
///
private float ViewportHeight()
{
return scrollRect.viewport.rect.height;
}protected virtual void UpdateChild(RecyclingListViewItem child, int rowIdx)
{
if (rowIdx < 0 || rowIdx >= rowCount)
{
child.gameObject.SetActive(false);
}
else
{
if (ItemCallback == null)
{
Debug.Log("RecyclingListView is missing an ItemCallback, cannot function", this);
return;
}// 移动到正确的位置
var childRect = ChildObj.RectTransform.rect;
Vector2 pivot = ChildObj.RectTransform.pivot;
float ytoppos = RowHeight() * rowIdx;
float ypos = ytoppos + (1f - pivot.y) * childRect.height;
float xpos = 0 + pivot.x * childRect.width;
child.RectTransform.anchoredPosition = new Vector2(xpos, -ypos);
child.NotifyCurrentAssignment(this, rowIdx);
// 更新数据
ItemCallback(child, rowIdx);
child.gameObject.SetActive(true);
}
}///
/// 更新content的高度
///
protected virtual void UpdateContentHeight()
{
// 列表高度
float height = ChildObj.RectTransform.rect.height * rowCount + (rowCount - 1) * RowPadding;
// 更新content的高度
var sz = scrollRect.content.sizeDelta;
scrollRect.content.sizeDelta = new Vector2(sz.x, height);
}protected virtual void DisableAllChildren()
{
if (childItems != null)
{
for (int i = 0;
i < childItems.Length;
++i)
{
childItems[i].gameObject.SetActive(false);
}
}
}
}
2、RecyclingListViewItem.cs
using UnityEngine;
///
/// 列表item,你自己写的列表item需要继承该类
///
[RequireComponent(typeof(RectTransform))]
public class RecyclingListViewItem : MonoBehaviour
{private RecyclingListView parentList;
///
/// 循环列表
///
public RecyclingListView ParentList
{
get => parentList;
}private int currentRow;
///
/// 行号
///
public int CurrentRow
{
get => currentRow;
}private RectTransform rectTransform;
public RectTransform RectTransform
{
get
{
if (rectTransform == null)
rectTransform = GetComponent();
return rectTransform;
}
}private void Awake()
{
rectTransform = GetComponent();
}///
/// item更新事件响应函数
///
public virtual void NotifyCurrentAssignment(RecyclingListView v, int row)
{
parentList = v;
currentRow = row;
}
}
推荐阅读
- Unity3D|(完结)Unity游戏开发——新发教你做游戏(七)(Animator控制角色动画播放)
- 深度学习与数据处理|pandas.read_csv() 详解与如何合适的读取行序号与列名
- Unity常见的优化性能设置
- 游戏|元宇宙密室逃脱游戏攻略来啦!
- Java基础|Java 打印空心等腰三角形(方法2)
- Unity3d|Unity中的层级以及渲染顺序
- Unity|unity 一些控制物体移动 小技巧