简单的UI框架,方便以后复制

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

namespace IL
{

public enum UILevel
{
Bottom,//底层
Common, //普通层
Top,//最前层
}


public class UIMgr : ASingleton<UIMgr>
{



private Dictionary<string, PanelView> mAllPanel = new Dictionary<string, PanelView>();

private Transform mBottomTrans;
private Transform mCommonTrans;
private Transform mTopTrans;

private Canvas mCanvas;
private CanvasScaler mCanvasScaler;
private GraphicRaycaster mGraphicRaycaster;
private RectTransform mRectTransform;

private EventSystem mEventSystem;

private Transform UIRoot;


public void Init()
{
UIRoot = GameObject.Instantiate(ResMgr.Ins.Load<GameObject>(AssetBundleName.UI,"uiroot")).transform;
GameObject.DontDestroyOnLoad(UIRoot);

mBottomTrans = UIRoot.Find("Bottom");
mCommonTrans = UIRoot.Find("Common");
mTopTrans = UIRoot.Find("Top");

mCanvas = UIRoot.GetComponent<Canvas>();
mCanvasScaler = UIRoot.GetComponent<CanvasScaler>();
mGraphicRaycaster = UIRoot.GetComponent<GraphicRaycaster>();
mRectTransform = UIRoot.GetComponent<RectTransform>();

mEventSystem = UIRoot.Find("EventSystem").GetComponent<EventSystem>();

}

public RectTransform RectTransform
{
get { return mRectTransform; }
}

public GraphicRaycaster GraphicRaycaster
{
get { return mGraphicRaycaster; }
}

public Canvas RootCanvas
{
get { return mCanvas; }
}
/// <summary>
/// 设置分辨率
/// </summary>
/// <param name="width"></param>
/// <param name="height"></param>
public void SetResolution(int width, int height)
{
mCanvasScaler.referenceResolution = new Vector2(width, height);
}


private T OpenUI<T>(string PanelName, UILevel uiLevel) where T:PanelView
{
if (!mAllPanel.ContainsKey(PanelName))
{
CreateUI(typeof(T), PanelName, uiLevel);
}
mAllPanel[PanelName].SetActive(true);
return mAllPanel[PanelName]as T;

}

private void OpenUIAsync<T>(string PanelName, Action<UnityEngine.Object> onLoaded = null) where T : PanelView, new()
{
if (!mAllPanel.ContainsKey(PanelName))
{
CreateUIAsync<T>(PanelName,onLoaded);
}
onLoaded(null);
// return mAllPanel[PanelName];

}

private void CreateUIAsync<T>(string panelName, Action<UnityEngine.Object> onLoaded) where T : PanelView, new()
{
ResMgr.Ins.LoadAsync(AssetBundleName.UI, panelName,onLoaded);
}

private void CreateUI(Type type, string panelName, UILevel uiLevel)
{
PanelView panelView = Activator.CreateInstance(type) as PanelView;

Transform parent;

switch (uiLevel)
{
case UILevel.Bottom:
parent = mBottomTrans;
break;
case UILevel.Common:
parent = mCommonTrans;
break;
case UILevel.Top:
parent = mTopTrans;
break;
default:
parent = mCommonTrans;
break;
}
GameObject ui = GameObject.Instantiate(ResMgr.Ins.Load<GameObject>(AssetBundleName.UI, panelName), parent);
ui.name = panelName;
panelView.SetGameObject(ui);
panelView.Init();
mAllPanel.Add(panelName, panelView);

}

public ViewType Open<ViewType>(UILevel uiLevel = UILevel.Common) where ViewType : PanelView
{
return OpenUI<ViewType>( GetName<ViewType>(), uiLevel);
}

public void OpenAsync<T>( Action<UnityEngine.Object> onLoaded = null)
{
OpenUIAsync<PanelView>(GetName<T>(), onLoaded);
}

private string GetName<T>()
{
if (Runtime.Ins.IsHotResProject)
{
string name = typeof(T).ToString();
return name.Replace("Type : ", "");
}
else
{
string name = typeof(T).ToString();
string[] nameSplits = name.Split('.');
return nameSplits[nameSplits.Length - 1];
}


//return name;
}
}
}

  • 颜色向量(Color Vector):一个通过红绿蓝(RGB)分量的组合描绘大部分真实颜色的向量。一个物体的颜色实际上是该物体所不能吸收的反射颜色分量。
  • 冯氏光照模型(Phong Lighting Model):一个通过计算环境光,漫反射,和镜面光分量的值来估计真实光照的模型。
  • 环境光照(Ambient Lighting):通过给每个没有被光照的物体很小的亮度,使其不是完全黑暗的,从而对全局光照进行估计。
  • 漫反射着色(Diffuse Shading):一个顶点/片段与光线方向越接近,光照会越强。使用了法向量来计算角度。
  • 法向量(Normal Vector):一个垂直于平面的单位向量。
  • 法线矩阵(Normal Matrix):一个3x3矩阵,或者说是没有平移的模型(或者模型-观察)矩阵。它也被以某种方式修改(逆转置),从而在应用非统一缩放时,保持法向量朝向正确的方向。否则法向量会在使用非统一缩放时被扭曲。
  • 镜面光照(Specular Lighting):当观察者视线靠近光源在表面的反射线时会显示的镜面高光。镜面光照是由观察者的方向,光源的方向和设定高光分散量的反光度值三个量共同决定的。
  • 冯氏着色(Phong Shading):冯氏光照模型应用在片段着色器。
  • Gouraud着色(Gouraud shading):冯氏光照模型应用在顶点着色器上。在使用很少数量的顶点时会产生明显的瑕疵。会得到效率提升但是损失了视觉质量。
  • GLSL结构体(GLSL struct):一个类似于C的结构体,用作着色器变量的容器。大部分时间用来管理输入/输出/uniform。
  • 材质(Material):一个物体反射的环境光,漫反射,镜面光颜色。这些东西设定了物体所拥有的颜色。
  • 光照属性(Light(properties)):一个光的环境光,漫反射,镜面光的强度。可以使用任何颜色值,对每一个冯氏分量(Phong Component)定义光源发出的颜色/强度。
  • 漫反射贴图(Diffuse Map):一个设定了每个片段中漫反射颜色的纹理图片。
  • 镜面光贴图(Specular Map):一个设定了每一个片段的镜面光强度/颜色的纹理贴图。仅在物体的特定区域显示镜面高光。
  • 定向光(Directional Light):只有一个方向的光源。它被建模为不管距离有多长所有光束都是平行而且其方向向量在整个场景中保持不变。
  • 点光源(Point Light):一个在场景中有位置的,光线逐渐衰减的光源。
  • 衰减(Attenuation):光随着距离减少强度的过程,通常使用在点光源和聚光下。
  • 聚光(Spotlight):一个被定义为在某一个方向上的锥形的光源。
  • 手电筒(Flashlight):一个摆放在观察者视角的聚光。
  • GLSL uniform数组(GLSL Uniform Array):一个uniform值数组。它的工作原理和C语言数组大致一样,只是不能动态分配内存。

  • OpenGL: 一个定义了函数布局和输出的图形API的正式规范。
  • GLAD: 一个拓展加载库,用来为我们加载并设定所有OpenGL函数指针,从而让我们能够使用所有(现代)OpenGL函数。
  • 视口(Viewport): 我们需要渲染的窗口。
  • 图形管线(Graphics Pipeline): 一个顶点在呈现为像素之前经过的全部过程。
  • 着色器(Shader): 一个运行在显卡上的小型程序。很多阶段的图形管道都可以使用自定义的着色器来代替原有的功能。
  • 标准化设备坐标(Normalized Device Coordinates, NDC): 顶点在通过在剪裁坐标系中剪裁与透视除法后最终呈现在的坐标系。所有位置在NDC下-1.0到1.0的顶点将不会被丢弃并且可见。
  • 顶点缓冲对象(Vertex Buffer Object): 一个调用显存并存储所有顶点数据供显卡使用的缓冲对象。
  • 顶点数组对象(Vertex Array Object): 存储缓冲区和顶点属性状态。
  • 索引缓冲对象(Element Buffer Object): 一个存储索引供索引化绘制使用的缓冲对象。
  • Uniform: 一个特殊类型的GLSL变量。它是全局的(在一个着色器程序中每一个着色器都能够访问uniform变量),并且只需要被设定一次。
  • 纹理(Texture): 一种包裹着物体的特殊类型图像,给物体精细的视觉效果。
  • 纹理缠绕(Texture Wrapping): 定义了一种当纹理顶点超出范围(0, 1)时指定OpenGL如何采样纹理的模式。
  • 纹理过滤(Texture Filtering): 定义了一种当有多种纹素选择时指定OpenGL如何采样纹理的模式。这通常在纹理被放大情况下发生。
  • 多级渐远纹理(Mipmaps): 被存储的材质的一些缩小版本,根据距观察者的距离会使用材质的合适大小。
  • stb_image.h: 图像加载库。
  • 纹理单元(Texture Units): 通过绑定纹理到不同纹理单元从而允许多个纹理在同一对象上渲染。
  • 向量(Vector): 一个定义了在空间中方向和/或位置的数学实体。
  • 矩阵(Matrix): 一个矩形阵列的数学表达式。
  • GLM: 一个为OpenGL打造的数学库。
  • 局部空间(Local Space): 一个物体的初始空间。所有的坐标都是相对于物体的原点的。
  • 世界空间(World Space): 所有的坐标都相对于全局原点。
  • 观察空间(View Space): 所有的坐标都是从摄像机的视角观察的。
  • 裁剪空间(Clip Space): 所有的坐标都是从摄像机视角观察的,但是该空间应用了投影。这个空间应该是一个顶点坐标最终的空间,作为顶点着色器的输出。OpenGL负责处理剩下的事情(裁剪/透视除法)。
  • 屏幕空间(Screen Space): 所有的坐标都由屏幕视角来观察。坐标的范围是从0到屏幕的宽/高。
  • LookAt矩阵: 一种特殊类型的观察矩阵,它创建了一个坐标系,其中所有坐标都根据从一个位置正在观察目标的用户旋转或者平移。
  • 欧拉角(Euler Angles): 被定义为偏航角(Yaw),俯仰角(Pitch),和滚转角(Roll)从而允许我们通过这三个值构造任何3D方向。

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

