oynix

于无声处听惊雷,于无色处见繁花

支持多种缩放模式的Unity图片组件 - ScaleImage

Unity自带的Image组件基本可以满足大多显示固定尺寸图片的需求,但是对于一些提前不知道尺寸以及宽高比例,同时显示空间又固定的场景,比如一个图片列表,其中有些可能来自网络、也可能来自用户从本地上传,有着大大小小的尺寸和比例,填入固定大小的Image组件时,便会图片变形、或是填不满item空间,又或是使用Mask遮罩裁剪额外增加DC。

1. 简言

既然展示的空间固定,那么就需要调整展示图片的区域。刚拿到这个需求时,最直接的想法就是通过写个Shader,通过控制控制Material的参数来控制绘制哪些像素,后来看到Image的源码中onPopulateMesh方法时,发现通过重写这个方法更合适,可以在代码里直接控制需要把图片的哪个区域绘制到Image组件的哪个区域上。

举个例子,比如Image组件设定的大小是100x100像素,而图片的大小是200x300像素,需求是不对图片进行任何缩放,能展示多少内容就展示多少内容,那么最终就是将图片中心区域100x100的像素,绘制到Image组件上。

当需求变成必须填满Image组件,但图片不能变形,这个时候就需要按照Image组件的比例从图片中截取同样比例的最大区域的像素,也就是选取图片中心区域200x200的像素,绘制到Image组件上。

借鉴Android中ImageView,我设计了7种不同的缩放模式,分别是:

  • Center
  • CenterInside
  • CenterCrop
  • FitXY
  • FitCenter
  • FitStart
  • FitEnd

接下来一一介绍。

2. 缩放模式:ScaleType

下面说明中所用到的图片尺寸是:220x229

2.1 Center

这种模式下,不对图片进行任何缩放,Image能展示多少便展示多少,示例图:

如图所示,假定了3种情况,当Image比图片大,图片在Image中间显示,白色边框是Image的尺寸;尺寸相同时,刚好可以完整显示;当Image小于图片尺寸时,则只显示图片中心同等大小区域的像素。

2.2 CenterInside

与Center类似,Image能完全显示图片时候则不进行任何缩放,仅显示;当不能完全显示时,则会按照原试图比例缩放,使得至少有一个方向完全填满Image。

2.3 CenterCrop

这种模式下会将Image区域填满,如果图片的大小过大或者过小,则按照原试图比例缩放,直到将Image的两个方向都填满,超出的部分则裁剪。

2.4 FitXY

这种模式最好理解,直接用图片的所有像素填满Image区域,所以当两个比例不同时,会出现图片变形。

2.5 FitCenter

这种模式下,图片会以尽可能大的原始比例尺寸去显示,即当图片过大或者过小时,则按照原试图的比例进行缩放,直到至少填满Image的一个方向,如果比例相同则可以填满两个方向。乍一看和CenterInside有些像,二者的区别是,当可以完全显示时,CenterInside只是将图片放到Image中心而不进行缩放,而FitCenter会进行缩放,看示例图可以直观看出区别。

2.6 FitStart

与FitCenter类似,区别在于当图片不能填满Image区域时,会对齐Start方向,水平方向有空白区域时则对齐左侧,垂直方向有空白区域时,则对齐顶部。

2.7 FitEnd

看过FitCenter和FitStart,这个模式就很好理解了,当图片不能填满Image区域时,则对齐右侧或者底部。

3. 圆角

除了有多种可选的缩放模式,ScaleImage组件还实现了常见的圆角功能。常见的实现方式是用一张带圆角的图片,结合Mask遮罩来实现,这种方式可能会打断DrawCall合批,在网上搜索时,发现一种新思路:圆角处的扇形,也可以拆成多个三角形,当三角形的数量足够多时,就会无限接近扇形。两种方式在大多时候都没有太大区别,只是后者更灵活些。

3.1 思路

Image代码中的onPopulateMesh方法,是通过VertexHelper来添加顶点和三角形来绘制图片。通过4个顶点和2个三角形,便可以绘制一个矩形的图片:

所以,在明确了上一点之后,如果按照如下的方式拆分顶点和三角形,便可以实现类似圆角的效果:

3.2 效果

以上是不同数量的三角时,圆角呈现的效果,可以看出,当使用6个三角形时,棱角分明的感觉就很弱了,当三角形数量来到10时,视觉效果已经很圆滑,数量继续增加时,对效果不再有明显的提升。

4. 使用

GitHub上打包了一个unitypackage,导入项目后,Hierarchy窗口中右击菜单的UI子菜单中就可以看到ScaleImage选项,点击之后就可以添加。之后就可以在Inspector中看到如下图所示的设置:

因为继承自UGUI的Image组件,所以上半部分是原始的属性,下面是额外增加的属性,因为本身增加的是处理缩放的内容,所以Image组件原本的ImageType属性隐藏了,不处理Sliced、Tiled和Filled这几种情况。

Set Native Size按钮用来快速将自身尺寸设置成图片的尺寸,Use Parent Size按钮用来快速将自身尺寸设置成和父组件的尺寸。

勾选Round Corner后,将处理圆角参数,不勾选时则跳过。Radius Ratio是圆角的半径和边长的比例,变化范围0到0.5,实现时会选择使用较短的边;而Triangle Num则是动态设定圆角处三角形的数量,可根据实际情况调整,本着效果刚刚好设置即可,过高对效果没有帮助,反而会增加顶点和三角形的数量,增加绘制成本。

5. 实现代码

完整实现代码会放到GitHub,这里只拿出部分关键的代码来说说。

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
protected override void OnPopulateMesh(VertexHelper toFill)
{
if (activeSprite == null)
{
base.OnPopulateMesh(toFill);
return;
}

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

上面是Image的源代码,可以看出,重写OnPopulateMesh方法时,实现一个可以替代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);
}

这个方法很简单,正如上面所言,绘制一个矩形的图片只需要4个顶点,和2个三角形,而这个方法内也正是通过VertextHelper添加了这些,剩下的就是GPU的事了。

其中的Vector4 v是目标Image绘制区域,Verctor4 uv则是图片的像素区域,4个值分别代表着区域的4条边:left,bottom,right和top。

所以,如果自定义填满Image的那些区域,则需要修改变量Vector4 v;同样,如果自定义需要图片中哪些区域的像素,则需要修改Vector4 uv变量;绘制圆角时,则是自定义添加顶点和三角形。

用CenterCrop举例说明一下,这种模式下,不需要操作Image的区域,因为是完全填满的,只需要计算图片需要选取的区域。

因为是按照原图比例缩放,所以需要先确定是按照宽缩放,还是按照高缩放,这取决于图片的比例和Image比例的大小。如果是以宽为准,也就是让图片的宽完全显示,则先计算图片的宽到Image的宽的缩放值,然后以同样的缩放值去缩放高度,缩放之后的高度无法完全展示,则需要计算截取的部分,然后去偏移图片区域uv的bottom和top即可。

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
void GenerateScaleSprite(VertexHelper vh)
{
var uv = (activeSprite != null) ? DataUtility.GetOuterUV(activeSprite) : Vector4.zero;
var v = GetDrawingDimensionsIgnorePadding(false);
var r = rectTransform.rect;
var mBoundSize = new Vector2(r.width, r.height);

var sSize = activeSprite.rect.size;

switch (scaleType)
{
case ScaleType.CenterCrop:
{
var boundRatio = mBoundSize.x / mBoundSize.y;
var spriteRatio = sSize.x / sSize.y;

if (spriteRatio > boundRatio)
{
var oldW = sSize.x;
var w = sSize.y * boundRatio;
var offsetRatio = (oldW - w) / 2f / oldW;
var offset = offsetRatio * (uv.z - uv.x);
uv.x += offset;
uv.z -= offset;
}
else
{
var oldH = sSize.y;
var h = sSize.x / boundRatio;
var offsetRatio = (oldH - h) / 2f / oldH;
var offset = (uv.w - uv.y) * offsetRatio;
uv.y += offset;
uv.w -= offset;
}

break;
}
}


if (!roundCorner)
DrawRect(vh, v, uv);
else
DrawRoundCornerRect(vh, v, uv);
}

依此类推,按需调整图片区域和Image区域4条边的偏移,即可实现对应的效果。

6. 仓库地址

https://github.com/oynix/ScaleImage

7. 参考

  • SimpleRoundedImage-不使用mask实现圆角矩形图片
------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2025/07/9b16008baea0/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道