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

posted @ 2022-03-15 16:00  是只香菇  阅读(1894)  评论(0编辑  收藏  举报