namespace GameKit
{
public class Joystick : MonoBehaviour
{

[Header("摇杆最大半径(UGUI)")]
public float maxRadius = 0;
[Header("摇杆最小半径(UGUI)")]
public float minRadius = 0;
[Header("摇杆框")]
public Transform stickBorder;
[Header("摇杆")]
public Transform stick;

/// <summary>
/// 绑定的相机
/// </summary>
[HideInInspector]
public Camera camera;

/// <summary>
/// 触摸的起始位置
/// </summary>
Vector2 _touchStartPos;

/// <summary>
/// 摇杆起始位置
/// </summary>
Vector3 _stickBorderInitPos;

/// <summary>
/// 当Stick的值改变时触发
/// </summary>
public event Action<Vector2> onValueChange;

List<KeyCode> _pressedKeyCode = new List<KeyCode>();

Vector2 _lastValue;

bool _isStickMode = false;

CanvasGroup stickBorderGroup;

void Start()
{
_stickBorderInitPos = stickBorder.position;
stickBorderGroup = stickBorder.gameObject.GetComponent<CanvasGroup>();
}


private void FixedUpdate()
{
if (_isStickMode == false)
{
CheckKeyPress(KeyCode.UpArrow);
CheckKeyPress(KeyCode.DownArrow);
CheckKeyPress(KeyCode.LeftArrow);
CheckKeyPress(KeyCode.RightArrow);

CheckKeyRelease(KeyCode.UpArrow);
CheckKeyRelease(KeyCode.DownArrow);
CheckKeyRelease(KeyCode.LeftArrow);
CheckKeyRelease(KeyCode.RightArrow);

Vector2 tempValue = Vector2.zero;
if (_pressedKeyCode.Count > 0)
{
switch (_pressedKeyCode[0])
{
case KeyCode.UpArrow:
tempValue = Vector2.up;
break;
case KeyCode.DownArrow:
tempValue = Vector2.down;
break;
case KeyCode.LeftArrow:
tempValue = Vector2.left;
break;
case KeyCode.RightArrow:
tempValue = Vector2.right;
break;
}
}

SetValue(tempValue);
}
}

void SetValue(Vector2 value)
{
if (_lastValue != value)
{
_lastValue = value;
onValueChange?.Invoke(_lastValue);
}
}

void CheckKeyPress(KeyCode keyCode)
{
if (Input.GetKeyDown(keyCode))
{
_pressedKeyCode.Remove(keyCode);
_pressedKeyCode.Insert(0, keyCode);
}
}

void CheckKeyRelease(KeyCode keyCode)
{
if (!Input.GetKey(keyCode))
{
_pressedKeyCode.Remove(keyCode);
}
}

/// <summary>
/// 得到指定GameObject下,鼠标相对的localposition坐标
/// </summary>
/// <param name="go"></param>
/// <returns></returns>
Vector2 GetLocalMousePosition(GameObject go)
{
if(null == camera)
{
Debug.LogError("Joystick need binding a camera");
}

Vector2 screenMouse = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
Vector2 localPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(go.GetComponent<RectTransform>(), screenMouse, camera, out localPoint);

//Debug.LogFormat("Mouse:{0} Screen:{1} LocalPoint:{2}", Input.mousePosition, screenMouse, localPoint);
return localPoint;
}

/// <summary>
/// 触摸开始的时候
/// </summary>
/// <param name="e"></param>
public void OnPointerDown(BaseEventData e)
{
stickBorder.localPosition = GetLocalMousePosition(gameObject);

stickBorderGroup.alpha = 0.4f;
}

/// <summary>
/// 滑动开始的时候
/// </summary>
/// <param name="e"></param>
public void OnBeginDrag(BaseEventData e)
{
_isStickMode = true;

_touchStartPos = GetLocalMousePosition(stickBorder.gameObject);
}

/// <summary>
/// 滑动中
/// </summary>
/// <param name="e"></param>
public void OnDrag(BaseEventData e)
{
Vector2 touchNowPos = GetLocalMousePosition(stickBorder.gameObject);

var moveVector = (touchNowPos - _touchStartPos);

if (moveVector.magnitude <= minRadius)
{
return;
}

moveVector = Vector3.ClampMagnitude(moveVector, maxRadius);

stick.localPosition = moveVector;

Vector2 value = new Vector2(moveVector.x, moveVector.y);

SetValue(value);
}

/// <summary>
/// 滑动结束的时候
/// </summary>
/// <param name="e"></param>
public void OnEndDrag(BaseEventData e)
{
stick.localPosition = Vector3.zero;
onValueChange?.Invoke(Vector2.zero);
_isStickMode = false;

ResetStickBorder();
}

void ResetStickBorder()
{
stickBorderGroup.alpha = 0.0f;
stickBorder.position = _stickBorderInitPos;
}
}
}

  1. 《C++必知必会》

