EGUI详解-LoopScrollRect解析
前言
在毕设demo中想要实现一个角色选择界面,其中是需要用到类似背包的GridLayoutGroup组件的,同时之前的EGUI文章中也没对其中的LoopHorizontalScrollRect和LoopVerticalScrollRect进行解析,所以也趁这个机会看一下源码,且其对于背包系统的优化来说对很多类型的游戏都是通用的。
整体设计
对于抽象类LoopScrollRect来说,其基于Unity官方的ScrollView进行了一定的拓展。由LoopScrollRect继承出两种滚动类型,分别为HorizontalScrollRect和VerticalScrollRect。
本文主要针对于其的拓展与采取的优化手段进行解析:
拓展部分的UML图(包括Item的数据存储):
数据存储
对于数据的存储主要是通过LoopScrollPrefabSource和LoopScrollDataSource,前者主要是对UIPrefab源,后者是对其具体存储数据源
LoopScrollPrefabSource
- 内部存储了对应UIPrefab的prefabName,通过AB包的方式加载,并生成对应的对象池Pool,其Pool的GameObject存放在Global/PoolRoot中。
- 具体封装了GetObject()和ReturnObject(Transform go,bool isDestroy = false)
LoopScrollDataSource
- scrollMoveEvent存储滑动时对应Index的Item及其Transform的Action,在LoopScrollRect的GetFromTempPool(int itemIdx)中会对池子获取到的nextItem进行data注入(即执行dataSource.ProvideData(nextItem,itemIdx))
- 具有LoopScrollSendIndexSource和LoopScrollArraySource<T>两种继承类,两者都在ProvideData接口重写中调用scrollMoveEvent,而后者在初始化的构造函数中需要传入objectToFill作为传入参数(可供后期在ProvideData执行时拓展对objectsToFill的统一操作)。
滚动优化
UpdateItems
对于Content来说,其内部的Item是动态进行对象池的获取和回收的。
UpdateItems在其父类LoopScrollRect中的UpdateBounds(bool updateItems = false)中调用
而UpdateBounds在updateItems=true下的具体调用则分为以下几种情况:- SetContentAnchoredPosition(Vector2 position),即设定Content窗口的AnchoredPosition,其具体在OnScroll(PointerEventData data)、OnDrag(PointerEventData data)、LateUpdate()中进行调用
- SetNormalizedPosition(float value,int axis),即设定Content窗口的规格化NormalizedPosition,其具体在SetHorizontalNormalizedPosition(float value)、SetVerticalNormalizedPosition(float value)中进行调用
1
2
3
4
5
6
7
8// ============LoopScrollRect============
// Don't do this in Rebuild
if (Application.isPlaying && updateItems && UpdateItems(m_ViewBounds,m_ContentBounds))
{
Canvas.ForceUpdateCanvases();
m_ContentBounds = GetBounds();
}
// ============LoopScrollRect============UpdateItems具体的实现主要分为五种情况(以LoopVerticalScrollRect为例子,Horizontal类似)
在一帧内滑动超过一页,判断条件为(viewBounds.min.y > contentBounds.max.y && itemTypeEnd > itemTypeStart)
对应源码为:
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
26if (viewBounds.min.y > contentBounds.max.y && itemTypeEnd > itemTypeStart)
{
float currentSize = contentBounds.size.y;
float elementSize = (currentSize - contentSpacing * (CurrentLines - 1)) / CurrentLines;
ReturnToTempPool(false, itemTypeEnd - itemTypeStart);
itemTypeEnd = itemTypeStart;
int offsetCount = Mathf.FloorToInt((viewBounds.min.y - contentBounds.max.y) / (elementSize + contentSpacing));
if (totalCount >= 0 && itemTypeStart - offsetCount * contentConstraintCount < 0)
{
offsetCount = Mathf.FloorToInt((float)(itemTypeStart) / contentConstraintCount);
}
itemTypeStart -= offsetCount * contentConstraintCount;
if (totalCount >= 0)
{
itemTypeStart = Mathf.Max(itemTypeStart, 0);
}
itemTypeEnd = itemTypeStart;
float offset = offsetCount * (elementSize + contentSpacing);
content.anchoredPosition += new Vector2(0, offset + (reverseDirection ? currentSize : 0));
contentBounds.center += new Vector3(0, offset + currentSize / 2, 0);
contentBounds.size = Vector3.zero;
changed = true;
}- 计算出elementSize,即每一行Item的size.y大小
- 将当前的所有Item都放进待删除的脏池子中(翻页情况)
- 计算出offsetCount,即滑动偏移的行数,并与实际最多可偏移行数比较得出最终offsetCount
- 计算出最终需要偏移的offset,同时将content的anchoredPosition向上方移动offset,设置其包围盒的中心点与anchoredPosition重合,并重置包围盒大小
显示包围盒的y的最小值大于内容包围盒的y的最小值+Item判定大小,判断条件为(viewBounds.min.y>contentBounds.min.y+threshold),threshold为item的boundSize的1.5倍,用一张图来方便观察。
红色为view,蓝色为content
进行代码分析:1
2
3
4
5
6
7
8
9
10
11if (viewBounds.min.y > contentBounds.min.y + threshold)
{
float size = DeleteItemAtEnd(), totalSize = size;
while (size > 0 && viewBounds.min.y > contentBounds.min.y + threshold + totalSize)
{
size = DeleteItemAtEnd();
totalSize += size;
}
if (totalSize > 0)
changed = true;
}- 删除末尾的Item并计算得出删除的size,对totalSize进行赋值
- 判断删除后边界大小是否满足viewBounds.min.y+threshold+totalSize>viewBounds.min.y情况,若不满足则继续删除末尾Item并累计删除的size
- 如果删除的totalSize>0,则将changed设为true,在函数的末尾进行脏数据的删除和清空
显示包围盒的y最大值小于内容包围盒的y最大值-Item判定大小,判断条件为(viewBounds.max.y < contentBounds.max.y - threshold)
红色为viewBounds,蓝色为contentBounds进行代码分析:
1
2
3
4
5
6
7
8
9
10
11if (viewBounds.max.y < contentBounds.max.y - threshold)
{
float size = DeleteItemAtStart(), totalSize = size;
while (size > 0 && viewBounds.max.y < contentBounds.max.y - threshold - totalSize)
{
size = DeleteItemAtStart();
totalSize += size;
}
if (totalSize > 0)
changed = true;
}- 删除开头的Item并得出删除Item的size,用totalSize进行存储
- 判断是否符合viewBounds.max.y>contentBounds.max.y-threshold-totalSize,若不满足则继续删除开头的Item
- 如果删除的totalSize>0,则将changed设为true,在函数的末尾进行脏数据的删除和清空
显示包围盒的y最小值小于内容包围盒的y最小值,判断条件为(viewBounds.min.y<contentBounds.min.y)
红色为viewBounds,蓝色为contentBounds
代码分析:1
2
3
4
5
6
7
8
9
10
11if (viewBounds.min.y < contentBounds.min.y)
{
float size = NewItemAtEnd(), totalSize = size;
while (size > 0 && viewBounds.min.y < contentBounds.min.y - totalSize)
{
size = NewItemAtEnd();
totalSize += size;
}
if (totalSize > 0)
changed = true;
}- 在末尾添加Item并记录下添加的Item大小,用totalSize进行存储
- 判断是否符合viewBounds.min.y>contentBounds.min.y-totalSize,若不满足则继续在末尾增加Item
- 如果删除的totalSize>0,则将changed设为true,在函数的末尾进行脏数据的删除和清空
显示包围盒的y最大值大于内容包围盒的y最大值,判断条件为(viewBounds.max.y>contentBounds.max.y)
红色为viewBounds,蓝色为contentBounds代码分析:
1
2
3
4
5
6
7
8
9
10
11if (viewBounds.max.y > contentBounds.max.y)
{
float size = NewItemAtStart(), totalSize = size;
while (size > 0 && viewBounds.max.y > contentBounds.max.y + totalSize)
{
size = NewItemAtStart();
totalSize += size;
}
if (totalSize > 0)
changed = true;
}- 在开头添加Item并记录下添加的Item大小,用totalSize进行存储
- 判断是否符合viewBounds.max.y<contentBounds.max.y+totalSize,若不满足则继续在开头增加Item
- 如果删除的totalSize>0,则将changed设为true,在函数的末尾进行脏数据的删除和清空
New/Delete ItemAt Start/End
上述的UpdateItems中,在对Item进行首尾的增删时用到了标题中的函数,我们以NewItemAtStart()与DeleteItemAtStart()为例子对函数的源码进行分析:
NewItemAtStart()
1 | protected float NewItemAtStart() |
- 对于itemTypeStart的增删是逐行/列进行的,每次增加会减少itemTypeStart(首部的Itemindex记录,用于获取信息)
- 实际添加的位置会根据deletedItemTypeStart调整(待删除Item的起始点,其前面所有Item都会在该帧ClearTempPool()时被删除)
- 在首部增加Item时需要对content的anchoredPosition进行上移,在后续GetBounds()时获取到新的包围盒
DeleteItemAtStart()
1 | protected float DeleteItemAtStart() |
- 在ReturnToTempPool(true)内部会执行deletedItemTypeStart+=1,即将待删除结点的起点向后移一位
- 在首部删除Item时需要对content的anchoredPosition进行下移,在后续GetBounds()时获取到新的包围盒
总体结构
滑动效果展示
主要是增删Item对Content的动态anchoredPosition修改展示: