0%

Unity3D UGUI 源码学习 Image

Image是UGUI中最常用的组件之一,用于在UI中显示一个图片(Sprite),继承自MaskableGraphic,覆写了GraphicMaskableGraphic的一些方法。同时实现了三个接口,ISerializationCallbackReceiver接收序列化和反序列化完成的回调,ILayoutElement作为自动布局元素,实现了计算自动布局尺寸的接口,ICanvasRaycastFilter实现了用于处理射线投射时过滤的方法,后边会一一详述。

1
public class Image : MaskableGraphic, ISerializationCallbackReceiver, ILayoutElement, ICanvasRaycastFilter

首先关注Image用于绘制和显示图像的逻辑。

用于显示图像:MaskableGraphic

纹理图片绘制到UI层级树上,需要网格、纹理、shader等要素,结合之前Graphic的章节可知,UI树上绘制元素可以覆盖虚方法UpdateGeometry()UpdateMaterial()来自行实现指定网格和材质的方法,以实现衍生组件类的绘制。所以此处也就讨论Geometry和
Material两部分:

Geometry

Image并没有直接覆UpdateGeometry()方法,而是覆写了生成网格的OnPopulateMesh(...)方法。对于一个长方形的纹理来说,其实需要两个三角形四个顶点即可完成绘制。满足这一简单需求的组件即是RawImage或者后边会提到的Image的simple类型。而对于Image,其中包含的是一个Sprite,可以提供更多更强大更复杂的功能,而不仅仅是显示矩形的纹理。

Image有四种类型,对应的是四种绘制的方式,在Image类一开始就定义了枚举类型:

1
2
3
4
5
6
7
public enum Type
{
Simple,
Sliced,
Tiled,
Filled
}

在使用Image时会选择其中一种来使用。所以生成网格时也会分四种类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected override void OnPopulateMesh(VertexHelper toFill)
{
if (activeSprite == null)
{
base.OnPopulateMesh(toFill);
return;
}

switch (type)
{
case Type.Simple:
GenerateSimpleSprite(toFill, m_PreserveAspect);
break;
case Type.Sliced:
GenerateSlicedSprite(toFill);
break;
case Type.Tiled:
GenerateTiledSprite(toFill);
break;
case Type.Filled:
GenerateFilledSprite(toFill, m_PreserveAspect);
break;
}
}

根据四种不同的类型,分别调用四种GenerateXxxSprite()

GenerateSimpleSprite

最简单的一种,也是Image默认的类型。Simple类型的Image示意图如下:

alt text

以下是GenerateSimpleSprite()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void GenerateSimpleSprite(VertexHelper vh, bool lPreserveAspect)
{
Vector4 v = GetDrawingDimensions(lPreserveAspect);
var uv = (activeSprite != null) ? Sprites.DataUtility.GetOuterUV(activeSprite) : Vector4.zero;

var color32 = color;
vh.Clear();
vh.AddVert(new Vector3(v.x, v.y), color32, new Vector2(uv.x, uv.y));
vh.AddVert(new Vector3(v.x, v.w), color32, new Vector2(uv.x, uv.w));
vh.AddVert(new Vector3(v.z, v.w), color32, new Vector2(uv.z, uv.w));
vh.AddVert(new Vector3(v.z, v.y), color32, new Vector2(uv.z, uv.y));

vh.AddTriangle(0, 1, 2);
vh.AddTriangle(2, 3, 0);
}

获取顶点坐标v和顶点纹理坐标uv,填充到vh中。获取顶点坐标时直接使用GetDrawingDimensions()方法,而在获取纹理UV时,会调用静态方法Sprites.DataUtility.GetOuterUV(),如果没有指定sprite则返回四个0。

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
/// Image's dimensions used for drawing. X = left, Y = bottom, Z = right, W = top.
private Vector4 GetDrawingDimensions(bool shouldPreserveAspect)
{
var padding = activeSprite == null ? Vector4.zero : Sprites.DataUtility.GetPadding(activeSprite);
var size = activeSprite == null ? Vector2.zero : new Vector2(activeSprite.rect.width, activeSprite.rect.height);

Rect r = GetPixelAdjustedRect();
// Debug.Log(string.Format("r:{2}, size:{0}, padding:{1}", size, padding, r));

int spriteW = Mathf.RoundToInt(size.x);
int spriteH = Mathf.RoundToInt(size.y);

var v = new Vector4(
padding.x / spriteW,
padding.y / spriteH,
(spriteW - padding.z) / spriteW,
(spriteH - padding.w) / spriteH);

if (shouldPreserveAspect && size.sqrMagnitude > 0.0f)
{
var spriteRatio = size.x / size.y;
var rectRatio = r.width / r.height;

if (spriteRatio > rectRatio)
{
var oldHeight = r.height;
r.height = r.width * (1.0f / spriteRatio);
r.y += (oldHeight - r.height) * rectTransform.pivot.y;
}
else
{
var oldWidth = r.width;
r.width = r.height * spriteRatio;
r.x += (oldWidth - r.width) * rectTransform.pivot.x;
}
}

v = new Vector4(
r.x + r.width * v.x,
r.y + r.height * v.y,
r.x + r.width * v.z,
r.y + r.height * v.w
);

return v;
}

GetDrawingDimensions()用来获取Image的顶点坐标。传入一个布尔参数表示是否要为图片保持原有的宽高比。得到一个Vector4返回值,Vector4中的四个值分别是左、下、右、上边界的值(即x min、y min、x max和y max)。

其中用到的SpriteactiveSprite,其定义如下,如有m_OverrideSprite则使用,否则使用自身的sprite

1
private Sprite activeSprite { get { return m_OverrideSprite != null ? m_OverrideSprite : sprite; } }