渲染流水线

坐标转换依次顺序

Object space 模型空间

World space 世界坐标系空间

Eye space 观察坐标系空间

Clip and Project space 屏幕坐标空间

注意:

  1. 光照计算通常在World coordinate space(世界坐标空间)里计算,也可以在Eye space 里计算。
  2. 顶点法向量属于Object space ,转化为World space时,要通过(world matrix)转置矩阵的逆矩阵来转换 (复习线性代数去了)

Eye Space

以Camera为原点 ,由视线方向、视角和远近平面共同组成一个梯形三维空间,称之为viewing frustum(视锥),超出部分会被裁剪 frustum culling(视锥裁剪)

Project and clip space

因为在不规则的体(viewing frustum)中进行裁剪并非易事,所以应该是先投影再裁剪具体分为三个步骤:

  1. 用透视变换矩阵把顶点从视锥体中变换到裁剪空间的CVV中;
  2. 在CVV进行图元裁剪;
  3. 屏幕映射:将进过前述过程得到的坐标映射到屏幕坐标系上。
  • 在第一个步骤里的过程为“投影”,主要投影方法有两种:正交投影和透视投影。
  • 只有图元完全或部分存在于视锥内部时才需要光栅化。超出部分进行裁剪。
  • 视点去除可以不用在GPU中进行,可以使用高级语言在CPU上实现,提前可减去GPU负担。

