在工作中用到Canvas Scaler组件时,只使用到了UIScaleMode为ScaleWithScreenSize选项的情况。今天把这块的源码仔细看了一下,并且参考了这篇GAD的文章,终于弄懂了一些,下面分享一下自己的学习心得。
tips:建议读者也阅读一下上文提到的文章,因为这篇文章没有分析过程。
CanvasScaler是一个控制Canvas缩放的组件,共分为以下四种模式:
- Constant Pixel Size 固定像素大小,此时使用ScaleFactor值直接控制Canvas的Scale值。
- Scale With Screen Size 按照屏幕大小进行缩放,会根据Resolution大小与当前屏幕大小以及三种缩放策略控制Canvas的Scale值。
- Constant Physical Size 固定物理大小,此时根据设置的物理单位直接控制Canvas的Scale值。
- World 当Canvas的RenderMode为WorldSpace时会显示这种模式,此时CanvasScaler不能控制Canvas的缩放。
在详细说明四种模式之前,我想先提出一个概念:UGUI单位(或简称UI单位)。UI单位指的是你在设置UI组件大小时,所使用的单位。
例如下图中,我会把960和640的单位称为UI单位,我认为有这样一个名词对于理解接下来的内容有很大帮助。
有了上面的定义之后,我们可以得到:当UI组件的根Canvas的Scale为1时,1UI单位的大小为1像素。
Const Pixel Size 固定像素大小
在这种模式下,我们的UI组件在不同大小的屏幕上,会占用相同数量的像素。当ScaleFactor的值为x时,一个(100,100)的Image,在任何屏幕上,都会占用100x*100x 个像素,即1UI单位等于x个像素。
在采用这种模式的情况下,同样的UI在手机上会比在电脑上小很多,因为手机屏幕的像素密度要远远大于电脑(即手机屏幕的DPI大于电脑屏幕)
Constant Physical Size 固定物理大小
在这种模式下,我们的UI组件在不同大小的屏幕上,会占用相同大小的物理长度。假如我们设置PhysicalUnit的值为Inches,则我们的Canvas的Scale值会被设置为96(我电脑屏幕的DPI为96),此时1UI单位等于96个像素(即1英寸)。
举例来说,当你有一张你手掌的图片,你希望在任意的设备上,这张手掌图片都可以和你的手掌完全重合,可以采用这种模式。
这种模式下的两个参数:Fallback Screen DPI和DefaultSpriteDPI,是Unity无法取得屏幕的DPI值时的默认值和Sprite DPI默认值。当无法取得当前设备屏幕时,使用FallbackScreenDPI,而Sprite DPI是描述你使用图片的DPI,对于这个值我现在不能很好的理解,目前我认为这个值与FallbackScreenDPI设置为相同的比较好,如果有错误的话还希望告知。
实现代码如下:
protected virtual void HandleConstantPhysicalSize()
{
float currentDpi = Screen.dpi;
float dpi = (currentDpi == 0 ? m_FallbackScreenDPI : currentDpi);
float targetDPI = 1;
switch (m_PhysicalUnit)
{
case Unit.Centimeters: targetDPI = 2.54f; break;
case Unit.Millimeters: targetDPI = 25.4f; break;
case Unit.Inches: targetDPI = 1; break;
case Unit.Points: targetDPI = 72; break;
case Unit.Picas: targetDPI = 6; break;
}
SetScaleFactor(dpi / targetDPI);
SetReferencePixelsPerUnit(m_ReferencePixelsPerUnit * targetDPI / m_DefaultSpriteDPI);
}
Scale With Screen Size 根据屏幕大小缩放
在这种模式下,Canvas的Scale值会根据屏幕的实际大小与预设屏幕大小的比例进行缩放,缩放的策略分为三种:
- 根据长宽的Match值进行调整
- 使用长宽中最小的比例进行缩放
- 使用长宽中最大的比例进行缩放
具体的实现代码如下:
protected virtual void HandleScaleWithScreenSize()
{
Vector2 screenSize = new Vector2(Screen.width, Screen.height);
float scaleFactor = 0;
switch (m_ScreenMatchMode)
{
case ScreenMatchMode.MatchWidthOrHeight:
{
// We take the log of the relative width and height before taking the average.
// Then we transform it back in the original space.
// the reason to transform in and out of logarithmic space is to have better behavior.
// If one axis has twice resolution and the other has half, it should even out if widthOrHeight value is at 0.5.
// In normal space the average would be (0.5 + 2) / 2 = 1.25
// In logarithmic space the average is (-1 + 1) / 2 = 0
float logWidth = Mathf.Log(screenSize.x / m_ReferenceResolution.x, kLogBase);
float logHeight = Mathf.Log(screenSize.y / m_ReferenceResolution.y, kLogBase);
float logWeightedAverage = Mathf.Lerp(logWidth, logHeight, m_MatchWidthOrHeight);
scaleFactor = Mathf.Pow(kLogBase, logWeightedAverage);
break;
}
case ScreenMatchMode.Expand:
{
scaleFactor = Mathf.Min(screenSize.x / m_ReferenceResolution.x, screenSize.y / m_ReferenceResolution.y);
break;
}
case ScreenMatchMode.Shrink:
{
scaleFactor = Mathf.Max(screenSize.x / m_ReferenceResolution.x, screenSize.y / m_ReferenceResolution.y);
break;
}
}
SetScaleFactor(scaleFactor);
SetReferencePixelsPerUnit(m_ReferencePixelsPerUnit);
}
World 世界模式
World模式下不能设置Canvas的Scale值,此时能设置的一个值为Dynamic Pixels Per Unit。这个值目前看来是控制Text在世界UI下时显示的清晰度的,该值越大,相同Text显示的越清晰。应该说Unity在这里的实现比较诡异的(在我的理解中不应该放在Canvas Scaler里面)。
另外,所有的模式下都有一个可设置的值叫做Reference Pixels Per Unit。这个值是用来描述在NativeSize下1UI单位,应该对应多少Sprite中的像素的。
例如 Reference Pixels Per Unit 设置为200 则一个100x100的图片放入Image组件,点击SetNativeSize之后,得到的长宽是(200,200)
具体代码如下:
public float pixelsPerUnit
{
get
{
float spritePixelsPerUnit = 100;
if (sprite)
spritePixelsPerUnit = sprite.pixelsPerUnit;
float referencePixelsPerUnit = 100;
if (canvas)
referencePixelsPerUnit = canvas.referencePixelsPerUnit;
return spritePixelsPerUnit / referencePixelsPerUnit;
}
}
public override void SetNativeSize()
{
if (overrideSprite != null)
{
float w = overrideSprite.rect.width / pixelsPerUnit;
float h = overrideSprite.rect.height / pixelsPerUnit;
rectTransform.anchorMax = rectTransform.anchorMin;
rectTransform.sizeDelta = new Vector2(w, h);
SetAllDirty();
}
}