SPARKFUL

Blog

Insights from the SPARKFUL teams

成為 UGUI 的排版大師 - 一次精通 RectTransform

Magic Hungdev-notes
name

想要成為 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 的運作原理後,我們接著就可以透過撰寫程式在程式運行中輕鬆控制排版的行為,由於 RectTransformTransform 的子類別,所以直接透過轉型就可以拿到:

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 介面前需要花時間理解的重要基礎。

參考文獻

  1. Unity - Manual: Rect Transform (https://docs.unity3d.com/Manual/class-RectTransform.html).
  2. Image via Chris HE, CC Licensed.
Magic Hung
Magic Hung
Technical Director
Technical Director @ Fourdesire. Create fun and crafts via programming.
divider
Related Posts
name

在 Unity 打造簡易的動作系統

在產品的介面中,適當的提供有意義並精細的動畫,實用性上能直覺的讓使用者理解目前系統的狀態、取得回饋、幫助探索產品,更能在情感上提供愉悅的體驗。此篇將介紹如何在 Unity 環境中建構一個簡易的動作系統,來幫助快速開發介面的動態效果。動...Read more
name

用 Particle System 實作模糊效果的圓形進度條

實現圓形進度條的方式有很多種,最簡易的方式就是調整 Image 的參數: Image Type - Filled 、Fill Method - Radial 360、Fill Origin - Top,這篇文章 裡有更詳細的介紹。但這個方式無法符合所有設計稿的需求,例如模糊效果、圓角、流動效果...Read more
name

在 Unity 裡做 TDD -- FDMonoBehav Framework 的誕生

這篇文章主要是在說明我們如何在 Unity 專案中導入 TDD 開發方法。開發環境主要使用 C# 語言在 Unity 2017.4.4f1 + Visual Studio for Mac Community 7.5.1,並使用 NUnit 和 NSubstitute 來作為開發單元測試專案...Read more