想要成為 Unity Canvas UI (UGUI) 的排版大師,那麼我們就一定要充分了解 UGUI 最基礎的排版類別 RectTransform ,它屬於 Transform 的子類別,用於描述元件如何擺放在二維介面中,既然是在二維介面中元件的 Transform,那麼叫做 Rect 的 Transform 也是很合理的吧!關於 RectTransform 的官方技術文件我們可以在 Unity - Manual: Rect Transform 找得到。
在編輯器中快速設定 RectTransform
快速控制器的基本操作我們就不在這裡贅述,基本上都可以在 Unity 網站裡找到很好的教學文件與影片。
RectTransform 的控制精髓:錨點們 Anchor Points
在透過快速設定器修改 RectTransform 的過程中,你會發現右上區域會隨著不同的配置出現不同的屬性設定如下圖:
Pos X 與 Left、Pos Y 與 Top、Width 與 Right、Height 與 Bottom 這四對屬性個別不會同時出現,有 Pos X 就沒有 Left,有 Width 就沒有 Right,那麼他們出現的規則是什麼呢?
簡單來說其實就是:
「當兩個錨點的某一維度值相等時,該維度的尺寸則是固定的(跟 Parent 尺寸無關),反之該維度的尺寸則是相對於 Parent 的尺寸而變化。」
其實全部都取決於控制 RectTransform 型態最重要的屬性「最大與最小錨點們(Min / Max Anchors)」,而快速設定器其實也只是在幫你快速的調整這兩個錨點的值,所以只要了解這兩個設定值關係與行為,其實你已經完全掌握了 RectTransform ,而依照上述邏輯,透過兩個錨點所產生出的配置型態總共有四種:
A. 當兩錨點 x, y 維度的值都相等時。 B. 當兩錨點 x 維度的值不相等、y 維度值相等時。 C. 當兩錨點 x 維度的值相等、y 維度值不相等時。 D. 當兩錨點 x, y 維度的值都不相等時。
A. 當兩錨點 x, y 維度的值都相等時:
當兩錨點 x, y 值都相等時,代表此物件的寬高尺寸都是固定值,所以我們會透過 PosX、PosY、Width 以及 Height 來定義此物件的顯示方式,PosX 與 PosY 則分別表示錨點到物件 Pivot 點的位移,而此物件的實際顯示區域則會受到 Pivot 的 x, y 值設定所影響。
B. 當兩錨點 x 維度的值不相等、y 維度值相等時:
當兩錨點 x 維度的值不相等、y 維度值相等時,代表 x 維度的尺寸會受到 Parent 的尺寸影響,在 x 維度上則是使用間距(Padding)的概念來排版,所以會用到 Left、PosY、Right 以及 Height,實際的 Width 是由 Left 與 Right 來控制。
C. 當兩錨點 x 維度的值相等、y 維度值不相等時:
當兩錨點 x 維度的值相等、y 維度值不相等時,代表 y 維度的尺寸會受到 Parent 的尺寸影響,在 y 維度上則是使用間距(Padding)的概念來排版,所以會用到 PosX、Top、Width 以及 Bottom,實際的 Height 是由 Top 與 Bottom 來控制。
D. 當兩錨點 x, y 維度的值都不相等時:
當兩錨點 x, y 維度的值都不相等時,代表物件的寬高尺寸都會受到 Parent 的影響,完全是使用四個方向的間距來定義此物件的顯示區域 Left、Top、Right 以及 Bottom。
用程式碼成為 RectTransform 的操控大師
當我們充分瞭解了 RectTransform 的運作原理後,我們接著就可以透過撰寫程式在程式運行中輕鬆控制排版的行為,由於 RectTransform 是 Transform 的子類別,所以直接透過轉型就可以拿到:
var rectTransform = this.transform as RectTransform
而所有的 UI Element 也都可以直接透過屬性 rectTranform 獲得:
public Text SomeText;
void Awake () {
var rectTransform = SomeText.rectTransform;
}
RectTransform 的屬性
在使用程式碼控制 RectTranform 之前,我們必須清楚了解各屬性實際代表的意義,才不會在執行時出現非預期的排版狀況,其中最常見的錯誤就是把 sizeDelta 誤認為在設定真正的 size,或是把父類別的 localPosition 當作 anchoredPosition 來用,下圖為所有參數的幾何關係圖:
sizeDelta 根據文件定義是:The size of this RectTransform relative to the distances between the anchors.
所以 RectTransform 真正的計算出的 rect.width 會等於 (anchorMax.x - anchorMin.x) * Parent.rect.width + sizeDelta.x
同理
rect.height 會等於 (anchorMax.y - anchorMin.y) * Parent.rect.height + sizeDelta.y
意思就是 sizeDelta 個別維度的值是跟兩錨點個別維度的差值相關,所以只有當兩錨點某的維度的值相等的時候,sizeDelta 在此維度的值才會剛好等於最後顯示的 size 大小。
而 offsetMin 則代表 Min 錨點到物體顯示區域左下角點的向量(注意正反方向);offsetMax 則代表 Max 錨點到物體顯示區域右上角點的向量,這就是為什麼 offsetMax 的值跟編輯器中 Top、Right 值剛好正負相反的原因。
實作對齊 (Alignment)排版功能
為了方便使用對齊功能,我們將各種對齊行為透過 FDRectAlignment 列舉(enum)來表達:
public enum FDRectAlignment {
TopLeft = 0,
Top,
TopRight,
Left,
Center,
Right,
BottomLeft,
Bottom,
BottomRight
}
接著我們就來擴充 RectTransform 類別對齊的方法:
public static class FDRectTransform {
static void SetAlignment (this RectTransform rect, Vector2 value) {
rect.anchorMax = value;
rect.anchorMin = value;
}
public static void TopLeft (this RectTransform rect) {
rect.SetAlignment (Vector2.up);
}
public static void Top (this RectTransform rect) {
rect.SetAlignment (new Vector2 (0.5f, 1f));
}
public static void TopRight (this RectTransform rect) {
rect.SetAlignment (Vector2.one);
}
public static void Left (this RectTransform rect) {
rect.SetAlignment (new Vector2 (0f, 0.5f));
}
public static void Center (this RectTransform rect) {
rect.SetAlignment (new Vector2 (0.5f, 0.5f));
}
public static void Right (this RectTransform rect) {
rect.SetAlignment (new Vector2 (1f, 0.5f));
}
public static void BottomLeft (this RectTransform rect) {
rect.SetAlignment (Vector2.zero);
}
public static void Bottom (this RectTransform rect) {
rect.SetAlignment (new Vector2 (0.5f, 0f));
}
public static void BottomRight (this RectTransform rect) {
rect.SetAlignment (Vector2.right);
}
public static void Align (this RectTransform rect) {
switch (alignment) {
case FDRectAlignment.TopLeft:
rect.TopLeft ();
break;
case FDRectAlignment.Top:
rect.Top ();
break;
case FDRectAlignment.TopRight:
rect.TopRight ();
break;
case FDRectAlignment.Left:
rect.Left ();
break;
case FDRectAlignment.Center:
rect.Center ();
break;
case FDRectAlignment.Right:
rect.Right ();
break;
case FDRectAlignment.BottomLeft:
rect.BottomLeft ();
break;
case FDRectAlignment.Bottom:
rect.Bottom ();
break;
case FDRectAlignment.BottomRight:
rect.BottomRight ();
break;
}
}
}
完成後我們就可以在其他地方輕鬆動態使用對齊:
public class AlignTest : MonoBehaviour {
public RectTransform SomeRect;
void AlignmentRect () {
SomeRect.BottomRight ();
/// or use enum
SomeRect.Align (FDRectAlignment.BottomRight);
}
}
但這樣的方式只有針對物體的 Pivot 做對齊,為了達到針對物體的內實體容 (content)完全地對齊,我們則需要針對 pivot 一併修改(預設是 true),就像 Unity 內建控制器提供的功能一樣:
public static class FDRectTransform {
static void SetAlignment (this RectTransform rect, Vector2 value, bool adjustPivot) {
rect.anchorMax = value;
rect.anchorMin = value;
if (adjustPivot) rect.pivot = value;
}
public static void TopLeft (this RectTransform rect, bool adjustPivot = true) {
rect.SetAlignment (Vector2.up, adjustPivot);
}
public static void Top (this RectTransform rect, bool adjustPivot = true) {
rect.SetAlignment (new Vector2 (0.5f, 1f), adjustPivot);
}
public static void TopRight (this RectTransform rect, bool adjustPivot = true) {
rect.SetAlignment (Vector2.one, adjustPivot);
}
public static void Left (this RectTransform rect, bool adjustPivot = true) {
rect.SetAlignment (new Vector2 (0f, 0.5f), adjustPivot);
}
public static void Center (this RectTransform rect, bool adjustPivot = true) {
rect.SetAlignment (new Vector2 (0.5f, 0.5f), adjustPivot);
}
public static void Right (this RectTransform rect, bool adjustPivot = true) {
rect.SetAlignment (new Vector2 (1f, 0.5f), adjustPivot);
}
public static void BottomLeft (this RectTransform rect, bool adjustPivot = true) {
rect.SetAlignment (Vector2.zero, adjustPivot);
}
public static void Bottom (this RectTransform rect, bool adjustPivot = true) {
rect.SetAlignment (new Vector2 (0.5f, 0f), adjustPivot);
}
public static void BottomRight (this RectTransform rect, bool adjustPivot = true) {
rect.SetAlignment (Vector2.right, adjustPivot);
}
public static void Align (this RectTransform rect, FDRectAlignment alignment, bool adjustPivot = true) {
switch (alignment) {
case FDRectAlignment.TopLeft:
rect.TopLeft (adjustPivot);
break;
case FDRectAlignment.Top:
rect.Top (adjustPivot);
break;
case FDRectAlignment.TopRight:
rect.TopRight (adjustPivot);
break;
case FDRectAlignment.Left:
rect.Left (adjustPivot);
break;
case FDRectAlignment.Center:
rect.Center (adjustPivot);
break;
case FDRectAlignment.Right:
rect.Right (adjustPivot);
break;
case FDRectAlignment.BottomLeft:
rect.BottomLeft (adjustPivot);
break;
case FDRectAlignment.Bottom:
rect.Bottom (adjustPivot);
break;
case FDRectAlignment.BottomRight:
rect.BottomRight (adjustPivot);
break;
}
}
}
為了使方法更好利用,我們可以再另外實作一組加上位移(offset)以及尺寸(size)設定的方法:
public static void TopLeft (this RectTransform rect, bool adjustPivot, Vector2 offset, Vector2 size) {
rect.TopLeft (adjustPivot);
rect.anchoredPosition = offset;
rect.sizeDelta = size;
}
public static void Top (this RectTransform rect, bool adjustPivot, Vector2 offset, Vector2 size) {
rect.Top (adjustPivot);
rect.anchoredPosition = offset;
rect.sizeDelta = size;
}
public static void TopRight (this RectTransform rect, bool adjustPivot, Vector2 offset, Vector2 size) {
rect.TopRight (adjustPivot);
rect.anchoredPosition = offset;
rect.sizeDelta = size;
}
public static void Left (this RectTransform rect, bool adjustPivot, Vector2 offset, Vector2 size) {
rect.Left (adjustPivot);
rect.anchoredPosition = offset;
rect.sizeDelta = size;
}
public static void Center (this RectTransform rect, bool adjustPivot, Vector2 offset, Vector2 size) {
rect.Center (adjustPivot);
rect.anchoredPosition = offset;
rect.sizeDelta = size;
}
public static void Right (this RectTransform rect, bool adjustPivot, Vector2 offset, Vector2 size) {
rect.Right (adjustPivot);
rect.anchoredPosition = offset;
rect.sizeDelta = size;
}
public static void BottomLeft (this RectTransform rect, bool adjustPivot, Vector2 offset, Vector2 size) {
rect.BottomLeft (adjustPivot);
rect.anchoredPosition = offset;
rect.sizeDelta = size;
}
public static void Bottom (this RectTransform rect, bool adjustPivot, Vector2 offset, Vector2 size) {
rect.Bottom (adjustPivot);
rect.anchoredPosition = offset;
rect.sizeDelta = size;
}
public static void BottomRight (this RectTransform rect, bool adjustPivot, Vector2 offset, Vector2 size) {
rect.BottomRight (adjustPivot);
rect.anchoredPosition = offset;
rect.sizeDelta = size;
}
public static void Align (this RectTransform rect, FDRectAlignment alignment, bool adjustPivot, Vector2 offset, Vector2 size) {
switch (alignment) {
case FDRectAlignment.TopLeft:
rect.TopLeft (adjustPivot, offset, size);
break;
case FDRectAlignment.Top:
rect.Top (adjustPivot, offset, size);
break;
case FDRectAlignment.TopRight:
rect.TopRight (adjustPivot, offset, size);
break;
case FDRectAlignment.Left:
rect.Left (adjustPivot, offset, size);
break;
case FDRectAlignment.Center:
rect.Center (adjustPivot, offset, size);
break;
case FDRectAlignment.Right:
rect.Right (adjustPivot, offset, size);
break;
case FDRectAlignment.BottomLeft:
rect.BottomLeft (adjustPivot, offset, size);
break;
case FDRectAlignment.Bottom:
rect.Bottom (adjustPivot, offset, size);
break;
case FDRectAlignment.BottomRight:
rect.BottomRight (adjustPivot, offset, size);
break;
}
}
}
實作延展排版功能
為了方便設定成延展排版模式,我們一樣先定義延展的列舉,總共有七種延展的模式:
- HorizontalTop: 水平延展 & 垂直頂置對齊
- HorizontalCenter: 水平延展 & 垂直置中對齊
- HorizontalBottom: 水平延展 & 垂直底置對齊
- VerticalLeft: 垂直延展 & 水平置左對齊
- VerticalCenter: 垂直延展 & 水平置中對齊
- VerticalRight: 垂直延展 & 水平置右對齊
- FullStretch: 水平垂直全延展
public enum FDRectStretch {
HorizontalTop,
HorizontalCenter,
HorizontalBottom,
VerticalLeft,
VerticalCenter,
VerticalRight,
FullStretch
}
由第二節我們所知道的,當延展的方向不同時,我們則需要不同的幾何參數來決定正確設定這個 RectTransform:
當屬於水平延展時,我們需要的參數是 left、offsetY (posY)、right 以及 height 如 SetStretchHorizontalRect 方法:(此處函式參數名稱使用 padding 前綴自來強調排版的行為)
static void SetStretchHorizontalRect (this RectTransform rect, float paddingLeft, float offsetY, float paddingRight, float height) {
rect.offsetMin = new Vector2 (paddingLeft, rect.offsetMin.y);
rect.offsetMax = new Vector2 (-paddingRight, rect.offsetMax.y);
rect.anchoredPosition = new Vector2 (rect.anchoredPosition.x, offsetY);
rect.sizeDelta = new Vector2 (rect.sizeDelta.x, height);
}
如果屬於垂直延展時,我們需要的參數則是 offsetX (posX)、top、width 以及 bottom:
static void SetStretchVerticalRect (this RectTransform rect, float offsetX, float paddingTop, float width, float paddingBottom) {
rect.offsetMin = new Vector2 (rect.offsetMin.x, paddingBottom);
rect.offsetMax = new Vector2 (rect.offsetMax.y, -paddingTop);
rect.anchoredPosition = new Vector2 (offsetX, rect.anchoredPosition.y);
rect.sizeDelta = new Vector2 (width, rect.sizeDelta.y);
}
注意這裡的設定順序是很重要的,設定 offsetMin 以及 offsetMax 屬性的值會間接改變 anchoredPosition 與 sizeDelta ,所以必須要將這兩個設定放在後面。
接者再把要提供的功能方法實作完成就大功告成了:
- StretchHorizontalTop
- StretchHorizontalCenter
- StretchHorizontalRight
- StretchVerticalLeft
- StretchVerticalCenter
- StretchVerticalRight
- StretchFull
- Stretch
public static class FDRectTransform {
public static void StretchHorizontalTop (this RectTransform rect, float paddingLeft, float offsetY,
float paddingRight, float height, bool adjustPivot) {
rect.anchorMin = Vector2.up;
rect.anchorMax = Vector2.one;
if (adjustPivot) {
rect.pivot = new Vector2 (rect.pivot.x, 1f);
}
rect.SetStretchHorizontalRect (paddingLeft, offsetY, paddingRight, height);
}
public static void StretchHorizontalCenter (this RectTransform rect, float paddingLeft, float offsetY,
float paddingRight, float height, bool adjustPivot) {
rect.anchorMin = new Vector2 (0f, 0.5f);
rect.anchorMax = new Vector2 (1f, 0.5f);
if (adjustPivot) {
rect.pivot = new Vector2 (rect.pivot.x, 0.5f);
}
rect.SetStretchHorizontalRect (paddingLeft, offsetY, paddingRight, height);
}
public static void StretchHorizontalBottom (this RectTransform rect, float paddingLeft, float offsetY,
float paddingRight, float height, bool adjustPivot) {
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.right;
if (adjustPivot) {
rect.pivot = new Vector2 (rect.pivot.x, 0f);
}
rect.SetStretchHorizontalRect (paddingLeft, offsetY, paddingRight, height);
}
public static void StretchVerticalLeft (this RectTransform rect, float offsetX, float paddingTop,
float width, float paddingBottom, bool adjustPivot) {
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.up;
if (adjustPivot) {
rect.pivot = new Vector2 (0f, rect.pivot.y);
}
rect.SetStretchVerticalRect (offsetX, paddingTop, width, paddingBottom);
}
public static void StretchVerticalCenter (this RectTransform rect, float offsetX, float paddingTop,
float width, float paddingBottom, bool adjustPivot) {
rect.anchorMin = new Vector2 (0.5f, 0f);
rect.anchorMax = new Vector2 (0.5f, 1f);
if (adjustPivot) {
rect.pivot = new Vector2 (0.5f, rect.pivot.y);
}
rect.SetStretchVerticalRect (offsetX, paddingTop, width, paddingBottom);
}
public static void StretchVerticalRight (this RectTransform rect, float offsetX, float paddingTop,
float width, float paddingBottom, bool adjustPivot) {
rect.anchorMin = Vector2.right;
rect.anchorMax = Vector2.one;
if (adjustPivot) {
rect.pivot = new Vector2 (1f, rect.pivot.y);
}
rect.SetStretchVerticalRect (offsetX, paddingTop, width, paddingBottom);
}
public static void StretchFull (this RectTransform rect, float paddingLeft, float paddingTop,
float paddingRight, float paddingBottom) {
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.offsetMin = new Vector2 (paddingLeft, paddingBottom);
rect.offsetMax = new Vector2 (-paddingRight, -paddingTop);
}
public static void Stretch (this RectTransform rect, FDRectStretch stretch, float leftOrOffsetX,
float topOrOffsetY, float rightOrWidth, float bottomOrHeight, bool adjustPivot) {
switch (stretch) {
case FDRectStretch.HorizontalTop:
rect.StretchHorizontalTop (leftOrOffsetX, topOrOffsetY, rightOrWidth, bottomOrHeight, adjustPivot);
break;
case FDRectStretch.HorizontalCenter:
rect.StretchHorizontalCenter (leftOrOffsetX, topOrOffsetY, rightOrWidth, bottomOrHeight, adjustPivot);
break;
case FDRectStretch.HorizontalBottom:
rect.StretchHorizontalBottom (leftOrOffsetX, topOrOffsetY, rightOrWidth, bottomOrHeight, adjustPivot);
break;
case FDRectStretch.VerticalLeft:
rect.StretchVerticalLeft (leftOrOffsetX, topOrOffsetY, rightOrWidth, bottomOrHeight, adjustPivot);
break;
case FDRectStretch.VerticalCenter:
rect.StretchVerticalCenter (leftOrOffsetX, topOrOffsetY, rightOrWidth, bottomOrHeight, adjustPivot);
break;
case FDRectStretch.VerticalRight:
rect.StretchVerticalRight (leftOrOffsetX, topOrOffsetY, rightOrWidth, bottomOrHeight, adjustPivot);
break;
case FDRectStretch.FullStretch:
rect.StretchFull (leftOrOffsetX, topOrOffsetY, rightOrWidth, bottomOrHeight);
break;
}
}
}
結語
RectTransform 是很方便也很常用的排版類別,但若沒搞清楚其背後的原理,往往在撰寫程式上會一頭霧水,怎麼指定位置最後顯示跟預期的不一樣,是在開始實做 UGUI 介面前需要花時間理解的重要基礎。
參考文獻
- Unity - Manual: Rect Transform (https://docs.unity3d.com/Manual/class-RectTransform.html).
- Image via Chris HE, CC Licensed.