深入UGUI源码去认识Image。
作为Graphic家族最重要的成员之一,我相信你的UI里面Image是必不可少的元素。我也相信大部分使用者都能够熟练的应用Image。这一篇文章并不是Image的使用教程,只是从源码角度的对Image的剖析,以及总结(源码请自行下载)。
属性
首先简单介绍一下image面板上各属性:
pro.png
Source Image:图片资源,支持精灵贴图;
Color:图片颜色,默认为白色;
Material:材质;
Raycast Target:是否是射线投射目标;是——此Image可以接受射线投射,并且会遮挡被覆盖UI的事件调用;否——射线忽视Image,可以穿透Image。
建议:普通Image选择否,需要添加事件调用的Image选择是。
Image Type:图片显示方式,总共有4种:Simple,Sliced,Tiled,Filled(本文的介绍重点,此处不在解释)。
Preserve Aspect:图片是否以原比例显示;
Set Native Size:设置图片以原尺寸显示。
源码剖析
接下来主要通过Image几个重要的函数来解读Image:
1. private Vector4 GetDrawingDimensions(bool shouldPreserveAspect);
返回的向量就是图片去掉padding之后中间部分的x,y,z,w坐标。源码如下:
/// Image's dimensions used for drawing. X = left, Y = bottom, Z = right, W = top.
///shouldPreserveAspect为是否按精灵的原比例显示
///rect (x,y是左下角相对于中心点的坐标,weight和height分别为宽,高
private Vector4 GetDrawingDimensions(bool shouldPreserveAspect)
{
//当前精灵的填充内边框(left,bottom,right,top),一般情况下都是(0,0,0,0)
var padding = activeSprite == null ? Vector4.zero : Sprites.DataUtility.GetPadding(activeSprite);
//当前精灵的大小(包含了边框的大小)
var size = activeSprite == null ? Vector2.zero : new Vector2(activeSprite.rect.width, activeSprite.rect.height);
//目标绘制区域的坐标及大小(x,y为该UI相对于轴心的坐标,width,height为UI宽高)
Rect r = GetPixelAdjustedRect();
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;
}
}
//重新计算x,y,z,w的大小
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;
}
计算最后的向量如图所示:
v.png
2. private void GenerateSimpleSprite(VertexHelper vh, bool lPreserveAspect);
简单模式下的顶点信息。源码如下:
void GenerateSimpleSprite(VertexHelper vh, bool lPreserveAspect)
{
//获得图片的位置信息
Vector4 v = GetDrawingDimensions(lPreserveAspect);
//获得精灵的uv坐标
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);
}
可以看得出来,网格是由4个顶点,两个三角形构成。在Unity中如图所示:
simple.png
simple.gif
在这个模式下,无论图片怎么变化,网格永远是由4个顶底,两个三角形构成。这种模式也是最少消耗性能的模式。但是带来的问题是,如果图形需要非等比例缩放,那么就会引起图片显示比例失调而失真。
3. private void GenerateSlicedSprite(VertexHelper toFill)
裁剪模式下的顶点信息。源码如下:
static readonly Vector2[] s_VertScratch = new Vector2[4];
static readonly Vector2[] s_UVScratch = new Vector2[4];
/// <summary>
/// 得到9个区域(用边框裁剪开的9个区域)
/// </summary>
/// <param name="toFill"></param>
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();
//生成9个矩形区域
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));
}
}
}
假设上下左右都存在外边框的话(即九宫格的图片格式),那么顶点的个数是固定的16个,如果去除中心的话,三角形是16个,带有中心的话,三角形是18个。从源码可以看出,网格的边框4个角部是永远不会被拉伸,一直是边框大小原比例显示(除非是显示的尺寸小于边框的大小),边框的中部,以及图片去除边框的中心是会随着图形的拉伸而变化。如图所示:
sliced.gif
接下来,把目光放在这张图片的圆角上:
simple1.gif
上图为简单模式下的图片拉伸
sliced1.gif
上图为裁剪模式下的图片拉伸,你觉得那个更具原生态。而且我们也可以使用三宫格,以及更特殊的裁剪边框的模式来处理特殊的图片。
4. void GenerateTiledSprite(VertexHelper toFill);
平铺模式下的顶点信息。源码过长就不放了,其构造方式与裁剪模式相似,不过对于平铺模式下,除了网格边框的4个与裁剪模式一样,他的边框中部以及图像中间会按尺寸比例像瓦片一样平铺产生,因此会构造大量的顶点和三角形,因此这种情况除非是特殊需求,尽量不要使用。如图所示:
tiled.gif
5. void GenerateFilledSprite(VertexHelper toFill, bool preserveAspect)
覆盖模式的顶点信息。其内部通过FillAmount的值来控制需要构建的顶点的数量。这种模式对于处理进度类似的效果非常有效。除此之外他与简单模式具有一样的特点。如图所示:
filled.gif
小结
作为UGUI最重要的控件之一:Image,我们不仅仅要会使用,还要懂得他背后的原理。Image这几种模式,各有各的特点,比如icon我们更偏向使用简单模式,并按比例显示,对于需要拉伸的图片,我们往往会使用裁剪模式。很多时间,我们的项目中需要显示的是一张原生态的图片,那么如果使我们的图片显得更加自然,相信在文中,你能找到答案。关于顶点这一块有什么不懂的地方,请参考之前文章:Unity_UGUI|通向UGUI源码的入口VertexHelper 链接:https://www.jianshu.com/p/2245969a9173
网友评论