• Unity UGUI 循环滑动列表实现思路及简单代码实现


    前言:

    自己之前其实比着书上实现过一个循环滑动列表,并且商业化到了项目里,上线后也在用。可后来怎么也想不起来细节,看着之前的代码也看不很懂。这次复习一下,希望真能理解它的本质,也记录一下,分享给所有需要的同学。

    我们看源代码之所以看不懂,是因为没有理解它想干什么,只有理解了一段代码的思想,才能够顺着代码去读懂它的本意。要分清楚,什么是主要的,什么是附加的,就先弄明白一件事,它到底要做一件什么事。

    为什么需要循环列表,很多面试在问,项目中也在用,但其实很多项目可能并没有真正遇到过这个问题。一般来说一个循环列表里元素在一百个以下,是没有什么问题的,在真机上也不会卡,直接把所有元素初始化然后塞到ScrollRect的content里就完事了,简单好用,还容易扩展。但我之前的项目,一个scrollrect里要塞500个元素,每个元素里,有几张图片,几个文本,性能就扛不住了,一滑动就卡的不行,那种情况下,才真正需要用到循环滑动列表。

    其实循环滑动列表也不是唯一的选择,很多rpg根本就不做这个。而是用分页的方式显示背包元素,配合展示界面。所以最开始还是弄清需求,弄清数据规模,然后再进行技术选型。

    思路:

    之所以直接塞太多元素会卡顿,本质上还是因为,里边的元素的可见性都是true的,只是通过ScrollRect的Mask把它们裁剪掉了。而通过循环列表,不可见的元素并不会参与运算,所以性能会得到较大提高。所以核心思路还是计算,哪些元素可见,哪些元素不可见。

    目前主流的做法都是,让Content的Size等于真实的Size,也就是说,你有100个元素,那content的大小就是100个元素布局后的宽和高。只是里边的元素并不都显示,我这里说的也是依赖于ScrollRect做的一种方法。因为自带的ScrollRect,可以方便的帮你处理好回弹。如果是自己不依赖于ScrollRect去通过监听鼠标滑动来实现,会麻烦很多。

    所以核心要解决的问题,就是计算剔除,但其中又分为两部分,第一部分是初始化,第二部分是滑动的处理。两部分一起思考复杂度会高很多,因此我先推荐一种简单的思路,理解后自己举一反三即可。

    我们首先要做的一件事,就是记录可视区域。可视区域可以用一个Rect结构来存储。可视区域指哪呢,一般就是指ViewPort,或者ScrollRect的大小本身。在这个矩形内的元素,可见,不在这个矩形内的元素,不可见。这里请注意,如果你的可视区域始终定义为(0, 0, width, height)那你里边的元素在滑动时坐标就需要配合Content的坐标做偏移。如果你的可视区域,是相对于content的左上角来计算的,那滑动过程中,指定index的元素的坐标是不会变的,但可视区域的起始坐标就一直在变。这点注意好即可。

    然后我说一下最简单的理解核心算法的方法, 不考虑效率。首先假设你的滑动列表里要显示100个元素。那么你直接从0到100去遍历,每个元素的位置是不是可以算出来?这个相对简单,知道每行每列有多少个元素(可以走设置,也可以自动计算),那么指定的index位置你就可以算出来,然后看这个元素的rect和可视区域是否有交集,有的话,这个元素就需要被显示,否则就需要隐藏。显示的元素你从一个池子里去拿,不需要显示的元素你给它塞回池子里。这不就可以了?最开始不建议思考什么滑动的时候,下边的元素弄到上边去,不要这样理解,会增加复杂度,而是不可见的元素塞到池子里。可见的元素从池子里拿。

    其实根据上边的思想,最简单的代码实现就有了,后边所处理的细节都是和外部系统进行沟通,以及算法优化而已。因为比如上述算法明显有一个缺点就是,每次滑动关心的元素太多,可见的元素在滑动过程中并没有离开可视界面,如果也从池子里从新拿一份出来,效率太差。所以一般第一步优化就是,记录哪些已经是可见的元素,在滑动过程中,那些仍然可见的就保留。不可见的塞回池子,然后除了之前可见的元素外,新变的可见的再从池子里拿即可。然后就是滑动的方向性。从下往上滑动的时候,小的index会变的不可见,大的index会变得可见,因此一部分元素是不需要计算的等等。

    说的不是很清楚,反正先记录一下吧,后续再优化,有想交流的可以私信给我。后续可能考虑做个视频讲解实现,我不太擅长写图文并茂的文章,写文章功底还差得远。

    贴代码:

    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5. using UnityEditor;
    6. using UnityEngine;
    7. using UnityEngine.UI;
    8. public class CircleScrollView : MonoBehaviour
    9. {
    10. public float ItemWidth = 100;
    11. public float ItemHeight = 100;
    12. public float SpaceX = 10;
    13. public float SpaceY = 10;
    14. public CircleScrollItemBase ItemPrefab;
    15. private float ViewWidth;
    16. private float ViewHeight;
    17. private int ColNum;
    18. private int RowNum;
    19. private Rect VisibleRect;
    20. private ScrollRect _scroll;
    21. public ScrollRect Scroll
    22. {
    23. get
    24. {
    25. if (_scroll == null)
    26. {
    27. _scroll = GetComponent();
    28. }
    29. return _scroll;
    30. }
    31. }
    32. void Awake()
    33. {
    34. InitSet();
    35. }
    36. void InitSet()
    37. {
    38. Rect rect = GetComponent().rect;
    39. ViewWidth = rect.width;
    40. ViewHeight = rect.height;
    41. ColNum = (int)((ViewWidth + SpaceX) / (ItemWidth + SpaceX));
    42. Scroll.vertical = true;
    43. Scroll.horizontal = false;
    44. VisibleRect = new Rect(0, 0, ViewWidth, ViewHeight);
    45. _list = new List();
    46. _pool = new Stack();
    47. Scroll.onValueChanged.AddListener(OnScroll);
    48. }
    49. [NonSerialized]
    50. public int DataCount = 0;
    51. private int VisibleCount = 0;
    52. public void SetData(List<int> datas)
    53. {
    54. SetData(datas.Count);
    55. }
    56. public void SetData(int count)
    57. {
    58. DataCount = count;
    59. CalContentSize();
    60. CalItemCount();
    61. FillItems();
    62. _lastPosition = Scroll.content.anchoredPosition;
    63. }
    64. private void CalItemCount()
    65. {
    66. int rowNum = (int)(Math.Ceiling(ViewHeight + SpaceY) / (ItemHeight + SpaceY));
    67. VisibleCount = (rowNum + 1) * ColNum;
    68. }
    69. public void FillItems()
    70. {
    71. //至少保证
    72. if (_list.Count < VisibleCount)
    73. {
    74. int diff = VisibleCount - _list.Count;
    75. for (int i = 0; i < diff; ++i)
    76. {
    77. _list.Add(GetFromPool());
    78. }
    79. }
    80. for (int i = 0; i < _list.Count; ++i)
    81. {
    82. int startRow = i / ColNum;
    83. int startCol = i % ColNum;
    84. if (i < DataCount)
    85. {
    86. _list[i].gameObject.SetActive(true);
    87. _list[i].Index = i;
    88. _list[i].Refresh(i);
    89. _list[i].LeftTop = new Vector2(startCol * (SpaceX + ItemWidth), -startRow * (SpaceY + ItemHeight));
    90. }
    91. }
    92. }
    93. private List _list;
    94. private Stack _pool;
    95. private void CalContentSize()
    96. {
    97. RowNum = (int) Mathf.Ceil((float)DataCount / ColNum);
    98. Vector2 vec = new Vector2(ColNum * ItemWidth + (ColNum - 1) * SpaceX,
    99. RowNum * ItemHeight + (RowNum - 1) * SpaceY);
    100. vec.x = Math.Max(vec.x, ViewWidth);
    101. vec.y = Math.Max(vec.y, ViewHeight);
    102. Scroll.content.sizeDelta = vec;
    103. Scroll.content.pivot = new Vector2(0, 1);
    104. Scroll.content.anchorMin = new Vector2(0, 1);
    105. Scroll.content.anchorMax = new Vector2(0, 1);
    106. Scroll.content.anchoredPosition = Vector2.zero;
    107. }
    108. private CircleScrollItemBase GetFromPool()
    109. {
    110. if (_pool.Count > 0)
    111. {
    112. return _pool.Pop();
    113. }
    114. else
    115. {
    116. GameObject go = Instantiate(ItemPrefab.gameObject, Scroll.content, false);
    117. go.SetActive(false);
    118. return go.GetComponent();
    119. }
    120. }
    121. private void ReleaseItem(CircleScrollItemBase item)
    122. {
    123. item.gameObject.SetActive(false);
    124. item.Reset();
    125. _pool.Push(item);
    126. }
    127. private Vector2 _lastPosition;
    128. private Rect GetGridRect(int index)
    129. {
    130. int startRow = index / ColNum;
    131. int startCol = index % ColNum;
    132. Vector2 startPos = new Vector2(startCol * (SpaceX + ItemWidth), startRow * (SpaceY + ItemHeight)) - Scroll.content.anchoredPosition;
    133. Rect rect = new Rect(startPos.x, startPos.y, ItemWidth, ItemHeight);
    134. return rect;
    135. }
    136. private void OnScroll(Vector2 delta)
    137. {
    138. for (int i = _list.Count - 1; i >= 0 ; --i)
    139. {
    140. Rect rect = GetGridRect(_list[i].Index);
    141. if (!Visible(rect))
    142. {
    143. ReleaseItem(_list[i]);
    144. _list.RemoveAt(i);
    145. }
    146. else
    147. {
    148. _list[i].gameObject.SetActive(true);
    149. }
    150. }
    151. if (_list.Count == 0)
    152. {
    153. for (int i = 0; i < DataCount; ++i)
    154. {
    155. Rect rect = GetGridRect(i);
    156. if (Visible(rect))
    157. {
    158. CircleScrollItemBase item = AppendItem(i);
    159. _list.Add(item);
    160. }
    161. }
    162. }
    163. else
    164. {
    165. int index = _list[0].Index - 1;
    166. int afterIndex = _list[_list.Count - 1].Index + 1;
    167. while (index >= 0)
    168. {
    169. Rect rect = GetGridRect(index);
    170. if (!Visible(rect))
    171. break;
    172. CircleScrollItemBase item = AppendItem(index);
    173. index--;
    174. _list.Insert(0, item);
    175. }
    176. while (afterIndex < DataCount)
    177. {
    178. Rect rect = GetGridRect(afterIndex);
    179. if (!Visible(rect))
    180. break;
    181. CircleScrollItemBase item = AppendItem(afterIndex);
    182. afterIndex++;
    183. _list.Add(item);
    184. }
    185. }
    186. }
    187. private CircleScrollItemBase AppendItem(int index)
    188. {
    189. int startRow = index / ColNum;
    190. int startCol = index % ColNum;
    191. CircleScrollItemBase item = GetFromPool();
    192. item.gameObject.SetActive(true);
    193. item.Index = index;
    194. item.Refresh(index);
    195. item.LeftTop = new Vector2(startCol * (SpaceX + ItemWidth), -startRow * (SpaceY + ItemHeight));
    196. return item;
    197. }
    198. private bool Visible(Rect rect)
    199. {
    200. return rect.Overlaps(VisibleRect);
    201. }
    202. }

  • 相关阅读:
    Promise的基本用法,基于Promise处理ajax请求
    模数转换器-ADC基础
    JAVA实现数学函数图像
    人家不卡学历,是自己真的没能力
    Linux C语言无名信号量与有名信号量(无名使用 <semaphore.h>,有名信号量<sys/sem.h>)
    P33 JSlider滑块
    mysql之存储过程
    分组后合并记录中的字段值
    Swift网络爬虫与数据可视化的结合 (1)
    Vite5+Vue3整合Tailwindcss详细教程
  • 原文地址:https://blog.csdn.net/Moyiii/article/details/134017813