这里还涉及到一个工具类,Sprite.DataUtility, 用于获取Sprite的数据。它包含有四个静态方法,与前边遇到的GetPadding相似都需要传入一个Sprite实例来获取它的信息:

  • GetPadding: 获取padding,如果sprite的纹理来自图集,那么当其被打到图集里时可能四边会被裁掉一些透明区域,padding即表示上下左右四边被裁掉的区域的大小,单位为像素;
  • GetInnerUV: 获取内圈UV(纹理坐标);
  • GetMinSize: 获取最小宽高尺寸;
  • GetOuterUV: 获取外圈UV(纹理坐标);

这些信息来自图集和Sprite的设置,Unity仅仅是提供了这一组接口。关于padding以及后边会提到的Spriteborder,可以参考下边的图:

alt text

左侧是原始的png图片,周围白色的区域是透明底。右侧是裁剪后的图片,红色的线框是去除透明区域后的包络矩形,外边是所谓的padding。当使用此图片作为Sprite时,编辑器中显示的图片信息是这样的:

alt text

activeSprite.rect此方法返回一个Rect,其中包含Sprite原始纹理的坐标与尺寸。还用到了一个方法GetPixelAdjustedRect(),定义在抽象类Graphic中,用于获取像素修正过的Rect

1
2
3
4
5
6
7
public Rect GetPixelAdjustedRect()
{
if (!canvas || canvas.renderMode == RenderMode.WorldSpace || canvas.scaleFactor == 0.0f || !canvas.pixelPerfect)
return rectTransform.rect;
else
return RectTransformUtility.PixelAdjustRect(rectTransform, canvas);
}

除了上边这些东西之外,全都是最简单的坐标计算了。这里的加加减减目的是为了消除padding的影响,把纹理坐标还原到(对应到)顶点坐标上,size是纹理的宽高尺寸(包含padding),rRectTransform的尺寸。同时还处理了当shouldPreserveAspect时,根据宽度或高度中较小的值来调整另一个维度的尺寸,以确保绘制出来的RectTransformsize有相同的宽高比。

GenerateSlicedSprite

这个应该是UI中Image使用的最多的类型了,常说的九宫格图片,Slice类型的Image示意图如下:

alt text

以下是GenerateSlicedSprite()

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
static readonly Vector2[] s_VertScratch = new Vector2[4];
static readonly Vector2[] s_UVScratch = new Vector2[4];

private void GenerateSlicedSprite(VertexHelper toFill)
{
if (!hasBorder)
{
GenerateSimpleSprite(toFill, false);
return;
}

Vector4 outer, inner, padding, border;

if (activeSprite != null)
{
outer = Sprites.DataUtility.GetOuterUV(activeSprite);
inner = Sprites.DataUtility.GetInnerUV(activeSprite);
padding = Sprites.DataUtility.GetPadding(activeSprite);
border = activeSprite.border;
}
else
{
outer = Vector4.zero;
inner = Vector4.zero;
padding = Vector4.zero;
border = Vector4.zero;
}

Rect rect = GetPixelAdjustedRect();
Vector4 adjustedBorders = GetAdjustedBorders(border / pixelsPerUnit, rect);
padding = padding / pixelsPerUnit;

s_VertScratch[0] = new Vector2(padding.x, padding.y);
s_VertScratch[3] = new Vector2(rect.width - padding.z, rect.height - padding.w);

s_VertScratch[1].x = adjustedBorders.x;
s_VertScratch[1].y = adjustedBorders.y;

s_VertScratch[2].x = rect.width - adjustedBorders.z;
s_VertScratch[2].y = rect.height - adjustedBorders.w;

for (int i = 0; i < 4; ++i)
{
s_VertScratch[i].x += rect.x;
s_VertScratch[i].y += rect.y;
}

s_UVScratch[0] = new Vector2(outer.x, outer.y);
s_UVScratch[1] = new Vector2(inner.x, inner.y);
s_UVScratch[2] = new Vector2(inner.z, inner.w);
s_UVScratch[3] = new Vector2(outer.z, outer.w);

toFill.Clear();

for (int x = 0; x < 3; ++x)
{
int x2 = x + 1;

for (int y = 0; y < 3; ++y)
{
if (!m_FillCenter && x == 1 && y == 1)
continue;

int y2 = y + 1;

AddQuad(toFill,
new Vector2(s_VertScratch[x].x, s_VertScratch[y].y),
new Vector2(s_VertScratch[x2].x, s_VertScratch[y2].y),
color,
new Vector2(s_UVScratch[x].x, s_UVScratch[y].y),
new Vector2(s_UVScratch[x2].x, s_UVScratch[y2].y));
}
}
}

相比Simple类型的Sprite会稍稍复杂一些,用到了两个额外的Vector2数组来存储顶点坐标及纹理坐标。hasBorder的判断如下,其实是获取activeSprite.border并判断其是否有至少一个不为0:

1
2
3
4
5
6
7
8
9
10
11
12
public bool hasBorder
{
get
{
if (activeSprite != null)
{
Vector4 v = activeSprite.border;
return v.sqrMagnitude > 0f;
}
return false;
}
}

border四边都是0时,则九宫格图会退化到最简单的图直接使用GenerateSimpleSprite来绘制。

对于带有border的Slice型Image,最多会绘制9个Quad(九宫格名字应该就这么来的),所以后边的工作就是在计算这9个Quad所对应的顶点坐标和UV。内圈外圈的纹理坐标前边说到了Sprites.DataUtility可以取到,border尺寸Sprite直接就能获得,最后不得不考虑的还有padding。有了这四个Vector4就可以来计算9个Quad的顶点和UV了。

这里还有一点值得注意。像素与单位的换算。此换算基于SpritepixelsPerUnitCanvasreferencePixelsPerUnit属性,前者表示Sprite单位长度对应的像素数(像素密度),后者是一个参考值,表示默认的单位像素数。Image计算pixelsPerUnit时是将二者相除。当pixelsPerUnitreferencePixelsPerUnit相等时,Image会得到pixelsPerUnit为1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public float pixelsPerUnit
{
get
{
float spritePixelsPerUnit = 100;
if (activeSprite)
spritePixelsPerUnit = activeSprite.pixelsPerUnit;

float referencePixelsPerUnit = 100;
if (canvas)
referencePixelsPerUnit = canvas.referencePixelsPerUnit;

return spritePixelsPerUnit / referencePixelsPerUnit;
}
}

