雁过留声

a tiger in me sniffs roses

0%

UGUI中的anchor和canvas(屏幕适配)

ugui的布局功能比css更为灵活,但相应也导致更多概念,但是如果能理解下边几个问题,有助于更好地使用ugui布局

  • Rect Transform上的锚点(anchor)和支点(pivot),作用是什么?对自身位置有什么影响?
  • CanvasScaler原理?和anchor和pivot有关系没有?
  • LayoutElement如何影响Rect Transform?

RectTransform继承自Transform并添加了一些布局属性,最重要的就是anchor和pivot。Transform表示一个,RectTransform表示一个矩形区域。值得注意的是Canvas节点也是一个RectTransform组件上挂了一个Canvas组件,它也是一个Rect
另外需要知道的是,UGUI的屏幕坐标系左下角为(0,0),右上角为(1,1)
alt

Anchor

内部表示为2个点:

1
2
public Vector2 anchorMin { get; set; }
public Vector2 anchorMax { get; set; }

是在父矩形的坐标,0-1之间,即父矩形的左下角、右上角,构成了一个矩形区域,用来表示自身在父组件的定位。因为一个矩形可以理解成四条边、四个顶点,所以会有不同的理解。
根据anchor中x,y不同,RectTransform组件会展现不同的属性:
alt
alt

当anchor中anchorMin.x != anchorMax.x时,x横向位置和组件宽由left,right决定,当相等时,表示为posX和width  
当anchor中anchorMin.y != anchorMax.y时,y纵向位置和组件高由top,bottom决定,当相等时,表示为posY和height  

这会让人有些费解:

  1. pos X, pos Y为pivot点与anchor点在父坐标系中的差值,width,height为自身Rect的大小,当父Rect大小变化时,自身的大小和相对位置不会变,不会自适应

  2. 当为left,right, top,bottom时是自身Rect的四边与anchor在父坐标系的距离,由于这四个相对距离不会变化,当父Rect大小变化时,子Rect的width,height就会跟着变化以维持不变的相对距离

Pivot

Pivot即上图中空心圆环的位置,是在自身Rect中规一化的坐标。表示的是在自己坐标系中的的位置
它是旋转和缩放的中心点,同时也是Rect定位到父级时pos X, pos Y的定位点(当anchorMin, anchorMax重合时)

RectTransform组件的蓝图和原始编辑模式

蓝图模式

按下之后忽略RectTransform上的旋转和缩放,看下图就明白了,只是用于方便编辑
alt

原始编辑模式(Raw Edit Mode)

默认情况下,调整Pivot和Anchor会维持当前Rect的位置和大小,当按下Raw Edit Mode时,Rect的大小和位置会随着调整变化
alt

CanvasScaler

会随着Canvas组件Render Mode会有不同情况:

  • World Space ui会通过Canvas上指定的相机绘制,通常用于3d ui,这时CanvasScaler组件 UI Scale Mode变为 World 且不可更改。Canvas组件上的位置、大小都可以自行调节。此时ui组件的单位为Unity单位,可视范围受相机的可视范围影响

  • 当为 Screen Space - OverlayScreen Space - Camera 时,ui都在屏幕空间绘制,ui一定遮挡场景的物体。这时的Canvas组件时,它的大小是不能人为调整的,受CanvasScaler组件控制,UI Scale Mode会有三个选项

UI Scale Mode

Canvas组件的scale * width和height为屏幕的像素宽高,scale表示将canvas中的元素缩放scale值

  1. Constant Pixel Size: Canvas中的ui 大小不会随屏幕改变,保持固定大小的像素值。但因为不同设备有不同的分辨率,ui 在屏幕在就会显示过大或者过小(占据了不同的比例)

  2. Constant Physical Size: 与Constant Pixel Size类似,不过通过指定一个物理单位(cm, mm, inch, points等),ui元素的width,height是按这个单位计算,实际的像素会乘以不同设备的DPI
    比如在1920x1080分辨率,dpi为96的设备上,屏幕为宽:1920 / 96 = 20inch, 高1080 / 96 = 11.25inch,当设置image with=10,2个即会占满整个屏幕:

a

  1. Scale With Screen Size: 响应式ui会用这个,会随着屏幕缩放,ui元素占据屏幕的比例是不变的。这时会出现一个参照分辨率的选项(Reference Resolution),也叫设计分辨率,因为在做ui设计时会按照一个固定的分辨率设计ui。比如设置参照分辨率为1920x1080时,摆放的ui在4k屏幕上(3840x2160)看不出有什么不同,因为Canvas的scale会设置成2 ,即当屏幕大于设计分辨率时,scale大小1,反之小于1。这时根据 Screen Match Mode scale又有不同的计算策略:
    1. Expand,增大Canvas的size保证UI都能显示

      1
      scaleFactor = Mathf.Min(screenSize.x / referenceresolution.x, screenSize.y / referenceresolution.y);

      假设referenceresolution = (800,600), screenSize=(1920,1080)

      screenSize.x / referenceresolution.x = 2.4
      screenSize.y / referenceresolution.y = 1.8
      所以scale=1.8

      当screenSize 大于 referenceresolution时会尽可小的放大Canvas上的元素,保证在屏幕内
      当screenSize 小于 referenceresolution时,会尽可能地缩小Canvas上的UI元素,保证在屏幕内

      因为canvasSize = screenSize / min(scale),所以Canvas的Size总是大于referenceresolution

    2. Shrink,缩小Canvas可能会裁剪掉UI,但是不会出色空白
      这种情况和 Expand相反

      1
      scaleFactor = Mathf.Max(screenSize.x / referenceresolution.x, screenSize.y / referenceresolution.y);

      上例中的scale=2.4,canvas的size就会小于referenceresolution

    3. Match Width Or Height

      1
      2
      3
      4
      5
      const float kLogBase = 2;
      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);

      在上例子中:
      当m_MatchWidthOrHeight=0,以width为参照,即scale=2.4; 这时x方向的分辨率变化UI都能完全显示,但y方向可能会被裁剪
      当m_MatchWidthOrHeight=1,以height为参照,即scale=1.8;这时y方向的分辨率变化UI都能完全显示,但x方向可能会被裁剪

      在2.4-1.8之间并不是线性的,因为这样的效果更好:

      // 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

      alt

      anchor和pivot会作用于CanvasScaler,当Canvas变化时,会因anchor和pivot不同有影响没有?
      准确说没有直接关系,父rect的变化(包括Canvas)会因为anchor原因影响子rect的大小

Reference Pixels Per Unit 作用

sprite 有一个属性,Pixels Per Unit 表示每unity单位占多少像素,默认是100,当修改为50时,相同的图片占的unity单位*2,所以变大了

alt

Reference Pixels Per Unit 是用于将像素转换到UI的单位时计算ui的content size。当导入的sprite size为100, Pixels Per Unit 为100,Reference Pixels Per Unit 为 100,则把这个图片放到ui上的size为 100 * (Reference Pixels Per Unit / Pixels Per Unit) = 100。当_Reference Pixels Per Unit_ 为200时,同样的图片在ui中的size = 200

RectTransform进阶

理解RectTransform中的属性很有帮助,可以在编程中修改:

RectTransform.position 是unity的世界坐标系中的位置,即使不是Screen Space绘制在世界中也有一个坐标

RectTransform.localPosition 是pivot点在父元素坐标系中的位置(父元素的pivot),是有z值的。当父子元素pivot相同时,此时localPostion的值和anchoredPositon相等

RectTransform.anchoredPosition 无z值,是pivot相对于anchor的坐标(anchor重合),往右为正,往上为正。可以理解成锚定后的相对偏移量
alt

RectTransform.sizeDelta 并不能直接获取到Rect的长宽(除非它的anchor点重合),因为sizeDelta真正含义是RECT的大小比anchor的矩形大小差值

alt

即图中红色两段相加为sizeDelta.x, 蓝色两段相加为sizeDelta.y,因为ui比anchor矩形小,所以为负。
sizeDelta = (-63,-89)
内部

1
2
3
4
5
6
sizeDelta = offsetMax - offsetMin
```

```c#
offsetMax = ui矩形右上角坐标 - 锚点矩形右上角坐标
offsetMin = ui矩形左下角坐标 - 锚点矩形左下角坐标

当需要获取ui大小时,可以使用rect.size而不是sizeDelta。但是rect.size是只读的,如果要设置大小的话最方便的是使用RectTransform.SetSizeWithCurrentAnchors函数

下边是计算代码和关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
RectTransform rect = GetComponent<RectTransform>();
RectTransform parentRect = transform.parent.GetComponent<RectTransform>();

Vector2 GetAnchoredPositionByLocalPosition()
{
Vector2 localPosition2D = new Vector2(rect.localPosition.x, rect.localPosition.y);
Vector2 anchorMinPos = parentRect.rect.min + Vector2.Scale(rect.anchorMin, parentRect.rect.size);
Vector2 rectMinPos = rect.rect.min + localPosition2D;
Vector2 offsetMin = rectMinPos - anchorMinPos;

Vector2 anchorMaxPos = parentRect.rect.max - Vector2.Scale(Vector2.one - rect.anchorMax, parentRect.rect.size);
Vector2 rectMaxPos = rect.rect.max + localPosition2D;
Vector2 offsetMax = rectMaxPos - anchorMaxPos;

Vector2 sizeDelta = offsetMax - offsetMin;

Vector2 anchoredPosition = offsetMin + Vector2.Scale(sizeDelta, rect.pivot);

return anchoredPosition;
}

LayoutElement

RectTransform能实现往往依赖父元素的进行布局。但是有些情况父元素又需要子元素来进行布局,或者说容器大小在具体元素内容确定前是无法提前预知的,是动态变化的。这时会用到ILayoutElement,它会根据LayoutConroller有不同的行为,间接影响RectTransform。很多组件都实现了ILayoutElemnt接口,比如Image,Text,LayoutGroup,LayoutElement可以去覆盖这些默认行为。简单说和anchor、pivot之类并没有直接关系,是两套系统,可以单独去理解,并不详细说明了