Unity中实现无限滑动的ScrollView
当邮件中有1000封邮件,商店列表中有1000个物体,如果直接实例化1000条数据显示则会大大增加 DrawCall,而大量不可见的数据被 Mask 组件排除在可视范围之外,但他们依然存在,这时就需要考虑通过一个无限滑动的 ScrollView 来优化渲染性能,下面这种复用方式提供了一个通用的思路来处理此类问题。
效果展示
实现思路
通过头下标和尾下标记录当前实例化数据的最大最小索引,之后用Content的锚点位置与当头下标的锚点位置进行比较判断滑动的方向以及是否超出滑动范围,如果正方向滑动超出范围则将第一个元素移动到最后一个,如果反方向滑动超出范围则将最后一个元素移动到第一个,这样场景中始终存在5个实例化的元素,依次改变元素的位置和显示即可。
使用说明
- 此功能脚本是对 ScrollRect 的扩展,所以必须添加 UGUI 提供的基础 Scroll View
- Content 上必须添加 GridLayoutGroup 组件,通过 GridLayoutGroup 组件设计布局,(我在 代码中对 startCorner、startAxis、childAlignment 和 constraintCount 进行了限制,不需要对其设置)
- 不能添加 ContentSizeFitter 组件
- 先调用 SetTotalCount 方法设置总的数据数量再调用 Init 方法进行初始化
- 根据需求修改 SetShow 方法体
- 只适用于单向单列滑动的情况,不能满足竖直和水平同时滑动的需求,因为大多数无限滑动列表的使用场景都是单向的
- 根据实际情况配置固定 item 数量,比如cell宽高、滑动区域的宽高等等
1void Start()
2{
3 // 先设置数量,再初始化
4 other.SetTotalCount(200);
5 other.Init();
6
7
8 // TODO ...
9 other.DestoryAll();
10}
完整代码
将InfiniteScrollView脚本挂载到ScrollView上
1using System.Collections.Generic;
2using UnityEngine;
3using UnityEngine.UI;
4using System.Linq;
5
6/// <summary>
7/// 无限滑动列表
8/// </summary>
9public class InfiniteScrollView : MonoBehaviour
10{
11 private ScrollRect scrollRect;//滑动框组件
12 private RectTransform content;//滑动框的Content
13 private GridLayoutGroup layout;//布局组件
14
15 [Header("滑动类型")]
16 public ScrollType scrollType;
17 [Header("固定的Item数量")]
18 public int fixedCount;
19 [Header("Item的预制体")]
20 public GameObject itemPrefab;
21
22 private int totalCount;//总的数据数量
23 private List<RectTransform> dataList = new List<RectTransform>();//数据实体列表
24 private int headIndex;//头下标
25 private int tailIndex;//尾下标
26 private Vector2 firstItemAnchoredPos;//第一个Item的锚点坐标
27
28 #region Init
29
30 /// <summary>
31 /// 实例化Item
32 /// </summary>
33 private void InitItem()
34 {
35 for (int i = 0; i < fixedCount; i++)
36 {
37 GameObject tempItem = Instantiate(itemPrefab, content);
38 dataList.Add(tempItem.GetComponent<RectTransform>());
39 SetShow(tempItem.GetComponent<RectTransform>(), i);
40 }
41 }
42
43 /// <summary>
44 /// 设置Content大小
45 /// </summary>
46 private void SetContentSize()
47 {
48 content.sizeDelta = new Vector2
49 (
50 layout.padding.left + layout.padding.right + totalCount * (layout.cellSize.x + layout.spacing.x) - layout.spacing.x - content.rect.width,
51 layout.padding.top + layout.padding.bottom + totalCount * (layout.cellSize.y + layout.spacing.y) - layout.spacing.y
52 );
53 }
54
55 /// <summary>
56 /// 设置布局
57 /// </summary>
58 private void SetLayout()
59 {
60 layout.startCorner = GridLayoutGroup.Corner.UpperLeft;
61 layout.startAxis = GridLayoutGroup.Axis.Horizontal;
62 layout.childAlignment = TextAnchor.UpperLeft;
63 layout.constraintCount = 1;
64 if (scrollType == ScrollType.Horizontal)
65 {
66 scrollRect.horizontal = true;
67 scrollRect.vertical = false;
68 layout.constraint = GridLayoutGroup.Constraint.FixedRowCount;
69 }
70 else if (scrollType == ScrollType.Vertical)
71 {
72 scrollRect.horizontal = false;
73 scrollRect.vertical = true;
74 layout.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
75 }
76 }
77
78 /// <summary>
79 /// 得到第一个数据的锚点位置
80 /// </summary>
81 private void GetFirstItemAnchoredPos()
82 {
83 firstItemAnchoredPos = new Vector2
84 (
85 layout.padding.left + layout.cellSize.x / 2,
86 -layout.padding.top - layout.cellSize.y / 2
87 );
88 }
89
90 #endregion
91
92 #region Main
93
94 /// <summary>
95 /// 滑动中
96 /// </summary>
97 private void OnScroll(Vector2 v)
98 {
99 if (dataList.Count == 0)
100 {
101 Debug.LogWarning("先调用SetTotalCount方法设置数据总数量再调用Init方法进行初始化");
102 return;
103 }
104
105 if (scrollType == ScrollType.Vertical)
106 {
107 //向上滑
108 while (content.anchoredPosition.y >= layout.padding.top + (headIndex + 1) * (layout.cellSize.y + layout.spacing.y)
109 && tailIndex != totalCount - 1)
110 {
111 //将数据列表中的第一个元素移动到最后一个
112 RectTransform item = dataList[0];
113 dataList.Remove(item);
114 dataList.Add(item);
115
116 //设置位置
117 SetPos(item, tailIndex + 1);
118 //设置显示
119 SetShow(item, tailIndex + 1);
120
121 headIndex++;
122 tailIndex++;
123 }
124 //向下滑
125 while (content.anchoredPosition.y <= layout.padding.top + headIndex * (layout.cellSize.y + layout.spacing.y)
126 && headIndex != 0)
127 {
128 //将数据列表中的最后一个元素移动到第一个
129 RectTransform item = dataList.Last();
130 dataList.Remove(item);
131 dataList.Insert(0, item);
132
133 //设置位置
134 SetPos(item, headIndex - 1);
135 //设置显示
136 SetShow(item, headIndex - 1);
137
138 headIndex--;
139 tailIndex--;
140 }
141 }
142 else if (scrollType == ScrollType.Horizontal)
143 {
144 //向左滑
145 while (content.anchoredPosition.x <= -layout.padding.left - (headIndex + 1) * (layout.cellSize.x + layout.spacing.x)
146 && tailIndex != totalCount - 1)
147 {
148 //将数据列表中的第一个元素移动到最后一个
149 RectTransform item = dataList[0];
150 dataList.Remove(item);
151 dataList.Add(item);
152
153 //设置位置
154 SetPos(item, tailIndex + 1);
155 //设置显示
156 SetShow(item, tailIndex + 1);
157
158 headIndex++;
159 tailIndex++;
160 }
161 //向右滑
162 while (content.anchoredPosition.x >= -layout.padding.left - headIndex * (layout.cellSize.x + layout.spacing.x)
163 && headIndex != 0)
164 {
165 //将数据列表中的最后一个元素移动到第一个
166 RectTransform item = dataList.Last();
167 dataList.Remove(item);
168 dataList.Insert(0, item);
169
170 //设置位置
171 SetPos(item, headIndex - 1);
172 //设置显示
173 SetShow(item, headIndex - 1);
174
175 headIndex--;
176 tailIndex--;
177 }
178 }
179 }
180
181 #endregion
182
183 #region Tool
184
185 /// <summary>
186 /// 设置位置
187 /// </summary>
188 private void SetPos(RectTransform trans, int index)
189 {
190 if (scrollType == ScrollType.Horizontal)
191 {
192 trans.anchoredPosition = new Vector2
193 (
194 index == 0 ? layout.padding.left + firstItemAnchoredPos.x :
195 layout.padding.left + firstItemAnchoredPos.x + index * (layout.cellSize.x + layout.spacing.x),
196 firstItemAnchoredPos.y
197 );
198 }
199 else if (scrollType == ScrollType.Vertical)
200 {
201 trans.anchoredPosition = new Vector2
202 (
203 firstItemAnchoredPos.x,
204 index == 0 ? -layout.padding.top + firstItemAnchoredPos.y :
205 -layout.padding.top + firstItemAnchoredPos.y - index * (layout.cellSize.y + layout.spacing.y)
206 );
207 }
208 }
209
210 #endregion
211
212 #region 外部调用
213
214 /// <summary>
215 /// 初始化
216 /// </summary>
217 public void Init()
218 {
219 scrollRect = GetComponent<ScrollRect>();
220 content = scrollRect.content;
221 layout = content.GetComponent<GridLayoutGroup>();
222 scrollRect.onValueChanged.AddListener((Vector2 v) => OnScroll(v));
223
224 //设置布局
225 SetLayout();
226
227 //设置头下标和尾下标
228 headIndex = 0;
229 tailIndex = fixedCount - 1;
230
231 //设置Content大小
232 SetContentSize();
233
234 //实例化Item
235 InitItem();
236
237 //得到第一个Item的锚点位置
238 GetFirstItemAnchoredPos();
239 }
240
241 /// <summary>
242 /// 设置显示
243 /// </summary>
244 public void SetShow(RectTransform trans, int index)
245 {
246 //=====根据需求进行编写
247 trans.GetComponentInChildren<Text>().text = index.ToString();
248 trans.name = index.ToString();
249 }
250
251 /// <summary>
252 /// 设置总的数据数量
253 /// </summary>
254 public void SetTotalCount(int count)
255 {
256 totalCount = count;
257 }
258
259 /// <summary>
260 /// 销毁所有的元素
261 /// </summary>
262 public void DestoryAll()
263 {
264 for (int i = dataList.Count - 1; i >= 0; i--)
265 {
266 DestroyImmediate(dataList[i].gameObject);
267 }
268 dataList.Clear();
269 }
270
271 #endregion
272}
273
274/// <summary>
275/// 滑动类型
276/// </summary>
277public enum ScrollType
278{
279 Horizontal,//竖直滑动
280 Vertical,//水平滑动
281}
Lua版本
由于实际使用上还是用 Lua,于是根据原作者提供的思路和 c# 代码,翻译成了 Lua脚本:
1local class = require("middleclass")
2local MyScrollRectView = class("MyScrollRectView")
3
4local ScrollTypeEnum = {
5 HORIZONTAL = 0,
6 VERTICAL = 1
7}
8
9---@param parentTransform CS.UnityEngine.RectTransform 父布局
10function MyScrollRectView:initialize(parentTransform)
11 --- TODO 包含 ScrollView 的根布局(结构参考Gif)
12 ---@type CS.UnityEngine.GameObject
13 self.uiRoot = nil
14 self:_initField()
15 self.uiRoot.transform:SetParent(parentTransform.transform, false)
16end
17
18function MyScrollRectView:_initField()
19 --- TODO 要展示的 ItemPrefab
20 ---@type CS.UnityEngine.GameObject
21 self.itemPrefab = nil
22
23 ---@type CS.UnityEngine.UI.ScrollRect 滑动框组件
24 self.scrollRect = self.uiRoot:GetComponent(typeof(CS.UnityEngine.UI.ScrollRect))
25
26 ---@type CS.UnityEngine.RectTransform 滑动框的Content
27 self.content = self.scrollRect.content
28 self.content.anchorMax = CS.UnityEngineVector2(0.5, 1)
29 self.content.anchorMin = CS.UnityEngineVector2(0.5, 1)
30 self.content.pivot = CS.UnityEngineVector2(0, 1)
31
32 ---@type CS.UnityEngine.UI.GridLayoutGroup 布局组件
33 self.layout = self.uiRoot.transform:Find("Viewport/Content").gameObject:GetComponent(typeof(CS.UnityEngine.UI.GridLayoutGroup))
34
35 ---@type number 滑动方式 0表示水平 1表示竖直
36 self.scrollType = ScrollTypeEnum.VERTICAL
37
38 ---@type number 固定的 Item 数量
39 self.fixedCount = 0
40
41 -- 总的数据数量
42 self.totalCount = 0
43
44 -- 数据实体列表
45 self.dataList = {}
46
47 -- 头下标
48 self.headIndex = 0
49
50 -- 尾下标
51 self.tailIndex = 0
52
53 -- 第一个Item的锚点坐标
54 ---@type CS.UnityEngine.Vector2
55 self.firstItemAnchoredPos = CS.UnityEngine.Vector2.zero
56end
57
58function MyScrollRectView:_InitItem()
59 ---@type CS.UnityEngine.GameObject
60 local tempItem
61 for i = 0, self.fixedCount - 1 do
62 tempItem = CS.UnityEngine.GameObject.Instantiate(self.itemPrefab, self.content)
63 table.insert(self.dataList, tempItem:GetComponent(typeof(CS.UnityEngine.RectTransform)))
64 self:_SetShow(tempItem:GetComponent(typeof(CS.UnityEngine.RectTransform)), i)
65 end
66end
67
68function MyScrollRectView:_SetLayout()
69 self.layout.startCorner = CS.UnityEngine.UI.GridLayoutGroup.Corner.UpperLeft
70 self.layout.startAxis = CS.UnityEngine.UI.GridLayoutGroup.Axis.Horizontal
71 self.layout.childAlignment = CS.UnityEngine.TextAnchor.UpperLeft
72 self.layout.constraintCount = 1
73
74 -- ScrollType.Horizontal
75 if self.scrollType == ScrollTypeEnum.HORIZONTAL then
76 self.scrollRect.horizontal = true
77 self.scrollRect.vertical = false
78 self.layout.constraint = CS.UnityEngine.UI.GridLayoutGroup.Constraint.FixedRowCount
79 elseif self.scrollType == ScrollTypeEnum.VERTICAL then
80 self.scrollRect.horizontal = false
81 self.scrollRect.vertical = true
82 self.layout.constraint = CS.UnityEngine.UI.GridLayoutGroup.Constraint.FixedColumnCount
83 end
84end
85
86
87function MyScrollRectView:_OnScroll(v)
88 if #self.dataList == 0 then
89 print("先调用 SetDataLength 方法设置数据总数量,再调用 Init 方法进行初始化")
90 return
91 end
92
93 if self.scrollType == ScrollTypeEnum.VERTICAL then -- Vertical
94 -- 向上滑
95 while (self.content.anchoredPosition.y >= (self.layout.padding.top + (self.headIndex + 1) * (self.layout.cellSize.y + self.layout.spacing.y))) and (self.tailIndex ~= self.totalCount - 1) do
96 -- 将数据列表中的第一个元素移动到最后一个
97 local item = self.dataList[1]
98 table.remove(self.dataList, 1)
99 table.insert(self.dataList, item)
100
101 -- 设置位置
102 self:_SetPos(item, self.tailIndex + 1)
103
104 --设置显示
105 self:_SetShow(item, self.tailIndex + 1)
106
107 self.headIndex = self.headIndex + 1
108 self.tailIndex = self.tailIndex + 1
109 end
110
111 -- 向下滑
112 while (self.content.anchoredPosition.y <= self.layout.padding.top + self.headIndex * (self.layout.cellSize.y + self.layout.spacing.y)) and self.headIndex ~= 0 do
113 -- 将数据列表中的最后一个元素移动到第一个
114
115 local item = self.dataList[#self.dataList]
116 table.remove(self.dataList, #self.dataList)
117 table.insert(self.dataList, 1, item)
118
119 -- 设置位置
120 self:_SetPos(item, self.headIndex - 1)
121
122 --设置显示
123 self:_SetShow(item, self.headIndex - 1)
124
125 self.headIndex = self.headIndex - 1
126 self.tailIndex = self.tailIndex - 1
127 end
128 elseif self.scrollType == ScrollTypeEnum.HORIZONTAL then -- 水平滑动
129 -- 向左滑
130 while (self.content.anchoredPosition.x <= -self.layout.padding.left - (self.headIndex + 1) * (self.layout.cellSize.x + self.layout.spacing.x) and self.tailIndex ~= self.totalCount - 1) do
131 --将数据列表中的第一个元素移动到最后一个
132 local item = self.dataList[1]
133 table.remove(self.dataList, 1)
134 table.insert(self.dataList, item)
135
136 --设置位置
137 self:_SetPos(item, self.tailIndex + 1)
138
139 --设置显示
140 self:_SetShow(item, self.tailIndex + 1)
141
142 self.headIndex = self.headIndex + 1
143 self.tailIndex = self.tailIndex + 1
144 end
145
146 --向右滑
147 while (self.content.anchoredPosition.x >= -self.layout.padding.left - self.headIndex * (self.layout.cellSize.x + self.layout.spacing.x) and self.headIndex ~= 0) do
148 --将数据列表中的第一个元素移动到最后一个
149 local item = self.dataList[1]
150 table.remove(self.dataList, 1)
151 table.insert(self.dataList, item)
152
153 --设置位置
154 self:_SetPos(item, self.headIndex - 1)
155
156 --设置显示
157 self:_SetShow(item, self.headIndex - 1)
158
159 self.headIndex = self.headIndex - 1
160 self.tailIndex = self.tailIndex - 1
161 end
162 end
163end
164
165---@param item CS.UnityEngine.GameObject
166function MyScrollRectView:_SetPos(item, index)
167 local x = 0
168 local y = 0
169 ---@type CS.UnityEngine.RectTransform
170 local rt = item.transform
171 if self.scrollType == ScrollTypeEnum.HORIZONTAL then
172 x = index == 0 and (self.layout.padding.left + self.firstItemAnchoredPos.x) or (self.layout.padding.left + self.firstItemAnchoredPos.x + index * (self.layout.cellSize.x + self.layout.spacing.x))
173 y = self.firstItemAnchoredPos.y
174 rt.anchoredPosition = CS.UnityEngine.Vector2(x, y)
175 elseif self.scrollType == ScrollTypeEnum.VERTICAL then
176 x = self.firstItemAnchoredPos.x
177 y = index == 0 and (-self.layout.padding.top + self.firstItemAnchoredPos.y) or (-self.layout.padding.top + self.firstItemAnchoredPos.y - index * (self.layout.cellSize.y + self.layout.spacing.y))
178 rt.anchoredPosition = CS.UnityEngine.Vector2(x, y)
179 end
180end
181
182function MyScrollRectView:_SetContentSize()
183 if self.scrollType == ScrollTypeEnum.VERTICAL then
184 local x = self.layout.padding.left + self.layout.padding.right + self.layout.cellSize.x + self.layout.spacing.x
185 local y = self.layout.padding.top + self.layout.padding.bottom + self.totalCount * (self.layout.cellSize.y + self.layout.spacing.y) - self.layout.spacing.y
186 self.content.sizeDelta = CS.UnityEngine.Vector2(x, y)
187 elseif self.scrollType == ScrollTypeEnum.HORIZONTAL then
188 local x = self.layout.padding.left + self.layout.padding.right + self.layout.cellSize.x + self.layout.spacing.x
189 local y = self.layout.padding.top + self.layout.padding.bottom + self.totalCount * (self.layout.cellSize.y + self.layout.spacing.y) - self.layout.spacing.y
190 self.content.sizeDelta = CS.UnityEngine.Vector2(x, y)
191 end
192end
193
194function MyScrollRectView:_GetFirstItemAnchoredPos()
195 self.firstItemAnchoredPos = CS.UnityEngine.Vector2(self.layout.padding.left + self.layout.cellSize.x / 2, -self.layout.padding.top - self.layout.cellSize.y / 2)
196end
197
198--- #外部调用
199
200function MyScrollRectView:SetDataLength(length)
201 --- 设置总数据量
202 self.totalCount = length
203 self:Init()
204end
205
206function MyScrollRectView:Init()
207 if self.totalCount <= 0 then
208 return
209 elseif self.totalCount < 10 then
210 self.fixedCount = self.totalCount
211 else
212 self.fixedCount = 10
213 end
214
215 if self.scrollRect["onValueChanged"] ~= nil then
216 self.scrollRect["onValueChanged"]:RemoveAllListeners()
217 end
218
219 self.commonService:AddEventListener(self.scrollRect, "onValueChanged", function(vector2)
220 self:_OnScroll(vector2)
221 end)
222
223 --设置布局
224 self:_SetLayout()
225
226 --设置头下标和尾下标
227 self.headIndex = 0
228 self.tailIndex = self.fixedCount - 1
229
230 -- 设置Content大小
231 self:_SetContentSize()
232
233 --实例化Item
234 self:_InitItem()
235
236 --得到第一个Item的锚点位置
237 self:_GetFirstItemAnchoredPos()
238
239 --- Content位置复位
240 self.content.anchoredPosition = CS.UnityEngine.Vector2(self.content.anchoredPosition.x, 0)
241end
242
243function MyScrollRectView:DestoryAll()
244 -- print("执行DestoryAll, #self.dataList = ", #self.dataList)
245 for i = #self.dataList, 0, -1 do
246 if self.dataList[i] then
247 -- print("销毁: ", self.dataList[i].gameObject)
248 CS.UnityEngine.GameObject.Destroy(self.dataList[i].gameObject)
249 end
250 end
251 self.dataList = {}
252end
253
254--- #外部调用
255---
256--- 关闭的时销毁 GameObject
257function MyScrollRectView:Close()
258 self:DestoryAll()
259end
260
261--- 属于业务相关方法
262---@param i number index
263---@param trans CS.UnityEngine.RectTransform
264function MyScrollRectView:_SetShow(trans, i)
265 --- TODO Item如何展示 example code
266 trans.name = i .. ""
267 ---@type CS.UnityEngine.UI.RawImage
268 local raw = trans.gameObject:GetComponent("RawImage")
269 raw.color = CS.UnityEngine.Color(1, 1, 1, 0)
270end
271
272return MyScrollRectView
273
所以核心思想就是根据划进划出把头部的 GameObject 添加到尾部或者把尾部 GameObject 添加到头部!
数据量小/固定时的简单复用
如果数据量很小而且Item数量比较固定的时候直接简单复用即可:
1---列表item复用
2items = {}
3
4---show function
5for i = 1, #dataList do
6 ---@type CS.UnityEngine.GameObject
7 local itemRootUI = nil
8
9 if items[i] then
10 itemRootUI = items[i]
11 else
12 itemRootUI = GameObject.Instantiate(itemPrefab, contentRectTransform, false)
13 table.insert(items, itemRootUI)
14 end
15
16 itemRootUI:SetActive(true)
17 --- TODO show...
18end