使用GetAdjustedBordersborder的特殊情况处理,即当Image尺寸小于两侧border之和的时候,会对对应轴向的border做一个整体的缩放:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Vector4 GetAdjustedBorders(Vector4 border, Rect rect)
{
for (int axis = 0; axis <= 1; axis++)
{
// If the rect is smaller than the combined borders, then there's not room for the borders at their normal size.
// In order to avoid artefacts with overlapping borders, we scale the borders down to fit.
float combinedBorders = border[axis] + border[axis + 2];
if (rect.size[axis] < combinedBorders && combinedBorders != 0)
{
float borderScaleRatio = rect.size[axis] / combinedBorders;
border[axis] *= borderScaleRatio;
border[axis + 2] *= borderScaleRatio;
}
}
return border;
}

接下来是重点部分了,填充s_VertScratchs_UVScratch,摘录之前的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
s_VertScratch[0] = new Vector2(padding.x, padding.y);
s_VertScratch[3] = new Vector2(rect.width - padding.z, rect.height - padding.w);

s_VertScratch[1].x = adjustedBorders.x;
s_VertScratch[1].y = adjustedBorders.y;

s_VertScratch[2].x = rect.width - adjustedBorders.z;
s_VertScratch[2].y = rect.height - adjustedBorders.w;

for (int i = 0; i < 4; ++i)
{
s_VertScratch[i].x += rect.x;
s_VertScratch[i].y += rect.y;
}

s_UVScratch[0] = new Vector2(outer.x, outer.y);
s_UVScratch[1] = new Vector2(inner.x, inner.y);
s_UVScratch[2] = new Vector2(inner.z, inner.w);
s_UVScratch[3] = new Vector2(outer.z, outer.w);

具体可参看下图,还是前边提到的那张图片,图上p0p3共四个点,分别对应的是填充后的s_VertScratchs_UVScratch下标0到3的四个点的位置信息。可以看出当图片尺寸变化时,rect的尺寸会随之变化,但border的值是不变的,s_VertScratch[0]s_VertScratch[1]不会变化,而s_VertScratch[2]s_VertScratch[3]会偏移变化,进而保证只有中间的区域被拉伸或缩小。

alt text

在两个数组里边保存好顶点坐标和纹理的UV之后,就开始绘制了,共分为9个四边形(Quad),使用两层遍历。Image里边又封装了一层AddQuad的方法,直接传入两点坐标即可向VertexHelper中添加两个三角形(一个四边形)。

1
2
3
4
5
6
7
8
9
10
11
12
static void AddQuad(VertexHelper vertexHelper, Vector2 posMin, Vector2 posMax, Color32 color, Vector2 uvMin, Vector2 uvMax)
{
int startIndex = vertexHelper.currentVertCount;

vertexHelper.AddVert(new Vector3(posMin.x, posMin.y, 0), color, new Vector2(uvMin.x, uvMin.y));
vertexHelper.AddVert(new Vector3(posMin.x, posMax.y, 0), color, new Vector2(uvMin.x, uvMax.y));
vertexHelper.AddVert(new Vector3(posMax.x, posMax.y, 0), color, new Vector2(uvMax.x, uvMax.y));
vertexHelper.AddVert(new Vector3(posMax.x, posMin.y, 0), color, new Vector2(uvMax.x, uvMin.y));

vertexHelper.AddTriangle(startIndex, startIndex + 1, startIndex + 2);
vertexHelper.AddTriangle(startIndex + 2, startIndex + 3, startIndex);
}

GenerateTiledSprite

另一种常用的Image类型是Tiled。当Image尺寸大于Sprite的尺寸时,会用平铺的方式填充。注意对于带有borderSprite,平铺时会保留border的部分,只将Spriteborder以外的纹理做平铺(border中除了四个角上的区域以外会沿着单个方向平铺)。Tiled类型的Image示意图如下:

alt text