Primitive Assemble&&Triangle setup

  • Primitive Assembly,图元装配,即将顶点根据Primitive(原始的连接关系),还原出网格结构。
  • 涉及到三角形的顶点顺序(三角形的法向量朝向)根据右手来决定三角面片的法向量(逆时针排列),法向量朝向视点为正,如果为反面进行背面去除操作(Back-face-Culling)。
  • 所有的裁剪剔除都是为了减少需要绘制的顶点个数。
  • 裁剪算法主要包括:视域剔除(View Frustum Culing)、背面剔除(Back-Face Culling)、遮挡剔除(Occlusing Culling)和视口裁剪等。

光栅化

目前我们拿到了每个点的屏幕坐标值(Screen coordinate),也知道我们需要绘制的图元(点、线、面)但是有两个问题:

  1. 点的屏幕坐标都是浮点数,像素都是由整数表示。(绘制的位置为接近两指定端点的实际线段位置如(10.48,20.51)转化为(10,21)四舍五入 或 加0.5取整)。
  2. 在屏幕上需要绘制的有点、线、面,如何根据两个已经确定位置的2个像素点绘制一条线段,如何根据已经确定了位置的3个像素点绘制一个三角形面片。(区域填充推荐慕课课程大力点击进入

Pixel Operation

片元操作:计算出每个像素的颜色值,包括

  1. 被遮挡面通过一个被称为深度测试的过程而消除。

  2. Texture operatioin,纹理操作,根据像素的纹理坐标,查询对应的纹理值。

  3. Blending,混合,根据目前已经画好的颜色,与正在计算的颜色的透明度混合为两种颜色,作为新的颜色输出,通常称之为alpha混合技术。屏幕上的每个像素都关联一个RGB颜色值和一个Z缓冲器深度值,alpha值(可以根据需要生成并存储)。

    从渲染管线得到的RGBA,使用over操作符进行混合:

    a是透明度值(alpha)Ca表示透明物体的颜色,Cs表示混合前像素的颜色,Cd为最终计算得到的颜色。

    为了绘制透明物体,需要对物体进行排序,用z buffer 首先绘制不透明物体,然后从后往前混合透明物体。

  4. Filtering,将正在算的颜色经过某种Filtering(滤波)后输出,可以理解为:经过一种数学运算变成新的颜色值。(如最近邻滤波和线性滤波)

最终像素的颜色写入帧缓存,过程如下图

出现’permission denied: 的解决办法

1
libs chmod 777 path

查看端口占用情况命令

1
2
sudo lsof -i tcp:port
//如: sudo lsof -i tcp:8082

看到进程的PID,可以将进程杀死。

1
2
sudo kill -9 PID
//如:sudo kill -9 3210

苹果打包隐藏Home键

1
2
3
4
(UIRectEdge)preferredScreenEdgesDeferringSystemGestures
{
return UIRectEdgeAll;
}

实现原理

  • 基于队列实现
  • 根据当前Content视口的大小计算出视口里最多存在的Item
  • 根据位置不断更新当前显示的数据
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// 基于UGUI ScrollView组件的高性能列表组件。对性能有高要求的列表组件可以考虑使用该组件
/// </summary>
[RequireComponent(typeof(ScrollRect))]
public class ScrollViewList : MonoBehaviour
{
public enum ELayoutType
{
/// <summary>
/// 垂直列表
/// </summary>
VERTICAL,
/// <summary>
/// 横向列表
/// </summary>
HORIZONTAL,
}

/// <summary>
/// 布局方式
/// </summary>
public ELayoutType layoutType = ELayoutType.VERTICAL;

/// <summary>
/// 列表项的间隔
/// </summary>
public float itemGap = 0;

/// <summary>
/// 滚动组件
/// </summary>
ScrollRect _scrollRect;

/// <summary>
/// 存储位置
/// </summary>
RectTransform _content;

/// <summary>
/// 列表项的Prefab,默认Content第一个物体
/// </summary>
GameObject _itemPrefab;

/// <summary>
/// 列表项的大小
/// </summary>
Vector2 _itemSize;

/// <summary>
/// 生成的列表项示例列表
/// </summary>
List<GameObject> _items = new List<GameObject>();

/// <summary>
/// 列表视口
/// </summary>
Rect _viewport;

/// <summary>
/// 视口可见的最大列表项数量
/// </summary>
int _itemMaxCount;

/// <summary>
/// 列表的数据
/// </summary>
object[] _datas;

/// <summary>
/// 列表项更新的回调
/// </summary>
Action<int, object, GameObject> _onItemUpdate;

/// <summary>
/// 列表项占用的空间
/// </summary>
float _itemSpace = 0;

/// <summary>
/// Item 的缓存池
/// </summary>
Queue<GameObject> _itemPool = new Queue<GameObject>();

/// <summary>
/// 正在显示的列表项
/// </summary>
Dictionary<int, GameObject> _showedDic = new Dictionary<int, GameObject>();

private void Awake()
{
_scrollRect = GetComponent<ScrollRect>();
_viewport = _scrollRect.GetComponent<RectTransform>().rect;
_content = transform.Find("Viewport/Content").GetComponent<RectTransform>();
_itemPrefab = _content.GetChild(0).gameObject;
var rt = _itemPrefab.GetComponent<RectTransform>();
_itemSize = rt.sizeDelta;
var topLeft = new Vector2(0, 1);
rt.pivot = topLeft;
rt.anchorMin = topLeft;
rt.anchorMax = topLeft;
_itemPrefab.SetActive(false);
SetItemGap(itemGap);
}


#region 测试

//public void Update()
//{
// if (Input.GetKeyDown(KeyCode.A))
// {
// SetItemUpdata(OnItemUpDate);
// SetItemGap(10);
// List<string> ls = new List<string>();
// for (int i = 0; i < 1000000; i++)
// {
// ls.Add("current"+i);
// }
// SetData(ls.ToArray());
// }
//}
#endregion

/// <summary>
/// 设置列表项间隙
/// </summary>
/// <param name="gap"></param>
public void SetItemGap(float gap)
{
itemGap = gap;
SetData(_datas);
}



private void OnEnable()
{
_scrollRect.onValueChanged.AddListener(OnScroll);
}

public void SetData(object[] data)
{
Clear();

switch (layoutType)
{
case ELayoutType.VERTICAL:
_itemSpace = _itemSize.y + itemGap;
_itemMaxCount = Mathf.CeilToInt(_viewport.height / _itemSpace) + 1;
break;
case ELayoutType.HORIZONTAL:
_itemSpace = _itemSize.x + itemGap;
_itemMaxCount = Mathf.CeilToInt(_viewport.width / _itemSpace) + 1;
break;
}

_datas = data;
if (data != null)
{
_refreRate = 0.1f / data.Length;
}
Debug.LogError(_refreRate);

if (null == _datas || _datas.Length == 0)
{
return;
}
int count = _datas.Length;
SetContentSize((_itemSpace * count) - itemGap);
RefreshUI();
}

/// <summary>
/// 显示指定索引位置的列表项
/// </summary>
/// <param name="idx"></param>
public void ShowItem(int idx)
{
if(null == _datas)
{
return;
}
var pos = _itemSpace * idx;
SetContentPos(pos);
}

void SetContentSize(float size)
{
var contentSize = _content.sizeDelta;

switch (layoutType)
{
case ELayoutType.VERTICAL:
contentSize.y = size;
break;
case ELayoutType.HORIZONTAL:
contentSize.x = size;
break;
}

_content.sizeDelta = contentSize;
}

void SetContentPos(float pos)
{
var contentPos = _content.localPosition;

switch (layoutType)
{
case ELayoutType.VERTICAL:
contentPos.y = pos;
break;
case ELayoutType.HORIZONTAL:
contentPos.x = -1 * pos;
break;
}

_content.localPosition = contentPos;
}

public void Clear()
{
for(int i = 0; i < _items.Count; i++)
{
GameObject.Destroy(_items[i]);
}
_showedDic.Clear();
_items.Clear();
_itemPool.Clear();
_datas = null;
_scrollRect.velocity = Vector2.one;

SetContentPos(0);

switch (layoutType)
{
case ELayoutType.VERTICAL:
SetContentSize(_viewport.height);
break;
case ELayoutType.HORIZONTAL:
SetContentSize(_viewport.width);
break;
}
}

private void OnDisable()
{
_scrollRect.onValueChanged.RemoveListener(OnScroll);
}

/// <summary>
/// 当前位置
/// </summary>
private Vector2 _currentPos;

private float _refreRate;

private void OnScroll(Vector2 v)
{
if (CompareVector2(v, _currentPos ,_refreRate))
{
return;
}

_currentPos = v;
RefreshUI();
}

private bool CompareVector2(Vector2 v1,Vector2 v2,float dis)
{
return Mathf.Abs(v1.x - v2.x) < dis && Mathf.Abs(v1.y - v2.y) < dis;
}


HashSet<int> updateNeedlessSet = new HashSet<int>();
Dictionary<int, GameObject> showedDic = new Dictionary<int, GameObject>();

/// <summary>
/// 刷新界面
/// </summary>
private void RefreshUI()
{
if( _datas == null)
{
return;
}

float localPos;
if(layoutType == ELayoutType.VERTICAL)
{
localPos = _content.localPosition.y;
}
else
{
localPos = -1 * _content.localPosition.x ;
}

//显示开始的索引
int startIdx = Mathf.FloorToInt(localPos / _itemSpace);
if(startIdx < 0)
{
startIdx = 0;
}

//Item 显示开始的位置
Vector2 pos = new Vector2(0f, startIdx * _itemSpace);

int endIdx = startIdx + _itemMaxCount;
if (endIdx > _datas.Length)
{
endIdx = _datas.Length;
}

HashSet<int> updateNeedlessSet = new HashSet<int>();
Dictionary<int, GameObject> showedDic = new Dictionary<int, GameObject>();

//找出可以不用更新的Item
foreach (var entry in _showedDic)
{
if(entry.Key >= startIdx && entry.Key < endIdx)
{
//不用更新的
updateNeedlessSet.Add(entry.Key);
showedDic.Add(entry.Key, entry.Value);
}
else
{
//加入缓存池
_itemPool.Enqueue(entry.Value);
}
}

_showedDic = showedDic;

for (int i = startIdx; i < endIdx; i++)
{
if(updateNeedlessSet.Contains(i))
{
//已经显示的可以忽略不处理
continue;
}

if(layoutType == ELayoutType.VERTICAL)
{
pos.x = 0f;
pos.y = -1 * i * _itemSpace;
}
else
{
pos.x = i * _itemSpace;
pos.y = 0f;
}

CreateItem(i, pos, _datas[i], _itemPool);
}
}

private void OnItemUpDate(int num, object data, GameObject item)
{
item.transform.Find("Text").GetComponent<Text>().text = (string)data;
}

public void SetItemUpdata(Action<int ,object,GameObject> ItemUpdata)
{
_onItemUpdate = ItemUpdata;
}




public GameObject CreateItem(int idx, Vector2 pos, object data, Queue<GameObject> itemPool)
{
GameObject go = null;
if(itemPool.Count > 0)
{
go = itemPool.Dequeue();
}
else
{
go = GameObject.Instantiate(_itemPrefab, _content);
go.name = "item" + idx;
_items.Add(go);
}

_showedDic.Add(idx, go);
go.transform.localPosition = pos;
go.SetActive(true);
_onItemUpdate?.Invoke(idx, data, go);
return go;
}
}