文章

UGUI中的无限滚动列表实现

浅析无限滚动列表是啥,以及提供一个基于UGUI的ScrollView实现的通用无限滚动列表

UGUI中的无限滚动列表实现

一、应用背景

  • 若某页面内需实例化海量UI元素,且这导致了该页面的卡顿,针对不同情况可采取不同解决方案
    • 若只有在初始化时轻微卡顿,且初始化完成后的使用过程中不卡顿,且业务需求能确保该页面的数据后续不会大量增长(适用于有限数据量的页面)
      • 此时可只关注如何解决初始化卡顿的问题,将初始化时的UI元素实例化通过协程分帧异步实例化即可(原本必须等待全部实例化完成,故而会阻塞卡顿)
    • 若不仅只在初始化时卡顿,使用过程中也卡顿,那就必须要进一步优化才行
      • 从设计层面优化:将该页面的所有UI元素按类别分页以缓解单页的压力,缺点是治标不治本,且会增加美术工作量
      • 从技术层面优化:引入无限滚动列表的实现,且每次使用都需引入新的CSharp代码(因为一般需通过继承来扩展新列表,这对Lua热更新项目来说意味着新列表无法走热更新)

二、基本原理

  • 无限滚动列表的原理就是借助对象池使得场景中最多只存在足够塞满视口的UI元素实例,然后在视口内复用这些实例来模拟列表滚动的视图变化,以提高列表的性能,其需处理的核心问题如下
    • 拿到完整列表数据后,根据数据数量和列表元素预制体尺寸计算Content的尺寸
    • 在滑动列表的过程中
      • 判断哪些视口中的元素超出了视口范围,需将其回收入对象池并清空状态
      • 判断哪些逻辑上的元素进入了视口范围,需从池中取出对象并赋予数据应用到正确的位置
      • 记录当前视口内的元素是列表中的哪部分数据,保证数据驱动的视图显示无误
    • 如果数据发生了变化,则需根据新数据重新生成Content尺寸,并更新已经在视口内的元素的视图

三、工程实现

3.1 实现思路

  • 效果大概如下,思路就是将一行(针对垂直滚动列表)或一列(针对水平滚动列表)列表元素TGrid视作一个GridBundle整体,将其作为对象池的对象维护,滚动时以其为基本单位进行显示与隐藏即可,实现细节详见代码和示例场景:https://github.com/WhythZ/InfiniteScrollView

无限滚动列表示意图.png

3.2 接入方法

  • 使用该列表前需先制作列表元素的UI预制体,然后为其编写对应的脚本(例如TestGrid)和数据结构(例如TestGridData

无限滚动列表元素预制体示例.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using UnityEngine;
using UnityEngine.UI;

//类TestGrid对应的数据结构体,实际业务中元素的数据结构通常更为复杂
[System.Serializable]
public struct TestGridData
{
    public Sprite iconSprite; //赋值给Image组件的sprite
    public string descString; //赋值给Text组件的text
}

//挂载在列表元素预制体上的脚本类
public class TestGrid : MonoBehaviour
{
    public Image iconImg;
    public Text descTxt;

    public void RefreshView(Sprite _sprite, string _text)
    {
        iconImg.sprite = _sprite;
        descTxt.text = _text;
        GetComponent<Button>().onClick.RemoveAllListeners();
        GetComponent<Button>().onClick.AddListener(() => Debug.LogError("Click " + _text));
    }
}
  • 然后用上面两个东西作为泛型参数创建一个InfiniteScrollView的派生类脚本,实现抽象方法ResetGrid即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
using UnityEngine;
using System.Collections.Generic;

//InfiniteScrollView的一个使用示例
public class TestInfiniteScrollView : InfiniteScrollView<TestGrid, TestGridData>
{
    //实现抽象父类中需要的抽象方法,用于更新格子的数据和视图
    protected override void ResetGrid(TestGrid _gridInstance, TestGridData _gridData, int _gridIdx)
    {
        //此处显示格子,因为只有在视口范围内的格子才会被使用当前方法刷新
        _gridInstance.gameObject.SetActive(true);
        _gridInstance.RefreshView(_gridData.iconSprite, _gridData.descString);
    }

    #region ForTest
    //临时存放用于测试的数据
    private TestGridData[] dataForTest;

    private void Start()
    {
        //生成一堆用来测试用数据,之于实际应用中则需根据业务需求获取数据(如读取服务端、配置表、本地存档等)
        List<TestGridData> dataTemplates = new List<TestGridData>();
        dataTemplates.Add(new TestGridData() { iconSprite = Resources.Load<Sprite>("Sprites/Bow"), descString = "Bow" }); //此处从Assets/Resources/下加载资源
        dataTemplates.Add(new TestGridData() { iconSprite = Resources.Load<Sprite>("Sprites/Hammer"), descString = "Ham" });
        dataTemplates.Add(new TestGridData() { iconSprite = Resources.Load<Sprite>("Sprites/Sword"), descString = "Swd" });
        dataForTest = new TestGridData[333];
        for (int _i = 0; _i < dataForTest.Length; _i++)
        {
            dataForTest[_i] = dataTemplates[_i % dataTemplates.Count];
            dataForTest[_i].descString += (_i / dataTemplates.Count + 1).ToString();
        }

        //用这些数据生成滚动列表的Content
        Initialize(dataForTest);
    }

    protected override void Update()
    {
        base.Update();

        //用于测试
        if (Input.GetKeyDown(KeyCode.Alpha1))
        {
            for (int _i = 0; _i < dataForTest.Length; _i++)
            {
                //若索引对应的Grid在视口范围内,则会刷新该列表元素
                bool _visible = RefreshGridViewIfVisible(_i);
                if (_visible)
                    Debug.Log("TestInfiniteScrollView: The grid " + _i.ToString() + " is in viewport");
            }
        }
        if (Input.GetKeyDown(KeyCode.Alpha2))
        {
            //重新构造一组数据
            List<TestGridData> _newList = new List<TestGridData>();
            TestGridData _testGridData = new TestGridData() { iconSprite = Resources.Load<Sprite>("Sprites/Bow"), descString = "Bow" };
            for (int _i = 0; _i < 100; _i++)
            {
                _testGridData.descString = "New" + (_i + 1).ToString();
                _newList.Add(_testGridData);
            }
            //刷新一下界面
            Initialize(_newList);
        }
    }
    #endregion
}
  • 然后直接在所需位置创建一个ScrollView,为其挂载上述实现的派生类脚本,配置好参数即可测试使用

无限滚动列表元素测试列表示例.png

本文由作者按照 CC BY-NC-SA 4.0 进行授权