以下是GenerateTiledSprite()

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
void GenerateTiledSprite(VertexHelper toFill)
{
Vector4 outer, inner, border;
Vector2 spriteSize;

if (activeSprite != null)
{
outer = Sprites.DataUtility.GetOuterUV(activeSprite);
inner = Sprites.DataUtility.GetInnerUV(activeSprite);
border = activeSprite.border;
spriteSize = activeSprite.rect.size;
}
else
{
outer = Vector4.zero;
inner = Vector4.zero;
border = Vector4.zero;
spriteSize = Vector2.one * 100;
}

Rect rect = GetPixelAdjustedRect();
float tileWidth = (spriteSize.x - border.x - border.z) / pixelsPerUnit;
float tileHeight = (spriteSize.y - border.y - border.w) / pixelsPerUnit;
border = GetAdjustedBorders(border / pixelsPerUnit, rect);

var uvMin = new Vector2(inner.x, inner.y);
var uvMax = new Vector2(inner.z, inner.w);

// Min to max max range for tiled region in coordinates relative to lower left corner.
float xMin = border.x;
float xMax = rect.width - border.z;
float yMin = border.y;
float yMax = rect.height - border.w;

toFill.Clear();
var clipped = uvMax;

// if either width is zero we cant tile so just assume it was the full width.
if (tileWidth <= 0)
tileWidth = xMax - xMin;

if (tileHeight <= 0)
tileHeight = yMax - yMin;

if (activeSprite != null && (hasBorder || activeSprite.packed || activeSprite.texture.wrapMode != TextureWrapMode.Repeat))
{
// Sprite has border, or is not in repeat mode, or cannot be repeated because of packing.
// We cannot use texture tiling so we will generate a mesh of quads to tile the texture.

// Evaluate how many vertices we will generate. Limit this number to something sane,
// especially since meshes can not have more than 65000 vertices.

int nTilesW = 0;
int nTilesH = 0;
if (m_FillCenter)
{
nTilesW = (int)Math.Ceiling((xMax - xMin) / tileWidth);
nTilesH = (int)Math.Ceiling((yMax - yMin) / tileHeight);

int nVertices = 0;
if (hasBorder)
{
nVertices = (nTilesW + 2) * (nTilesH + 2) * 4; // 4 vertices per tile
}
else
{
nVertices = nTilesW * nTilesH * 4; // 4 vertices per tile
}

if (nVertices > 65000)
{
Debug.LogError("Too many sprite tiles on Image \"" + name + "\". The tile size will be increased. To remove the limit on the number of tiles, convert the Sprite to an Advanced texture, remove the borders, clear the Packing tag and set the Wrap mode to Repeat.", this);

double maxTiles = 65000.0 / 4.0; // Max number of vertices is 65000; 4 vertices per tile.
double imageRatio;
if (hasBorder)
{
imageRatio = (nTilesW + 2.0) / (nTilesH + 2.0);
}
else
{
imageRatio = (double)nTilesW / nTilesH;
}

double targetTilesW = Math.Sqrt(maxTiles / imageRatio);
double targetTilesH = targetTilesW * imageRatio;
if (hasBorder)
{
targetTilesW -= 2;
targetTilesH -= 2;
}

nTilesW = (int)Math.Floor(targetTilesW);
nTilesH = (int)Math.Floor(targetTilesH);
tileWidth = (xMax - xMin) / nTilesW;
tileHeight = (yMax - yMin) / nTilesH;
}
}
else
{
if (hasBorder)
{
// Texture on the border is repeated only in one direction.
nTilesW = (int)Math.Ceiling((xMax - xMin) / tileWidth);
nTilesH = (int)Math.Ceiling((yMax - yMin) / tileHeight);
int nVertices = (nTilesH + nTilesW + 2 /*corners*/) * 2 /*sides*/ * 4 /*vertices per tile*/;
if (nVertices > 65000)
{
Debug.LogError("Too many sprite tiles on Image \"" + name + "\". The tile size will be increased. To remove the limit on the number of tiles, convert the Sprite to an Advanced texture, remove the borders, clear the Packing tag and set the Wrap mode to Repeat.", this);

double maxTiles = 65000.0 / 4.0; // Max number of vertices is 65000; 4 vertices per tile.
double imageRatio = (double)nTilesW / nTilesH;
double targetTilesW = (maxTiles - 4 /*corners*/) / (2 * (1.0 + imageRatio));
double targetTilesH = targetTilesW * imageRatio;

nTilesW = (int)Math.Floor(targetTilesW);
nTilesH = (int)Math.Floor(targetTilesH);
tileWidth = (xMax - xMin) / nTilesW;
tileHeight = (yMax - yMin) / nTilesH;
}
}
else
{
nTilesH = nTilesW = 0;
}
}

if (m_FillCenter)
{
// TODO: we could share vertices between quads. If vertex sharing is implemented. update the computation for the number of vertices accordingly.
for (int j = 0; j < nTilesH; j++)
{
float y1 = yMin + j * tileHeight;
float y2 = yMin + (j + 1) * tileHeight;
if (y2 > yMax)
{
clipped.y = uvMin.y + (uvMax.y - uvMin.y) * (yMax - y1) / (y2 - y1);
y2 = yMax;
}
clipped.x = uvMax.x;
for (int i = 0; i < nTilesW; i++)
{
float x1 = xMin + i * tileWidth;
float x2 = xMin + (i + 1) * tileWidth;
if (x2 > xMax)
{
clipped.x = uvMin.x + (uvMax.x - uvMin.x) * (xMax - x1) / (x2 - x1);
x2 = xMax;
}
AddQuad(toFill, new Vector2(x1, y1) + rect.position, new Vector2(x2, y2) + rect.position, color, uvMin, clipped);
}
}
}
if (hasBorder)
{
clipped = uvMax;
for (int j = 0; j < nTilesH; j++)
{
float y1 = yMin + j * tileHeight;
float y2 = yMin + (j + 1) * tileHeight;
if (y2 > yMax)
{
clipped.y = uvMin.y + (uvMax.y - uvMin.y) * (yMax - y1) / (y2 - y1);
y2 = yMax;
}
AddQuad(toFill,
new Vector2(0, y1) + rect.position,
new Vector2(xMin, y2) + rect.position,
color,
new Vector2(outer.x, uvMin.y),
new Vector2(uvMin.x, clipped.y));
AddQuad(toFill,
new Vector2(xMax, y1) + rect.position,
new Vector2(rect.width, y2) + rect.position,
color,
new Vector2(uvMax.x, uvMin.y),
new Vector2(outer.z, clipped.y));
}

// Bottom and top tiled border
clipped = uvMax;
for (int i = 0; i < nTilesW; i++)
{
float x1 = xMin + i * tileWidth;
float x2 = xMin + (i + 1) * tileWidth;
if (x2 > xMax)
{
clipped.x = uvMin.x + (uvMax.x - uvMin.x) * (xMax - x1) / (x2 - x1);
x2 = xMax;
}
AddQuad(toFill,
new Vector2(x1, 0) + rect.position,
new Vector2(x2, yMin) + rect.position,
color,
new Vector2(uvMin.x, outer.y),
new Vector2(clipped.x, uvMin.y));
AddQuad(toFill,
new Vector2(x1, yMax) + rect.position,
new Vector2(x2, rect.height) + rect.position,
color,
new Vector2(uvMin.x, uvMax.y),
new Vector2(clipped.x, outer.w));
}

// Corners
AddQuad(toFill,
new Vector2(0, 0) + rect.position,
new Vector2(xMin, yMin) + rect.position,
color,
new Vector2(outer.x, outer.y),
new Vector2(uvMin.x, uvMin.y));
AddQuad(toFill,
new Vector2(xMax, 0) + rect.position,
new Vector2(rect.width, yMin) + rect.position,
color,
new Vector2(uvMax.x, outer.y),
new Vector2(outer.z, uvMin.y));
AddQuad(toFill,
new Vector2(0, yMax) + rect.position,
new Vector2(xMin, rect.height) + rect.position,
color,
new Vector2(outer.x, uvMax.y),
new Vector2(uvMin.x, outer.w));
AddQuad(toFill,
new Vector2(xMax, yMax) + rect.position,
new Vector2(rect.width, rect.height) + rect.position,
color,
new Vector2(uvMax.x, uvMax.y),
new Vector2(outer.z, outer.w));
}
}
else
{
// Texture has no border, is in repeat mode and not packed. Use texture tiling.
Vector2 uvScale = new Vector2((xMax - xMin) / tileWidth, (yMax - yMin) / tileHeight);

if (m_FillCenter)
{
AddQuad(toFill, new Vector2(xMin, yMin) + rect.position, new Vector2(xMax, yMax) + rect.position, color, Vector2.Scale(uvMin, uvScale), Vector2.Scale(uvMax, uvScale));
}
}
}

