Unity ScrollView 显示大量数据
原文链接:https://www.cnblogs.com/jingjiangtao/p/15827823.html
问题
Unity的ScrollView可以用滚动视图的形式显示列表。但是当列表中的数据非常多的时候,用ScrollView一次显示出来就会卡顿,并且生成列表的速度也会变慢。
要解决这个问题,可以只显示能看到的数据项,看不到的数据项就不加载,滑动列表时实时更新数据项,这样就只需要创建和更新能看到的数据项,加载和滑动都不会卡顿。
本文只实现垂直滚动的列表。其它形式的列表实现方式类似。
实现方法
要实现一个通用的滚动视图列表,有两种代码结构可供选择:接口和委托。其中接口的实现方法比较传统,委托的实现方法比较灵活。
本文选择用接口实现,如果要用委托,可以把所有接口中定义的方法在对应的位置改成委托。
搭建UI
新建ScrollView
首先需要搭建ScrollView的UI用来测试。新建一个场景,如果场景中没有Canvas就新建一个,然后在Canvas下新建一个Scroll View。
接下来要对列表做一些重要设置:
1. 选中ScrollRect组件所在的物体,取消勾选Horizontal以禁用水平滚动,将Scroll Sensitivity设为50使得鼠标滚轮滑动更灵敏。
2. 选中Content物体,先将Content物体的锚点和大小设置为和Viewport一样大,具体操作是点击Content物体的RectTransform组件的锚点设置工具,按住Alt,选择图中的布局:
这样,Content的大小就和Viewport一样大了
3. 再次选中Content物体,点击Content物体的RectTransform组件锚点设置工具,按住Shift,选择图中的布局:
这样就可以通过Content的PosY值获取Content的位置了。最后放一张Content的RectTransform组件值的图:
新建列表项
接下来需要创建列表项来显示列表中的数据。简单起见,列表项只包含一个表示列表项编号的文本和一个删除按钮。
1. 选中Content,在Content上点击右键,新建空物体,重命名为TextItem,并将空物体调整到合适的大小:
2. 对列表项进行一些重要设置:选中TextItem,点击RectTransform组件的锚点工具,按住Shift,选择图中的布局:
这是设置完成的RectTransform组件值:
3. 在TextItem中添加文本和按钮:
4. 在Assets目录下新建Prefabs目录,把TextItem物体拖到这个目录中,做成一个预制体,然后删掉Content下的TextItem:
5. 在Canvas下新建一个按钮,命名为AddBtn,位置如图,用来实现添加列表项的功能:
至此,用于测试的UI搭建完毕。
代码实现
存储滚动视图列表项的类 LoopScrollItem.cs
这个类用于保存单独列表项中的数据,需要用列表项的GameObject初始化。在这个例子中,列表项的数据只有它自身的RectTransform和GameObject,如果要加新的数据,可以直接在这个类中暴露出来,方便访问。
namespace LoopScrollTest.LoopScroll { public class LoopScrollItem { public RectTransform RectTransform => _transform; private RectTransform _transform; private GameObject _gameObject; public LoopScrollItem(GameObject gameObject) { _gameObject = gameObject; _transform = _gameObject.transform as RectTransform; } } }
用于滚动视图脚本调用的接口 ILoopScrollView.cs
滚动列表的接口,包含几种对列表的操作。如果有其它操作,可以在接口中声明,并在LoopScrollView中调用。
namespace LoopScrollTest.LoopScroll { public interface ILoopScrollView { /// <summary> /// 用给定索引处的数据更新给定的列表项 /// </summary> void UpdateItemContent(int index, LoopScrollItem item); /// <summary> /// 用给定索引处的数据生成列表项并返回 /// </summary> GameObject InitScrollViewList(int index); /// <summary> /// 生成一个新的列表项并返回 /// </summary> GameObject InitOneItem(); /// <summary> /// 删除指定的列表项 /// </summary> void DeleteOneItem(Transform item); /// <summary> /// 删除一个列表项之后处理其它所有的列表项 /// </summary> void OtherItemAfterDeleteOne(LoopScrollItem item); } }
控制滚动视图的脚本 LoopScrollView.cs
滚动视图的主要类,继承了MonoBehaviour,并且需要和ScrollRect组件挂在同一个物体上,并将ScrollRect组件拖到引用槽中。这个类处理了对滚动视图的所有操作,并调用了接口中的方法。
namespace LoopScrollTest.LoopScroll { [RequireComponent(typeof(ScrollRect))] public class LoopScrollView : MonoBehaviour { public ScrollRect scrollRect; // 列表项数组 protected List<LoopScrollItem> _items = new List<LoopScrollItem>(); protected float _itemHeight; protected int _visibleCount; protected float _visibleHeight; protected int _sourceListCount; // 列表操作接口类型的实例,需要在初始化时赋值 protected ILoopScrollView _scrollViewOperate; protected virtual void Update() { RefreshGestureScrollView(); } /// <summary> /// 初始化循环列表的数值和引用 /// </summary> public virtual void InitScrollView(float itemHeight, ILoopScrollView scrollViewOperate) { _itemHeight = itemHeight; _visibleHeight = (scrollRect.transform as RectTransform).rect.height; _visibleCount = (int)(_visibleHeight / _itemHeight) + 1; _scrollViewOperate = scrollViewOperate; } /// <summary> /// 初始化循环列表的数据源 /// </summary> public virtual void InitScrollViewList(int sourceListCount) { _sourceListCount = sourceListCount; int generateCount = ResizeContent(); scrollRect.content.anchoredPosition = Vector2.zero; _items.Clear(); for (int i = 0; i < generateCount; i++) { GameObject itemGameObject = _scrollViewOperate.InitScrollViewList(i); LoopScrollItem item = new LoopScrollItem(itemGameObject); float itemY = -i * _itemHeight; item.RectTransform.anchoredPosition = new Vector2(scrollRect.content.anchoredPosition.x, itemY); _items.Add(item); } } /// <summary> /// 将指定索引的项对齐到列表界面的顶部 /// </summary> public virtual void MoveIndexToTop(int index) { float contentY = index * _itemHeight; scrollRect.content.anchoredPosition = new Vector2(scrollRect.content.anchoredPosition.x, contentY); RefreshGestureScrollView(); } /// <summary> /// 将指定索引的项对齐到列表界面的底部 /// </summary> public virtual void MoveIndexToBottom(int index) { float contentY = (index + 1) * _itemHeight - _visibleHeight; contentY = contentY < 0 ? 0f : contentY; scrollRect.content.anchoredPosition = new Vector2(scrollRect.content.anchoredPosition.x, contentY); RefreshGestureScrollView(); } /// <summary> /// 判断指定的索引是否需要聚焦到底部,如果需要就对齐 /// </summary> public virtual void MoveToBottomIfNeeded(int index) { float itemY = -(index + 1) * _itemHeight; float bottomY = -(scrollRect.content.anchoredPosition.y + _visibleHeight); if (itemY < bottomY) { MoveIndexToBottom(index); } } /// <summary> /// 添加一条新项到列表中 /// </summary> public virtual void AddOneItem() { _sourceListCount++; int generateCount = ResizeContent(); if (_items.Count < generateCount) { GameObject itemGameObject = _scrollViewOperate.InitOneItem(); LoopScrollItem item = new LoopScrollItem(itemGameObject); _items.Add(item); } RefreshGestureScrollView(); } /// <summary> /// 删除一条列表项 /// </summary> public virtual void DeleteOneItem() { _sourceListCount--; int generateCount = ResizeContent(); if (generateCount < _items.Count) { int lastIndex = _items.Count - 1; _scrollViewOperate.DeleteOneItem(_items[lastIndex].RectTransform); _items.RemoveAt(lastIndex); } RefreshGestureScrollView(); foreach (LoopScrollItem item in _items) { _scrollViewOperate.OtherItemAfterDeleteOne(item); } } /// <summary> /// 根据当前手势项的数量重新调整内容的高度 /// </summary> protected virtual int ResizeContent() { int generateCount = Mathf.Min(_visibleCount, _sourceListCount); float contentHeight = _sourceListCount * _itemHeight; scrollRect.content.sizeDelta = new Vector2(scrollRect.content.sizeDelta.x, contentHeight); return generateCount; } /// <summary> /// 刷新列表内容 /// </summary> protected virtual void RefreshGestureScrollView() { float contentY = scrollRect.content.anchoredPosition.y; int skipCount = (int)(contentY / _itemHeight); for (int i = 0; i < _items.Count; i++) { if (skipCount >= 0 && skipCount < _sourceListCount) { _scrollViewOperate.UpdateItemContent(skipCount, _items[i]); float itemY = -skipCount * _itemHeight; _items[i].RectTransform.anchoredPosition = new Vector2(scrollRect.content.anchoredPosition.x, itemY); skipCount++; } } } } }
以上的脚本就是滚动视图的所有代码,使用时挂载需要的脚本,并在其它类中实现接口即可使用。
以下是调用循环列表的示例:
控制单独项的脚本 TextItem.cs
脚本继承了MonoBehaviour,需要挂在TextItem预制体上,并将Text和Button拖到引用字段中,用来于控制单独列表项的行为。
namespace LoopScrollTest { public class TextItem : MonoBehaviour { public Text text; public Button deleteBtn; public SourceData Data => _sourceData; private Action<TextItem> _deleteItemAction; private SourceData _sourceData; private void Awake() { deleteBtn.onClick.AddListener(OnClickDelete); } /// <summary> /// 初始化数据和按钮点击的委托 /// </summary> public void Init(SourceData data, Action<TextItem> deleteItemAction) { _sourceData = data; _deleteItemAction = deleteItemAction; text.text = _sourceData.text; } /// <summary> /// 更新引用的源数据 /// </summary> public void UpdateSourceData(SourceData data) { _sourceData = data; text.text = _sourceData.text; } /// <summary> /// 删除按钮的点击事件 /// </summary> private void OnClickDelete() { _deleteItemAction?.Invoke(this); } } }
数据源实体类
namespace LoopScrollTest { public class SourceData { // 数据内容 public string text; } }
调用其它脚本用于测试的脚本 TestLoopScrollView.cs
用于控制UI和调用循环列表的脚本,需要在场景中新建GameObject物体,并将对应的引用拖到引用槽中以赋值。
namespace LoopScrollTest { public class TestLoopScrollView : MonoBehaviour, ILoopScrollView { [Tooltip("源数据列表的个数,修改后需要重新运行才能生效")] public uint count = 300; // 对滚动列表的引用 public LoopScrollView loopScrollView; // 对添加按钮的引用 public Button addBtn; // 对ScrollView的Content物体的引用 public RectTransform content; // 对单个列表项预制体的引用 public TextItem textItemPrefab; // 源数据列表 private List<SourceData> _sourceList = new List<SourceData>(); private int nextId = 1; private void Awake() { addBtn.onClick.AddListener(OnClickAddItem); // 获取单个列表项的高度 float itemHeight = (textItemPrefab.transform as RectTransform).rect.height; // 初始化滚动列表中的引用 loopScrollView.InitScrollView(itemHeight, this); } private void Start() { // 初始化源数据列表,作为列表的数据源 for (int i = 0; i < count; i++) { SourceData data = new SourceData { text = nextId.ToString() }; _sourceList.Add(data); nextId++; } // 初始化滚动列表的显示 loopScrollView.InitScrollViewList(_sourceList.Count); } /// <summary> /// 在列表中追加新项 /// </summary> private void OnClickAddItem() { SourceData data = new SourceData { text = nextId.ToString(), }; _sourceList.Add(data); nextId++; loopScrollView.AddOneItem(); loopScrollView.MoveIndexToBottom(_sourceList.Count - 1); } /// <summary> /// 根据给定的源数据生成单个列表项 /// </summary> private TextItem InitTextItem(SourceData sourceData) { TextItem item = Instantiate(textItemPrefab, content); item.Init(sourceData, DeleteItemAction); return item; } /// <summary> /// 点击单个列表项的删除按钮的回调,删除单个列表项 /// </summary> private void DeleteItemAction(TextItem item) { _sourceList.Remove(item.Data); loopScrollView.DeleteOneItem(); } #region 实现的接口方法 public virtual void DeleteOneItem(Transform item) { DestroyImmediate(item.gameObject); } public virtual GameObject InitOneItem() { TextItem item = InitTextItem(_sourceList[_sourceList.Count - 1]); return item.gameObject; } public virtual GameObject InitScrollViewList(int index) { TextItem item = InitTextItem(_sourceList[index]); return item.gameObject; } public virtual void OtherItemAfterDeleteOne(LoopScrollItem item) { } public virtual void UpdateItemContent(int index, LoopScrollItem item) { // 更新列表项引用的源数据 TextItem textItem = item.RectTransform.GetComponent<TextItem>(); textItem.UpdateSourceData(_sourceList[index]); } #endregion } }
效果
完整项目
https://github.com/jingjiangtao/LoopScrollView
参考
https://blog.csdn.net/lxt610/article/details/90036080