首先也是计算了一系列的参数,xMinxMaxyMinyMax保存的是去除border以外的部分的待填充区域,uvMinuvMax也保存的是去除border之后的纹理坐标。在绘制时,分为两种情况:

  • Sprite有border、纹理不是Repeat模式、或者Sprite因为打合图等原因导致不能重复纹理来平铺;

  • 除了上边以外的情况,可以直接用纹理填充;

因此第二种情况会比较简单,直接绘制一个Quad,设置纹理坐标即可。比较复杂的是第一种情况,GenerateTiledSprite(...)中大部分代码是用于处理第一种情况的,绘制时需要分解为多个Quad,包括border部分及中心部分。各个Quad同样是需要计算顶点坐标和UV。

使用m_FillCenter来判断是否绘制中心区域,根据是否hasBorder来判断是否要处理border区域的逻辑。在计算各Quad的信息之前,首先确定参数tileWidthtileHeight,默认取Sprite中心区域宽高的像素数,如果是0则表示宽度或高度为整个Image中心区域的宽度或高度,接下来计算Quad数量(顶点的数量),如果顶点数量大于65000,则会输出错误,并强制修改tileWidthtileHeight,增大每个tile(Quad)的尺寸,从而减少tile的数量,进而减少顶点数,以确保顶点数目不会超过65000。

有了正确的tileWidthtileHeight之后,同样是依照m_FillCenterhasBorder来分别处理中心和边界区域的Quad。首先是中心区域:

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
if (m_FillCenter)
{
// TODO: we could share vertices between quads. If vertex sharing is implemented. update the computation for the number of vertices accordingly.
for (int j = 0; j < nTilesH; j++)
{
float y1 = yMin + j * tileHeight;
float y2 = yMin + (j + 1) * tileHeight;
if (y2 > yMax)
{
clipped.y = uvMin.y + (uvMax.y - uvMin.y) * (yMax - y1) / (y2 - y1);
y2 = yMax;
}
clipped.x = uvMax.x;
for (int i = 0; i < nTilesW; i++)
{
float x1 = xMin + i * tileWidth;
float x2 = xMin + (i + 1) * tileWidth;
if (x2 > xMax)
{
clipped.x = uvMin.x + (uvMax.x - uvMin.x) * (xMax - x1) / (x2 - x1);
x2 = xMax;
}
AddQuad(toFill, new Vector2(x1, y1) + rect.position, new Vector2(x2, y2) + rect.position, color, uvMin, clipped);
}
}
}

没有什么复杂逻辑,从下到上、从左到右填充,注意有一个变量clipped,记录的是最上一行及最右一列的位于右上角的纹理坐标。最上一行和最右一列的Quad可能不会是完整的Sprite的核心区域,因此需要绘制时需要与yMaxxMax作比较,并且记录纹理坐标clipped。接下来是边界部分:

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
if (hasBorder)
{
clipped = uvMax;
for (int j = 0; j < nTilesH; j++)
{
float y1 = yMin + j * tileHeight;
float y2 = yMin + (j + 1) * tileHeight;
if (y2 > yMax)
{
clipped.y = uvMin.y + (uvMax.y - uvMin.y) * (yMax - y1) / (y2 - y1);
y2 = yMax;
}
AddQuad(toFill,
new Vector2(0, y1) + rect.position,
new Vector2(xMin, y2) + rect.position,
color,
new Vector2(outer.x, uvMin.y),
new Vector2(uvMin.x, clipped.y));
AddQuad(toFill,
new Vector2(xMax, y1) + rect.position,
new Vector2(rect.width, y2) + rect.position,
color,
new Vector2(uvMax.x, uvMin.y),
new Vector2(outer.z, clipped.y));
}

// Bottom and top tiled border
clipped = uvMax;
for (int i = 0; i < nTilesW; i++)
{
float x1 = xMin + i * tileWidth;
float x2 = xMin + (i + 1) * tileWidth;
if (x2 > xMax)
{
clipped.x = uvMin.x + (uvMax.x - uvMin.x) * (xMax - x1) / (x2 - x1);
x2 = xMax;
}
AddQuad(toFill,
new Vector2(x1, 0) + rect.position,
new Vector2(x2, yMin) + rect.position,
color,
new Vector2(uvMin.x, outer.y),
new Vector2(clipped.x, uvMin.y));
AddQuad(toFill,
new Vector2(x1, yMax) + rect.position,
new Vector2(x2, rect.height) + rect.position,
color,
new Vector2(uvMin.x, uvMax.y),
new Vector2(clipped.x, outer.w));
}

// Corners
AddQuad(toFill,
new Vector2(0, 0) + rect.position,
new Vector2(xMin, yMin) + rect.position,
color,
new Vector2(outer.x, outer.y),
new Vector2(uvMin.x, uvMin.y));
AddQuad(toFill,
new Vector2(xMax, 0) + rect.position,
new Vector2(rect.width, yMin) + rect.position,
color,
new Vector2(uvMax.x, outer.y),
new Vector2(outer.z, uvMin.y));
AddQuad(toFill,
new Vector2(0, yMax) + rect.position,
new Vector2(xMin, rect.height) + rect.position,
color,
new Vector2(outer.x, uvMax.y),
new Vector2(uvMin.x, outer.w));
AddQuad(toFill,
new Vector2(xMax, yMax) + rect.position,
new Vector2(rect.width, rect.height) + rect.position,
color,
new Vector2(uvMax.x, uvMax.y),
new Vector2(outer.z, outer.w));
}

先绘制左右两边的边界,再绘制上下的边界,最后是四个角。每轮绘制之前会重置clippeduvMax,同样每列的最后一个和每行的最后一个需要与yMaxxMax比较,并修改clipped的值。最后是绘制四个角,会用到outer中保留的纹理坐标。

GenerateFilledSprite

Image的最后一种类型是filled,常用来做进度条等UI组件。这一类型的Image中又可分为多种填充方式,定义有枚举类型如下:

1
2
3
4
5
6
7
8
public enum FillMethod
{
Horizontal,
Vertical,
Radial90,
Radial180,
Radial360,
}

与指向对应的,各种填充类型又可以定义多种不同的填充起点:

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
public enum OriginHorizontal
{
Left,
Right,
}

public enum OriginVertical
{
Bottom,
Top,
}

public enum Origin90
{
BottomLeft,
TopLeft,
TopRight,
BottomRight,
}

public enum Origin180
{
Bottom,
Left,
Top,
Right,
}

public enum Origin360
{
Bottom,
Right,
Top,
Left,
}

其中一种比较复杂的Filled类型的Image,360度填充(Radial360),其示意图如下:

alt text

接下来看函数GenerateFilledSprite(...)

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
static readonly Vector3[] s_Xy = new Vector3[4];
static readonly Vector3[] s_Uv = new Vector3[4];
void GenerateFilledSprite(VertexHelper toFill, bool preserveAspect)
{
toFill.Clear();

if (m_FillAmount < 0.001f)
return;

Vector4 v = GetDrawingDimensions(preserveAspect);
Vector4 outer = activeSprite != null ? Sprites.DataUtility.GetOuterUV(activeSprite) : Vector4.zero;
UIVertex uiv = UIVertex.simpleVert;
uiv.color = color;

float tx0 = outer.x;
float ty0 = outer.y;
float tx1 = outer.z;
float ty1 = outer.w;

// Horizontal and vertical filled sprites are simple -- just end the Image prematurely
if (m_FillMethod == FillMethod.Horizontal || m_FillMethod == FillMethod.Vertical)
{
if (fillMethod == FillMethod.Horizontal)
{
float fill = (tx1 - tx0) * m_FillAmount;

if (m_FillOrigin == 1)
{
v.x = v.z - (v.z - v.x) * m_FillAmount;
tx0 = tx1 - fill;
}
else
{
v.z = v.x + (v.z - v.x) * m_FillAmount;
tx1 = tx0 + fill;
}
}
else if (fillMethod == FillMethod.Vertical)
{
float fill = (ty1 - ty0) * m_FillAmount;

if (m_FillOrigin == 1)
{
v.y = v.w - (v.w - v.y) * m_FillAmount;
ty0 = ty1 - fill;
}
else
{
v.w = v.y + (v.w - v.y) * m_FillAmount;
ty1 = ty0 + fill;
}
}
}

s_Xy[0] = new Vector2(v.x, v.y);
s_Xy[1] = new Vector2(v.x, v.w);
s_Xy[2] = new Vector2(v.z, v.w);
s_Xy[3] = new Vector2(v.z, v.y);

s_Uv[0] = new Vector2(tx0, ty0);
s_Uv[1] = new Vector2(tx0, ty1);
s_Uv[2] = new Vector2(tx1, ty1);
s_Uv[3] = new Vector2(tx1, ty0);

{
if (m_FillAmount < 1f && m_FillMethod != FillMethod.Horizontal && m_FillMethod != FillMethod.Vertical)
{
if (fillMethod == FillMethod.Radial90)
{
if (RadialCut(s_Xy, s_Uv, m_FillAmount, m_FillClockwise, m_FillOrigin))
AddQuad(toFill, s_Xy, color, s_Uv);
}
else if (fillMethod == FillMethod.Radial180)
{
for (int side = 0; side < 2; ++side)
{
float fx0, fx1, fy0, fy1;
int even = m_FillOrigin > 1 ? 1 : 0;

if (m_FillOrigin == 0 || m_FillOrigin == 2)
{
fy0 = 0f;
fy1 = 1f;
if (side == even)
{
fx0 = 0f;
fx1 = 0.5f;
}
else
{
fx0 = 0.5f;
fx1 = 1f;
}
}
else
{
fx0 = 0f;
fx1 = 1f;
if (side == even)
{
fy0 = 0.5f;
fy1 = 1f;
}
else
{
fy0 = 0f;
fy1 = 0.5f;
}
}

s_Xy[0].x = Mathf.Lerp(v.x, v.z, fx0);
s_Xy[1].x = s_Xy[0].x;
s_Xy[2].x = Mathf.Lerp(v.x, v.z, fx1);
s_Xy[3].x = s_Xy[2].x;

s_Xy[0].y = Mathf.Lerp(v.y, v.w, fy0);
s_Xy[1].y = Mathf.Lerp(v.y, v.w, fy1);
s_Xy[2].y = s_Xy[1].y;
s_Xy[3].y = s_Xy[0].y;

s_Uv[0].x = Mathf.Lerp(tx0, tx1, fx0);
s_Uv[1].x = s_Uv[0].x;
s_Uv[2].x = Mathf.Lerp(tx0, tx1, fx1);
s_Uv[3].x = s_Uv[2].x;

s_Uv[0].y = Mathf.Lerp(ty0, ty1, fy0);
s_Uv[1].y = Mathf.Lerp(ty0, ty1, fy1);
s_Uv[2].y = s_Uv[1].y;
s_Uv[3].y = s_Uv[0].y;

float val = m_FillClockwise ? fillAmount * 2f - side : m_FillAmount * 2f - (1 - side);

if (RadialCut(s_Xy, s_Uv, Mathf.Clamp01(val), m_FillClockwise, ((side + m_FillOrigin + 3) % 4)))
{
AddQuad(toFill, s_Xy, color, s_Uv);
}
}
}
else if (fillMethod == FillMethod.Radial360)
{
for (int corner = 0; corner < 4; ++corner)
{
float fx0, fx1, fy0, fy1;

if (corner < 2)
{
fx0 = 0f;
fx1 = 0.5f;
}
else
{
fx0 = 0.5f;
fx1 = 1f;
}

if (corner == 0 || corner == 3)
{
fy0 = 0f;
fy1 = 0.5f;
}
else
{
fy0 = 0.5f;
fy1 = 1f;
}

s_Xy[0].x = Mathf.Lerp(v.x, v.z, fx0);
s_Xy[1].x = s_Xy[0].x;
s_Xy[2].x = Mathf.Lerp(v.x, v.z, fx1);
s_Xy[3].x = s_Xy[2].x;

s_Xy[0].y = Mathf.Lerp(v.y, v.w, fy0);
s_Xy[1].y = Mathf.Lerp(v.y, v.w, fy1);
s_Xy[2].y = s_Xy[1].y;
s_Xy[3].y = s_Xy[0].y;

s_Uv[0].x = Mathf.Lerp(tx0, tx1, fx0);
s_Uv[1].x = s_Uv[0].x;
s_Uv[2].x = Mathf.Lerp(tx0, tx1, fx1);
s_Uv[3].x = s_Uv[2].x;

s_Uv[0].y = Mathf.Lerp(ty0, ty1, fy0);
s_Uv[1].y = Mathf.Lerp(ty0, ty1, fy1);
s_Uv[2].y = s_Uv[1].y;
s_Uv[3].y = s_Uv[0].y;

float val = m_FillClockwise ?
m_FillAmount * 4f - ((corner + m_FillOrigin) % 4) :
m_FillAmount * 4f - (3 - ((corner + m_FillOrigin) % 4));

if (RadialCut(s_Xy, s_Uv, Mathf.Clamp01(val), m_FillClockwise, ((corner + 2) % 4)))
AddQuad(toFill, s_Xy, color, s_Uv);
}
}
}
else
{
AddQuad(toFill, s_Xy, color, s_Uv);
}
}
}

这里也用到了两组Vector3数组,s_Xys_Uv以复用和缓存临时变量。水平填充和竖直填充的类型比较简单,根据m_FillOriginm_FillAmount计算出来顶点坐标(v表示)和纹理坐标(tx0tx1ty0ty1表示),顶点坐标和纹理坐标会分别被保存在s_Xys_Uv中。

这时候,如果满足条件:

1
m_FillAmount < 1f && m_FillMethod != FillMethod.Horizontal && m_FillMethod != FillMethod.Vertical

则处理接下来的比较复杂的逻辑,否则直接使用s_Xys_Uv中保存的顶点坐标和纹理坐标绘制Quad:

1
2
3
4
5
6
7
8
9
10
static void AddQuad(VertexHelper vertexHelper, Vector3[] quadPositions, Color32 color, Vector3[] quadUVs)
{
int startIndex = vertexHelper.currentVertCount;

for (int i = 0; i < 4; ++i)
vertexHelper.AddVert(quadPositions[i], color, quadUVs[i]);

vertexHelper.AddTriangle(startIndex, startIndex + 1, startIndex + 2);
vertexHelper.AddTriangle(startIndex + 2, startIndex + 3, startIndex);
}

这里的AddQuad与前边的同名函数参数不同,但是所执行的功能是一致的。如果不幸满足了前边提到的条件,也即是说填充方式m_FillMethod是径向的90度Radial90、180度Radial180、360度Radial360并且m_FillAmount不等于1,那么就要根据具体情况划分为更多个Quad来处理了。使用if else搞了三个分支,依次分别处理90度、180度、360度的情况。

90度的情况用到了方法RadialCut()如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static bool RadialCut(Vector3[] xy, Vector3[] uv, float fill, bool invert, int corner)
{
// Nothing to fill
if (fill < 0.001f) return false;

// Even corners invert the fill direction
if ((corner & 1) == 1) invert = !invert;

// Nothing to adjust
if (!invert && fill > 0.999f) return true;

// Convert 0-1 value into 0 to 90 degrees angle in radians
float angle = Mathf.Clamp01(fill);
if (invert) angle = 1f - angle;
angle *= 90f * Mathf.Deg2Rad;

// Calculate the effective X and Y factors
float cos = Mathf.Cos(angle);
float sin = Mathf.Sin(angle);

RadialCut(xy, cos, sin, invert, corner);
RadialCut(uv, cos, sin, invert, corner);
return true;
}

根据填充比例,如果小于0.001f直接不绘制,如果大于0.999f直接绘制全部。后边根据填充比例fill、填充方向是否invert(其实就是顺时针和逆时针方向),以及原点位置,来计算(修改)顶点坐标xy和纹理uv。使用的RadialCut()如下:

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
static void RadialCut(Vector3[] xy, float cos, float sin, bool invert, int corner)
{
int i0 = corner;
int i1 = ((corner + 1) % 4);
int i2 = ((corner + 2) % 4);
int i3 = ((corner + 3) % 4);

if ((corner & 1) == 1)
{
if (sin > cos)
{
cos /= sin;
sin = 1f;

if (invert)
{
xy[i1].x = Mathf.Lerp(xy[i0].x, xy[i2].x, cos);
xy[i2].x = xy[i1].x;
}
}
else if (cos > sin)
{
sin /= cos;
cos = 1f;

if (!invert)
{
xy[i2].y = Mathf.Lerp(xy[i0].y, xy[i2].y, sin);
xy[i3].y = xy[i2].y;
}
}
else
{
cos = 1f;
sin = 1f;
}

if (!invert) xy[i3].x = Mathf.Lerp(xy[i0].x, xy[i2].x, cos);
else xy[i1].y = Mathf.Lerp(xy[i0].y, xy[i2].y, sin);
}
else
{
if (cos > sin)
{
sin /= cos;
cos = 1f;

if (!invert)
{
xy[i1].y = Mathf.Lerp(xy[i0].y, xy[i2].y, sin);
xy[i2].y = xy[i1].y;
}
}
else if (sin > cos)
{
cos /= sin;
sin = 1f;

if (invert)
{
xy[i2].x = Mathf.Lerp(xy[i0].x, xy[i2].x, cos);
xy[i3].x = xy[i2].x;
}
}
else
{
cos = 1f;
sin = 1f;
}

if (invert) xy[i3].y = Mathf.Lerp(xy[i0].y, xy[i2].y, sin);
else xy[i1].x = Mathf.Lerp(xy[i0].x, xy[i2].x, cos);
}
}

最后经过一系列计算,更新xyuv的值,绘制出Quad。180度的情况,需要将整个绘制区域分为两部分,并根据起点位置、填充方向等确定fx0, fx1, fy0, fy1这组参数来切割Image及纹理。最终转化为90度的问题来解决,360度填充也同理。

Material

前边大量篇幅都是在处理顶点(包括顶点坐标和顶点纹理坐标),接下来是材质,Image覆写了Graphic的方法UpdateMaterial,额外对alphaTex做了一些处理,即获取activeSprite是否有附带的alpha纹理(如ETC1格式的图片等),如果有的话就将这张alpha纹理传给canvasRenderer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected override void UpdateMaterial()
{
base.UpdateMaterial();

// check if this sprite has an associated alpha texture (generated when splitting RGBA = RGB + A as two textures without alpha)

if (activeSprite == null)
{
canvasRenderer.SetAlphaTexture(null);
return;
}

Texture2D alphaTex = activeSprite.associatedAlphaSplitTexture;

if (alphaTex != null)
{
canvasRenderer.SetAlphaTexture(alphaTex);
}
}

自动布局

Image实现了接口ILayoutElement,因此可以作为自动布局元素。Image中主要是覆写了两个属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public virtual float preferredWidth
{
get
{
if (activeSprite == null)
return 0;
if (type == Type.Sliced || type == Type.Tiled)
return Sprites.DataUtility.GetMinSize(activeSprite).x / pixelsPerUnit;
return activeSprite.rect.size.x / pixelsPerUnit;
}
}
public virtual float preferredHeight
{
get
{
if (activeSprite == null)
return 0;
if (type == Type.Sliced || type == Type.Tiled)
return Sprites.DataUtility.GetMinSize(activeSprite).y / pixelsPerUnit;
return activeSprite.rect.size.y / pixelsPerUnit;
}
}

获取preferredWidthpreferredHeight的具体实现,如果时Sliced或者Tiled,使用Sprite的MinSize,否则使用rect的尺寸。

Raycast

Image实现了接口ICanvasRaycastFilter中的IsRaycastLocationValid方法,用于判断是否被射线射中:

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
public virtual bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
{
if (alphaHitTestMinimumThreshold <= 0)
return true;

if (alphaHitTestMinimumThreshold > 1)
return false;

if (activeSprite == null)
return true;

Vector2 local;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, eventCamera, out local))
return false;

Rect rect = GetPixelAdjustedRect();

// Convert to have lower left corner as reference point.
local.x += rectTransform.pivot.x * rect.width;
local.y += rectTransform.pivot.y * rect.height;

local = MapCoordinate(local, rect);

// Normalize local coordinates.
Rect spriteRect = activeSprite.textureRect;
Vector2 normalized = new Vector2(local.x / spriteRect.width, local.y / spriteRect.height);

// Convert to texture space.
float x = Mathf.Lerp(spriteRect.x, spriteRect.xMax, normalized.x) / activeSprite.texture.width;
float y = Mathf.Lerp(spriteRect.y, spriteRect.yMax, normalized.y) / activeSprite.texture.height;

try
{
return activeSprite.texture.GetPixelBilinear(x, y).a >= alphaHitTestMinimumThreshold;
}
catch (UnityException e)
{
Debug.LogError("Using alphaHitTestMinimumThreshold greater than 0 on Image whose sprite texture cannot be read. " + e.Message + " Also make sure to disable sprite packing for this sprite.", this);
return true;
}
}

首先判断特殊情况,alphaHitTestMinimumThreshold取0或者1,时就直接返回结果,否则需要做一系列了转换,得到射线射中位置的a即像素颜色的透明度,与alphaHitTestMinimumThreshold对比,如果大于等于则认为是有效(被射中)。

接受序列化回调

Image实现接口ISerializationCallbackReceiver,其中有两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public virtual void OnBeforeSerialize() {}

public virtual void OnAfterDeserialize()
{
if (m_FillOrigin < 0)
m_FillOrigin = 0;
else if (m_FillMethod == FillMethod.Horizontal && m_FillOrigin > 1)
m_FillOrigin = 0;
else if (m_FillMethod == FillMethod.Vertical && m_FillOrigin > 1)
m_FillOrigin = 0;
else if (m_FillOrigin > 3)
m_FillOrigin = 0;

m_FillAmount = Mathf.Clamp(m_FillAmount, 0f, 1f);
}

在反序列化之后,对成员变量的值的有效性进行校正。


本系列其它文章详见Unity3D UGUI 源码学习

REFERENCE

https://docs.unity3d.com/ScriptReference/Sprite.html

https://docs.unity3d.com/ScriptReference/Sprites.DataUtility.html