焦點事件(Focus events)

當你需要把焦點移到某個visual element或移開時,可以使用Focus event。

Focus events分為兩種不同的類型:

  • FocusOutEventFocusInEvent:在焦點發生變化之前,焦點離開事件(FocusOutEvent)和焦點進入事件(FocusInEvent)會沿著事件傳播路徑(propagation path)發送。
  • FocusEventBlurEvent:焦點事件(FocusEvent)和失焦事件(BlurEvent)會在焦點發生變化後立即發送到事件目標(event target)
Event 描述 Trickles down Bubbles up Cancellable
FocusOutEvent 在失去焦點之前,會發送一個FocusOutEvent
FocusInEvent 在element取得焦點之前,會發送一個FocusInEvent
BlurEvent 在失去焦點之後,會發送一個BlurEvent
FocusEvent 在element取得焦點之後,會發送一個FocusEvent

專屬於Focus Event的屬性

  • relatedTarget:在焦點事件中參與的第二個target element。
    • 對於FocusOutEventBlurEvent,這個屬性為聚焦的element。
    • 對於FocusInEventFocusEvent,這個屬性為失去焦點的element。
Event target relatedTarget
BlurEvent 失去焦點的element 取得焦點的element
FocusEvent 取得焦點的element 失去焦點的element
FocusInEvent 取得焦點的element 失去焦點的element
FocusOutEvent 失去焦點的element 取得焦點的element
例子:使用FocusInEventFocusOutEvent為TextField實現placeholder text功能
  1. 在Assets > Scripts > Editor 下建立一個 C# Script PlaceHolderExample.cs
  2. 將以下程式碼複製到剛剛建立的C# Script中
    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
    46
    47
    48
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.UIElements;

    public class PlaceHolderExample : EditorWindow
    {
    // 這個Attributes會在 Windows -> UI Tollkit 下加入一個 PlaceHolderExample
    [MenuItem("Window/UI Toolkit/PlaceHolderExample")]
    public static void ShowExample()
    {
    PlaceHolderExample wnd = GetWindow<PlaceHolderExample>();
    wnd.titleContent = new GUIContent("PlaceHolderExample");
    }

    private bool placeHolderMode = true;
    private const string placeHolderText = "Write here";

    public void CreateGUI()
    {
    // 建立 TextField
    TextField textField = new TextField();
    textField.value = placeHolderText;
    rootVisualElement.Add(textField);

    // 註冊FocusInEvent與FocusOutEvent
    textField.RegisterCallback<FocusInEvent>(OnFocusInTextField);
    textField.RegisterCallback<FocusOutEvent>(OnFocusOutTextField);
    }

    private void OnFocusInTextField(FocusInEvent evt)
    {
    // 如果這個 text field 收到聚焦,表示使用者想要輸入字串,因此要將placeholder文字清空
    if (placeHolderMode)
    {
    var textField = evt.target as TextField;
    textField.value = "";
    }
    }

    private void OnFocusOutTextField(FocusOutEvent evt)
    {
    // 如果這個 text field 失焦,表示使用者已經完成輸入,但如果text field為空字串,則要將placeholder文字填回去。
    var textField = evt.target as TextField;
    placeHolderMode = string.IsNullOrEmpty(textField.value);
    if (placeHolderMode)
    textField.value = placeHolderText;
    }
    }
  3. 在Unity編輯器中找到 Window > UI Toolkit > PlaceHolderExample

Reference: https://docs.unity3d.com/Manual/UIE-Focus-Events.html

點擊事件(Click events)

當一個Visual Element被滑鼠左鍵(或點擊裝置(pointing device)的第一個按鈕)點擊,將觸發一個Click Event。進行點擊操作的Visual Element會觸發Pointer Down EventPointer up Event。此外Click Event也可以檢測到非按鈕Visual Element的點擊,例如Toggle control便是使用了Click Event來處理顯示,隱藏勾號(check mask)並改變control的值。

Event 描述 Trickles down Bubbles up Cancellable
ClickEvent 當滑鼠左鍵點擊時,就會觸發一個Click Event
註冊Click Event
1
2
3
4
5
6
7
8
// 使用RegisterCallback註冊ClickEvent,同時還傳入一個VisualElement參數
// 這個asset是當click event被觸發之後,要關閉的element
btnClose.RegisterCallback<ClickEvent, VisualElement>(Clicked, asset);

private void Clicked(ClickEvent evt, VisualElement root)
{
root.ShowVisualElement(false);
}
例子:以下例子示範如何使用Click Event讓一個visual element在被點擊之後將原有顏色,替換為另一個新的顏色。
  1. 在Assets > Scripts > Editor 下建立一個 C# Script ClickEventExampleWindow.cs
  2. 將以下程式碼複製到剛剛建立的C# Script中
    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
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.UIElements;

    public class ClickEventExampleWindow : EditorWindow
    {
    // 這個Attributes會在 Windows -> UI Tollkit 下加入一個 ClickEventExample
    [MenuItem("Window/UI Toolkit/ClickEventExample")]
    public static void ShowExample()
    {
    var wnd = GetWindow<ClickEventExampleWindow>();
    wnd.titleContent = new GUIContent("Click Event Example");
    }

    public void CreateGUI()
    {
    // 建立四個不同顏色的box
    for (int i = 0; i < 4; i++)
    {
    // 為這些Visual Element設定隨機的background顏色
    var newBox = new VisualElement() { style = { flexGrow = 1, backgroundColor = GetRandomColor() } };
    rootVisualElement.Add(newBox);

    // 為這個Element註冊一個ClickEvent callback
    newBox.RegisterCallback<ClickEvent>(OnBoxClicked);
    }
    }

    // 這個callback會改變target的Background color。
    private void OnBoxClicked(ClickEvent evt)
    {
    // 只在taget階段執行這個callback
    if (evt.propagationPhase != PropagationPhase.AtTarget)
    return;

    // 設定新的Background color
    var targetBox = evt.target as VisualElement;
    targetBox.style.backgroundColor = GetRandomColor();
    }

    private Color GetRandomColor()
    {
    return new Color(Random.Range(0, 1f), Random.Range(0, 1f), Random.Range(0, 1f));
    }
    }
  3. 在Unity編輯器中找到 Window > UI Toolkit > ClickEventExample
  4. 觀看結果

Reference: https://docs.unity3d.com/Manual/UIE-Click-Events.html

變更事件(Change events)

當一個Element的值改變時,就會發送一個Change Event,例如使用者切換了(toggles)一個核取方塊(Checkbox),Change Event是一個帶有類型的事件(Typed Event),它含有這個Element先前的值和新值。由於Change Event是在新的值被賦值後才觸發,因此你無法透過取消Change Event的方式去來避免值的改變。

Event 描述 Trickles down Bubbles up Cancellable
ChangeEvent 當一個Element的值改變時,就會發送一個Change Event

專屬於Change Event的屬性

  • previousValue: 存放該Element先前的值 The previous value of the target control.
  • newValue: 存放該Element該要改變的新值

Change Event是一個通知事件(notification event)讓你可以在visual element的值改變時做出反應,例如:當使用者按下音樂核取方塊(Checkbox)那麼遊戲的音樂就應該都被關閉。

Change Event應用到所有有實作INotifyValueChanged<T>的controls其中<T>就是ChangeEvent,此外這個也被在內部使用,透過Data binding的方式來更新實體物件的屬性

避免觸發Change Event

如果你是透過程式碼更改control的值而觸發control的Change Event,你可以透過呼叫INotifyValueChange<T>中的SetValueWithoutNotify來更改control的值,並避免觸發Change Event

註冊Change Event
  1. 呼叫在Visual Element上的RegisterCallback<>()

    • 如果你想要在一個Element中監聽它的子Elements中是否有發生改變的話,可以使用
      1
      2
      3
      4
      5
      6
      7
      8
      // 使用RegisterCallback註冊callback
      rootVisualElement.RegisterCallback<ChangeEvent<bool>>(OnBoolChangedEvent);

      // callback方法,它會監聽bool值是否發生改變
      private void OnBoolChangedEvent(ChangeEvent<bool> evt)
      {
      // Handling code
      }
  2. 呼叫有實作INotifyValueChange的Visual Element上的RegisterValueChangedCallback()

    • 使用RegisterValueChangedCallback會比較方便因為它已經內建(build-in)好要傳值得類型
    • 取消註冊可以使用UnregisterValueChangedCallback
      1
      2
      3
      4
      5
      6
      var newToggle = new Toggle("Test Toggle");
      newToggle.RegisterValueChangedCallback(OnTestToggleChanged);
      private void OnTestToggleChanged(ChangeEvent<bool> evt)
      {
      // Handling code
      }
例子一:註冊兩個ChangeEvent,一個在toggle element上使用RegisterValueChangedCallback,另外一個在root element使用RegisterCallback

以下例子為示範如何使用ChangeEvent

  1. 在Assets > Scripts > Editor 下建立一個 C# Script ChangeEventTestWindow.cs
  2. 將以下程式碼複製到剛剛建立的C# Script中
    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
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.UIElements;

    public class ChangeEventTestWindow : EditorWindow
    {
    private Toggle m_MyToggle;

    // 這個Attributes會在 Windows -> UI Tollkit 下加入一個 Change Event Test Window
    [MenuItem("Window/UI Toolkit/Change Event Test Window")]
    public static void ShowExample()
    {
    ChangeEventTestWindow wnd = GetWindow<ChangeEventTestWindow>();
    wnd.titleContent = new GUIContent("Change Event Test Window");
    }

    public void CreateGUI()
    {
    // 建立一個 toggle control
    m_MyToggle = new Toggle("Test Toggle") { name = "My Toggle" };
    rootVisualElement.Add(m_MyToggle);

    // 使用RegisterValueChangedCallback為這個 toggle control註冊OnTestToggleChanged
    m_MyToggle.RegisterValueChangedCallback(OnTestToggleChanged);

    // 使用RegisterCallback在root element註冊OnBoolChangedEvent,它必須要指定泛型
    rootVisualElement.RegisterCallback<ChangeEvent<bool>>(OnBoolChangedEvent);
    }

    private void OnBoolChangedEvent(ChangeEvent<bool> evt)
    {
    Debug.Log($"Toggle changed. Old value: {evt.previousValue}, new value: {evt.newValue}");
    }

    private void OnTestToggleChanged(ChangeEvent<bool> evt)
    {
    Debug.Log($"A bool value changed. Old value: {evt.previousValue}, new value: {evt.newValue}");
    }
    }
  3. 在Unity編輯器中找到 Window > UI Toolkit > Change Events Test Window
  4. 觀看結果
例子二:使用程式碼觸發事件的話,可以透過element上的SetValueWithoutNotify避免觸發Change Event
  • 將以下程式碼覆蓋到ChangeEventTestWindow.cs Script中
    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
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.UIElements;

    public class ChangeEventTestWindow : EditorWindow
    {
    private Toggle m_MyToggle;

    [MenuItem("Window/UI Toolkit/Change Event Test Window")]
    public static void ShowExample()
    {
    GetWindow<ChangeEventTestWindow>().titleContent = new GUIContent("Change Event Test Window");
    }

    public void CreateGUI()
    {
    // 建立一個toggle並為其註冊callback
    m_MyToggle = new Toggle("Test Toggle") { name = "My Toggle" };
    m_MyToggle.RegisterValueChangedCallback((evt) => { Debug.Log("Change Event received"); });
    rootVisualElement.Add(m_MyToggle);

    // 建立一個button來更改toggle的值
    Button button01 = new Button() { text = "Toggle" };
    button01.clicked += () =>
    {
    // 這個會觸發Change Event
    m_MyToggle.value = !m_MyToggle.value;
    };
    rootVisualElement.Add(button01);

    // 建立另外一個button,但是使用SetValueWithoutNotify更改toggle的值,
    Button button02 = new Button() { text = "Toggle without notification" };
    button02.clicked += () =>
    {
    // 使用SetValueWithoutNotify()不會觸發Change Event
    m_MyToggle.SetValueWithoutNotify(!m_MyToggle.value);
    };
    rootVisualElement.Add(button02);
    }
    }
  • 觀看結果

Reference: https://docs.unity3d.com/Manual/UIE-Change-Events.html

事件捕捉(Capture events)

事件捕捉(Capture events)通知你滑鼠捕捉狀態(mouse capture state)的變化,在UI Toolkit中的事件有兩捕捉種類型:

  1. 滑鼠捕捉事件(Mouse capture events)
  2. 指標捕捉事件(Pointer capture events)

當一個element捕捉了滑鼠或指標,它是唯一接收來自點擊裝置(Pointing device,如滑鼠)事件的element,直到該裝置釋放或失去捕捉為止。例如:假設你使用滑鼠點擊了一個文字框(text box),這個文字框會捕捉這個滑鼠,此時滑鼠仍然可以移動,但是它不會觸發文字框之外的事件;只要這個文字框正在捕捉你的滑鼠,那麼它就不會觸發其他事件。當你在文字框之外按下滑鼠上的按鈕時,文字框會釋放其滑鼠捕捉。

捕捉滑鼠(Mouse capture)

滑鼠捕捉事件(Mouse capture events)是實體滑鼠或是模擬滑鼠的虛擬滑鼠上的事件。捕捉到滑鼠時,也會產生一個滑鼠指標的PointerCaptureEvent。當一個element釋放捕捉滑鼠時,會觸發相應的MouseCaptureOutEvent

注意:不會發生同時有兩個elements同時捕捉滑鼠的情況,如果另外一個Visual Element觸發了MouseCaptureEvent那麼原先捕捉滑鼠的Element就會釋滑鼠並收到一個MouseCaptureOutEvent

捕捉指標(Pointer capture)

在UI Toolkit中,指標事件優先於滑鼠事件。如果指標的類型是滑鼠的話,在捕捉到指標事件時也會觸發滑鼠事件。

Event 描述 Trickles down Bubbles up Cancellable
MouseCaptureEvent 當某個Element被滑鼠捕捉時會發送這個事件,此時target為這個Element
MouseCaptureOutEvent 當某個Element捕捉的滑鼠被釋放或是某些其他原因是放時,會發送這個事件,此時target為這個失去滑鼠捕捉的Element
PointerCaptureEvent 當某個Element捕捉指標時會發送這個事件,此時target為這個Element
PointerCaptureOutEvent 當某個Element捕捉的指標被釋放時,會發送這個事件,此時target為這個失去指標捕捉的Element
例子

以下例子為示範捕捉與釋放的行為

  1. Assets > Scripts > Editor 下建立一個 C# Script CaptureEventsTestWindow.cs

  2. 將以下程式碼複製到剛剛建立的C# Script中

    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
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.UIElements;

    public class CaptureEventsTestWindow : EditorWindow
    {
    // 這個Attributes會在 Windows -> UI Tollkit 下加入一個 Capture Events Test Window
    [MenuItem("Window/UI Toolkit/Capture Events Test Window")]
    public static void ShowExample()
    {
    var wnd = GetWindow<CaptureEventsTestWindow>();
    wnd.titleContent = new GUIContent("Capture Events Test Window");
    }

    private bool m_IsCapturing = false;

    public void CreateGUI()
    {
    for (int i = 0; i < 4; i++)
    {
    // 建立一個 Label 的Visual Element
    Label clickableLabel = new Label($"Label {i} - Click Me!");
    // 為這個Label Element註冊 MouseDownEvent
    clickableLabel.RegisterCallback<MouseDownEvent>((evt) => {
    // 這個匿名Callback會在Console中印出 Clicked on label *** 的字串
    Debug.Log($"Clicked on label '{(evt.target as Label).text}'");
    });
    rootVisualElement.Add(clickableLabel);
    }

    // 建立一個 Label 的Visual Element
    Label capturingLabel = new Label("Click here to capture mouse");
    // 為這個Label Element註冊 MouseDownEvent
    capturingLabel.RegisterCallback<MouseDownEvent>((evt) =>
    {
    if (!m_IsCapturing)
    {
    capturingLabel.text = "Click here to release mouse";
    // 捕捉滑鼠
    MouseCaptureController.CaptureMouse(capturingLabel);
    m_IsCapturing = true;
    }
    else
    {
    capturingLabel.text = "Click here to capture mouse";
    // 釋放捕捉
    MouseCaptureController.ReleaseMouse(capturingLabel);
    m_IsCapturing = false;
    }
    });
    rootVisualElement.Add(capturingLabel);

    // 註冊捕捉滑鼠事件
    rootVisualElement.RegisterCallback<MouseCaptureEvent>((evt) =>
    {
    Debug.Log("Mouse captured");
    });

    // 註冊滑鼠釋放捕捉事件
    rootVisualElement.RegisterCallback<MouseCaptureOutEvent>((evt) =>
    {
    Debug.Log("Mouse captured released");
    });
    }
    }
  3. 在Unity編輯器中找到 Window > UI Toolkit > Capture Events Test Window

  4. 點擊Test Window上面的Label來觀看結果

Reference: https://docs.unity3d.com/Manual/UIE-Change-Events.html

合成並發送事件(Synthesize and send events)

事件系統使用一個事件池(pool of events),以避免重複分配事件物件(event object)。要合成並發送自己的事件需要:

  1. 建立一個UnityEngine.Event用來持有初始化的資料
  2. 從事件池中獲取一個事件物件。
  3. 填寫事件的屬性(event properties)。
  4. 將事件封裝在using區塊中,以確保它被返回到事件池。
  5. 將事件傳遞給panel.visualTree.SendEvent()

下面的例子中會發送一個KeyDownEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void SynthesizeAndSendKeyDownEvent(IPanel panel, KeyCode code,
char character = '\0', EventModifiers modifiers = EventModifiers.None)
{
// 建立一個UnityEngine.Event用來持有初始化的資料
var evt = new Event() {
type = EventType.KeyDownEvent,
keyCode = code,
character = character,
modifiers = modifiers
};

// 將事件封裝在`using`區塊中,以確保它被返回到事件池
using (KeyDownEvent keyDownEvent = KeyDownEvent.GetPooled(evt))
{
panel.visualTree.SendEvent(keyDownEvent);
}
}

注意:

  1. 不能發送不是來自OS(operating system)的事件,
  2. 不能發送不在UnityEngine.Event types中的事件
  3. 某些事件是由UI Toolkit作為對內部狀態變化的反應而發送的,不應由外部過程發送。例如,如果您發送PointerCaptureEvent,visual elements會假定該事件的底層條件已滿足,並且不會為它們設置指針捕獲(pointer capture)。這可能會破壞visual elements的內部配置(configurations)並導致未定義的行為。

Event reference

Reference: https://docs.unity3d.com/Manual/UIE-Events-Synthesizing.html

Manipulators

Manipulators是處理使用者與UI Element交互的State machine,它被用來儲存,註冊,取消註冊event callbacks

建立與使用Manipulators

你不需要自己撰寫管理callback的class,你只需要要繼承UI Toolkit提供的manipulators class就可以使用它們來管理callback與event,步驟如下:

  1. 建立一個class,這個class封裝了針對特定使用者互動所需的事件處理邏輯,並繼承了UI Toolkit提供的manipulators class。
  2. 在這個class中,實作方法(method)來回應相關的互動,例如滑鼠點擊或拖曳。這些方法捕獲並處理必要的資訊以執行這個互動行為(behavior)。
  3. 當你完成設計這個class之後,你就可以實體化它並將其附加到目標(target) UI Element上。這個附加使這個Manipulator class可以攔截和管理指定的事件,並協調使用者互動,同時與你的UI程式碼保持清晰的分離。
  4. 在Visual Element上使用AddManipulator將建立的Manipulator加入到這個Element
  5. 在Visual Element上使用RemoveManipulator為這個Element移除指定的的Manipulator

以下是UI Toolkit提供的Manipulator class
Manipulator 繼承自 描述
Manipulator 所有manipulators的基類(Base class)
KeyboardNavigationManipulator Manipulator 將特定設備輸入事件(device-specific input event)轉換為可以使用鍵盤進行的較高階導航操作(higher-level navigation operations)
MouseManipulator Manipulator 處理滑鼠輸入,擁有一個啟用過濾器(ManipulatorActivationFilter)列表
ContextualMenuManipulator MouseManipulator 當使用者按下滑鼠右鍵或是menu鍵時,顯示內容選單(contextual menu)
PointerManipulator MouseManipulator 處理指標(pointer)輸入,擁有一個啟用過濾器(ManipulatorActivationFilter)列表
Clickable PointerManipulator 追蹤element上的滑鼠事件並辨認是否發生點擊事件,也就是說在同一個element中是否有發生指標按下放開

例子

以下範例將示範

  • 如何一個繼承PointerManipulator,來處理滑鼠輸入,
  • 並使用activators list屬性來設定可以啟用這個manipulator的條件,
    • 例如:當使用者點擊滑鼠左鍵時,啟用這個manipulator
      • 要做到這個,你只要實體化一個ManipulatorActivationFilter,將其button屬性設為MouseButton.LeftMouse並加入到activators list即可
  • 使用target屬性來存取附加這個manipulator的element,
  • Override RegisterCallbacksOnTargetUnregisterCallbacksFromTarget方法用以註冊和取消註冊event callbacks。
建立一個可以讓你拖動element的manipulator
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
using UnityEngine;
using UnityEngine.UIElements;

// 繼承PointerManipulator
public class ExampleDragger : PointerManipulator
{
private Vector3 m_Start;
protected bool m_Active;
private int m_PointerId;
private Vector2 m_StartSize;

public ExampleDragger()
{
m_PointerId = -1;
// 實體化一個`ManipulatorActivationFilter`,將其button屬性設為`MouseButton.LeftMouse`並加入到`activators` list
activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse });
m_Active = false;
}

// Override RegisterCallbacksOnTarget方法用以註冊event callbacks
protected override void RegisterCallbacksOnTarget()
{
target.RegisterCallback<PointerDownEvent>(OnPointerDown);
target.RegisterCallback<PointerMoveEvent>(OnPointerMove);
target.RegisterCallback<PointerUpEvent>(OnPointerUp);
}

// Override UnregisterCallbacksFromTarget方法用以取消註冊event callbacks
protected override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback<PointerDownEvent>(OnPointerDown);
target.UnregisterCallback<PointerMoveEvent>(OnPointerMove);
target.UnregisterCallback<PointerUpEvent>(OnPointerUp);
}

protected void OnPointerDown(PointerDownEvent e)
{
if (m_Active)
{
e.StopImmediatePropagation();
return;
}

if (CanStartManipulation(e))
{
m_Start = e.localPosition;
m_PointerId = e.pointerId;

m_Active = true;
target.CapturePointer(m_PointerId);
e.StopPropagation();
}
}

protected void OnPointerMove(PointerMoveEvent e)
{
if (!m_Active || !target.HasPointerCapture(m_PointerId))
return;

Vector2 diff = e.localPosition - m_Start;

target.style.top = target.layout.y + diff.y;
target.style.left = target.layout.x + diff.x;

e.StopPropagation();
}

protected void OnPointerUp(PointerUpEvent e)
{
if (!m_Active || !target.HasPointerCapture(m_PointerId) || !CanStopManipulation(e))
return;

m_Active = false;
target.ReleaseMouse();
e.StopPropagation();
}
}
建立一個可以當拖動時,改變element大小的manipulator
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
using UnityEngine;
using UnityEngine.UIElements;

public class ExampleResizer : PointerManipulator
{
private Vector3 m_Start;
protected bool m_Active;
private int m_PointerId;
private Vector2 m_StartSize;
public ExampleResizer()
{
m_PointerId = -1;
activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse });
m_Active = false;
}

protected override void RegisterCallbacksOnTarget()
{
target.RegisterCallback<PointerDownEvent>(OnPointerDown);
target.RegisterCallback<PointerMoveEvent>(OnPointerMove);
target.RegisterCallback<PointerUpEvent>(OnPointerUp);
}

protected override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback<PointerDownEvent>(OnPointerDown);
target.UnregisterCallback<PointerMoveEvent>(OnPointerMove);
target.UnregisterCallback<PointerUpEvent>(OnPointerUp);
}

protected void OnPointerDown(PointerDownEvent e)
{
if (m_Active)
{
e.StopImmediatePropagation();
return;
}

if (CanStartManipulation(e))
{
m_Start = e.localPosition;
m_StartSize = target.layout.size;
m_PointerId = e.pointerId;
m_Active = true;
target.CapturePointer(m_PointerId);
e.StopPropagation();
}
}

protected void OnPointerMove(PointerMoveEvent e)
{
if (!m_Active || !target.HasPointerCapture(m_PointerId))
return;

Vector2 diff = e.localPosition - m_Start;

target.style.height = m_StartSize.y + diff.y;
target.style.width = m_StartSize.x + diff.x;

e.StopPropagation();
}

protected void OnPointerUp(PointerUpEvent e)
{
if (!m_Active || !target.HasPointerCapture(m_PointerId) || !CanStopManipulation(e))
return;

m_Active = false;
target.ReleasePointer(m_PointerId);
m_PointerId = -1;
e.StopPropagation();
}
}
加入manipulator
1
2
3
4
var myElement = new VisualElement();

// 加入manipulator到這個VisualElement
myElement.AddManipulator(new ExampleDragger());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var box = new VisualElement()
{
style =
{
left = 100,
top = 100,
width = 100,
height = 100,
backgroundColor = Color.red
},
pickingMode = PickingMode.Position,
};

box.AddManipulator(new ExampleResizer());
移除manipulator
1
2
// 為這個VisualElement移除manipulator
myElement.RemoveManipulator<ExampleDragger>();

分派事件(Dispatch event)

事件分派器(Event Dispacther)

UI Toolkit有一個Event System它負責監聽事件,這些事件可能來自OS或是Scripts,當事件發生時,EventDispatcher會使用最適合的分派策略(appropriate dispatching strategy)將事件分派給對應的visual elements。

Visual elements對於一些預設行為以實作多個事件,這些事件在建立,執行時可能會產生一些額外的事件,例如:MouseMoveEvent會連帶產生一個MouseEnterEvent與一個MouseLeaveEvent這些事件會被放入一個Queue中,當目前的事件處理完之後在依序處理。(MouseMoveEvent處理完之後才處理連帶產生MouseEnterEventMouseLeaveEvent)

分派行為(Dispatch Behavior)

每種Event類型都有它自己的分派行為(dispatch behavior),這些行為(behavior)可以被分為三個階段(stage):

  1. Trickles down: 事件會在這個Trickles down階段發送給element
  2. Bubbles up: 事件會在這個Bubble-up階段發送給element
  3. Cancellable: 事件取消(execution cancelled),停止(stopped)或是阻止(prevented)自己原先的動作。

事件傳播(Event propagation)

事件分派器(event dispatcher)在選好事件目標(event target)之後,就會開始計算這個事件的傳播路徑(propagation path),傳播路徑是一個list,這個list為會收到該事件的Visual elements組成,且這些Elements是經過排序的。傳播路徑發生的順序:

  1. trickle-down phase:路徑從visual element treeroot visual element開始往下延伸(descends),直到找到事件目標(event target),這個過程稱為trickle-down phase
  2. 事件目標(event target)接收到事件
  3. bubble-up phase:事件沿著tree往上延伸(ascends)直到root element,這個過程稱為bubble-up phase

事件傳播時,會根據不同的type有不同的行為,

  • 例如有些event types會忽略bubble-up phase,有些event types只會發送給event target。
  • 此外,假設有個element被隱藏(hide)或是(停用)disable,那麼這個element就不會收到事件,但是祖先(ancestors)與子孫(descendants)傳播

事件目標(Event Target)

事件的目標是根據event type而定,例如滑鼠事件(mouse event)它的目標一般來說都是最上層被點擊的element;而鍵盤事件(keyboard event)它的目標則是有焦點(focus)的element。

  • Event.target:UI Toolkit的events都有一個target屬性,它持有事件發生element的參考(reference),此外這個Event.target在分派處理(dispatch process)時是不會改變的。
  • Event.currentTargetEvent.currentTarget存放目前正在處理event的visual element

滑鼠事件

PickingMode:大部分的mouse event會使用picking mode來決定它們的target,VisualElement class有一個pickingMode屬性來做這件事,其中

  • PickingMode.Position (預設): 根據位置矩形(position rectangle)來挑選
  • PickingMode.Ignore: 防止被mouse event選到
  • 此外,你可以override VisualElement.ContainsPoint()來自定挑選邏輯

Capture events:在MouseDownEvent之後,一些elements必須捕獲指標位置(pointer position),以確保它接收到所有後續的滑鼠事件,即使游標不再懸停(hovering over)在該element上。例如,當你點擊button、slider或scroll bar時。

  • element.CaptureMouse()或是MouseCaptureController.CaptureMouse()可以獲取滑鼠。
  • MouseCaptureController.ReleaseMouse()會釋放滑鼠。
  • 當某個element持有滑鼠時,另外一個element呼叫CaptureMouse()的話,原先持有滑鼠的element會收一個MouseCaptureOutEvent並失去滑鼠
    • 應用程式中一次只能有一個element捕獲滑鼠。當一個element捕獲了滑鼠時,除了滑鼠滾輪事件(mouse wheel event)之外,它將成為所有後續滑鼠事件的target。(這僅適用於那些尚未設定target且依賴於分派過程來確定targer的滑鼠事件。)

Focus Order:每個UI Toolkit panel都會有一個 focus ring,用來決定elements的focus order,預設是使用depth-first search(DFS)來決定

  • 下圖中的focus order為F,B,A,D,C,E,G,I,H
  • 有些events會使用focus order來決定來決定哪些element持有focus,例如鍵盤事件(keyboard event)的目標(target)就是具有焦點(focus)的element
  • focusable屬性決定一個visual element是否為可聚焦的(focusable)。預設中,VisualElement不是focusable的,但它的一些子類(subclass)如TextField預設是focusable。
  • tabIndex屬性可以控制focus order,
    • 預設tabIndex為零,
    • 如果tabIndex為負數,則無法使用tab鍵來聚焦在該element上。
    • 如果tabIndex為零,則由focus ring algorithm決定。
    • 如果tabIndex為正數,則比該element小的element會先被聚焦,之後才是該element。

Reference:https://docs.unity3d.com/Manual/UIE-Events-Dispatching.html

Flexbox Layout

UI Toolkit使用一個開源的Layout Engine: Yoga,Yoga是一個實作了Flexbox layout的HTML/CSS Layout system。

預設所有Visual Elements都是layout的一部份,在Layout中預設有以下行為

  • Container會將它的Children做垂直(vertically)分佈
  • Container矩形(rectangle)的位置包括其children rectangles
  • 在計算尺寸(size calculation)時,若這個visual element有Text的話,會以這個Text的大小來計算

Flexbox基本

  • Flex Item:在Flexbox中會有一個含有多個元素的Container稱為Flex Container,在這個Flex Container中的element稱為Flex Item

  • Flexbox的layout使用flex-flow directions的方式來做佈局,也就是說裡面的items會根據一個軸來從開頭到結尾做排列,如main axis的話從main-startmain-end排列;或cross axis的話從cross-startcross-end排列。

  • Main axis:由main-startmain-end的方向。

    • 預設是由左到右的方向與大部分語言文字方向相同的方向
    • 注意:Main axis不一定是由左到右的方向,它會根據flex-direction改變。
  • Cross axis:由cross-startcross-end的方向,也就是換行方向,如果Flex Item單行的數量多到超過Flex Container,需要換行時,預設會換到下一行的方向。

    • 預設是由上到下
  • Flex direction:設定main axis的方向,有以下方向可以設置

    • row (預設): 在LRT佈局中是從左到右;在RTL佈局中是從右到左
    • row-reverse: 與row相反,在LRT佈局中是從右到左;在RTL佈局中是從左到右
    • column: 方向是從上到下
    • column-reverse: 與column相反,從下到上
    • 以下是flex-direction為row的範例
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      <style>
      .container {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      }
      .item{
      margin: 10px;
      width: 60px;
      height: 60px;
      background-color: blue;
      color: white;
      }
      </style>
      <div class="container1">
      <div class="item">item1</div>
      <div class="item">item2</div>
      <div class="item">item3</div>
      </div>
      item1
      item2
      item3

  • flex-wrap:設定Flex是否換行,預設會讓flex items保持在一行。

    • nowrap:所有flex items保持在一行
    • wrap:當flex items多到容不下時,會換到下一行,方向是從上到下
    • wrap-reversewrap的相反,當flex items多到容不下時,會換到上一行,方向是從下到上
    • 以下是flex-wrap的的範例

      nowrap

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <style>
      .container2-1 {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      width:200px;
      flex-wrap: nowrap;
      }
      </style>
      <div class="container2-1">
      <div class="item red">1</div>
      <div class="item green">2</div>
      <div class="item blue">3</div>
      </div>
      1
      2
      3

      wrap

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <style>
      .container2-2 {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      width:200px;
      flex-wrap: wrap;
      }
      </style>
      <div class="container2-2">
      <div class="item red">1</div>
      <div class="item green">2</div>
      <div class="item blue">3</div>
      </div>
      1
      2
      3

      wrap-reverse

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <style>
      .container2-3 {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      width:200px;
      flex-wrap: wrap-reverse;
      }
      </style>
      <div class="container2-3">
      <div class="item red">1</div>
      <div class="item green">2</div>
      <div class="item blue">3</div>
      </div>
      1
      2
      3



  • justify-content:定義Flex Items在Flex Container在main axis上空間分配的方式

    • flex-start:Items以flex-direction的開始爲方向排列。
    • flex-end:Items以flex-direction的尾部為方向排列。
    • center:Items在中心排列
    • space-between:Items在main axis上平均排列,但首尾Items貼邊。
    • space-around:Items在main axis上平均排列,Items之間使用相同的空間
    • 以下是Justify Content的例子

      flex-start

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      <style>
      .container3-1 {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      justify-content: flex-start;
      }
      </style>
      <div class="container3-1">
      <div class="item red">1</div>
      <div class="item green">2</div>
      <div class="item blue">3</div>
      </div>
      1
      2
      3

      flex-end

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      <style>
      .container3-2 {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      justify-content: flex-end;
      }
      </style>
      <div class="container3-2">
      <div class="item red">1</div>
      <div class="item green">2</div>
      <div class="item blue">3</div>
      </div>
      1
      2
      3

      center

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      .container3-3 {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      justify-content: center;
      }
      <div class="container3-3">
      <div class="item red">1</div>
      <div class="item green">2</div>
      <div class="item blue">3</div>
      </div>
      1
      2
      3

      space-between

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      .container3-4 {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      justify-content: space-between;
      }
      <div class="container3-4">
      <div class="item red">1</div>
      <div class="item green">2</div>
      <div class="item blue">3</div>
      </div>
      1
      2
      3

      space-around

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      <style>
      .container3-5 {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      justify-content: space-around;
      }
      </style>
      <div class="container3-5">
      <div class="item red">1</div>
      <div class="item green">2</div>
      <div class="item blue">3</div>
      </div>
      1
      2
      3



  • align-items:設定Flex Items對cross axis方向的排列方式

    • flex-start:Items會從cross axis的開始位置對齊
    • flex-end:Items會從cross axis的尾端位置對齊
    • stretch:當Items沒有設定高度時,Items會被拉伸填滿Container,(遵守最小寬度(min-width)/最大寬度(max-width))
    • center:Items會從cross axis的中心位置對齊
    • 以下是align-items的例子

      flex-start

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <style>
      .container4-1 {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      height: 200px;
      align-items: flex-start;
      }
      </style>
      <div class="container4-1">
      <div class="item red">1</div>
      <div class="item green">2</div>
      <div class="item blue">3</div>
      </div>
      1
      2
      3

      flex-end

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <style>
      .container4-2 {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      height: 200px;
      align-items: flex-end;
      }
      </style>
      <div class="container4-2">
      <div class="item red">1</div>
      <div class="item green">2</div>
      <div class="item blue">3</div>
      </div>
      1
      2
      3

      stretch

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <style>
      .container4-3 {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      height: 200px;
      align-items: stretch;
      }
      </style>
      <div class="container4-3">
      <div style="background-color:coral;min-height:30px;width:60px;">1</div>
      <div style="background-color:lightblue;min-height:50px;width:60px;">2</div>
      <div style="background-color:lightgreen;min-height:190px;width:60px;">3</div>
      </div>
      1
      2
      3

      center

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <style>
      .container4-4 {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      height: 200px;
      align-items: center;
      }
      <div class="container4-4">
      <div class="item red">1</div>
      <div class="item green">2</div>
      <div class="item blue">3</div>
      </div>
      </style>
      1
      2
      3

  • align-self:與align-items類似,但是只會應用到該Item上

    • flex-start:Item會從cross axis的開始位置對齊
    • flex-end:Item會從cross axis的尾端位置對齊
    • stretch:Item會被拉伸填滿Container,(遵守最小寬度(min-width)/最大寬度(min-width))
    • center:Item會從cross axis的中心位置對齊
  • flex-grow:設定item在容器中佔多少空間,或是分到多少剩餘空間。

    • 以下舉例,容器的width為600px,紅色Item1的width為60px,綠色Item2的width為90px,藍色Item3的width為60px:
      沒有設定 flex-grow
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <style>
      .container5 {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      width: 600px;
      color: white;
      }
      </style>
      <div class="container5">
      <div class="red" style="width:60px;">1(60)</div>
      <div class="green" style="width:90px;">2(90)</div>
      <div class="blue" style="width:60px;">3(60)</div>
      </div>
      1(60)
      2(90)
      3(60)

      紅色Item的flex-grow:1,
      剩餘的空間為600-60-90-60=390,這會把剩餘的空間(390)全部分紅色Item
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <style>
      .container5 {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      width: 600px;
      color: white;
      }
      </style>
      <div class="container5">
      <div class="red item-a-5-1" style="width:60px;">1(60px+390px)</div>
      <div class="green" style="width:90px;">2(90px)</div>
      <div class="blue" style="width:60px;">3(60px)</div>
      </div>
      1(60px+390px)
      2(90px)
      3(60px)

      紅色Item的flex-grow:1,
      綠色Item的flex-grow:2,
      他們兩個會按比例分配剩餘空間390/3=130,紅色Item分配到130*1,綠色Item分到130*2
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <style>
      .container5 {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      width: 600px;
      color: white;
      }
      </style>
      <div class="container5">
      <div class="red item-a-5-1" style="width:60px;">1(60+130*1)</div>
      <div class="green item-a-5-2" style="width:90px;">2(90+130*2)</div>
      <div class="blue" style="width:60px;">3(60)</div>
      </div>
      1(60+130*1)
      2(90+130*2)
      3(60)

      flex-grow的數值也可以是小數,
      紅色Item的flex-grow:0.4,
      綠色Item的flex-grow:0.6,
      他們兩個按比例分配剩餘空間,紅色Item分配到390*0.4,綠色Item分到390*0.6
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <style>
      .container5 {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      width: 600px;
      color: white;
      }
      </style>
      <div class="container5">
      <div class="red" style="width:60px; flex-grow:0.4;">1(60+390*0.4)</div>
      <div class="green" style="width:90px; flex-grow:0.6;">2(90+390*0.6)</div>
      <div class="blue" style="width:60px;">3(60)</div>
      </div>
      1(60+390*0.4)
      2(90+390*0.6)
      3(60)

  • flex-shrink:在容器空間不足時,控制Item如何收縮

    • 此方式計算較複雜,除了flex-shrink設定比例,還需要根據容器中Item超出多少寬度,以及Item它本身的寬度來計算出各個Item收縮的比例。
    • 每個Item收縮的權重為其flex-shrink*寬度
    • 如果不希望被自動壓縮到,可以設定為0
    • 以下舉例,容器的width為600px,紅色Item1的width為300px,綠色Item2的width為300px,藍色Item3的width為300px,紅色Item的flex-shrink:1,綠色Item的flex-shrink:1,藍色Item的flex-shrink:2
      計算超出多少寬度:600(容器) - 300(紅色) - 300(綠色) - 300(藍色) = 300 (超出300)
      每個Item收縮的權重為其flex-shrink * 寬度:1*300 + 1*300 + 2*300 = 1200
      每個Item要收縮的寬度為:
      紅色:300(超出的寬度) * 1 (flex-shrink) * 300(Item的寬度) / 1200(權重) = 75
      綠色:300(超出的寬度) * 1 (flex-shrink) * 300(Item的寬度) / 1200(權重) = 75
      藍色:300(超出的寬度) * 2 (flex-shrink) * 300(Item的寬度) / 1200(權重) = 150
      因此三個元素最終的寬度為:
      紅色:300 - 75 = 225
      綠色:300 - 75 = 225
      藍色:300 - 150 = 150
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <style>
      .container6 {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      width: 600px;
      color: white;
      }
      </style>
      <div class="container6">
      <div class="red" style="width:300px; flex-shrink:1;">225</div>
      <div class="green" style="width:300px; flex-shrink:1;">225</div>
      <div class="blue" style="width:300px; flex-shrink:2;">150</div>
      </div>
      225
      225
      150

    • 再看另外一個例子:容器的width為600px,紅色Item的width為150px,綠色Item的width為300px,藍色Item的width為300px,紅色Item的flex-shrink:0,綠色Item的flex-shrink:2,藍色Item的flex-shrink:3
      計算超出多少寬度:600(容器) - 150(紅色) - 300(綠色) - 300(藍色) = 150 (超出150)
      每個Item收縮的權重為其flex-shrink * 寬度:0*150 + 2*300 + 3*300 = 1500
      每個Item要收縮的寬度為:
      紅色:150(超出的寬度) * 0 (flex-shrink) * 150(Item的寬度) / 1500(權重) = 0
      綠色:150(超出的寬度) * 2 (flex-shrink) * 300(Item的寬度) / 1500(權重) = 60
      藍色:150(超出的寬度) * 3 (flex-shrink) * 300(Item的寬度) / 1500(權重) = 90
      因此三個元素最終的寬度為:
      紅色:150 - 0 = 150
      綠色:300 - 60 = 240
      藍色:300 - 90 = 210
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <style>
      .container6 {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      width: 600px;
      color: white;
      }
      </style>
      <div class="container6 width:600px">
      <div class="red" style="width:150px; flex-shrink:0;">150</div>
      <div class="green" style="width:300px; flex-shrink:2;">240</div>
      <div class="blue" style="width:300px; flex-shrink:3;">210</div>
      </div>
      150
      240
      210

    • flex-shrink也可以是小數:容器的width為600px,紅色Item的width為300px,綠色Item的width為300px,藍色Item的width為300px,紅色Item的flex-shrink:0.1,綠色Item的flex-shrink:0.4,藍色Item的flex-shrink:0.5
      計算超出多少寬度:600(容器) - 300(紅色) - 300(綠色) - 300(藍色) = 300 (超出150)
      每個Item收縮的權重為其flex-shrink * 寬度:0.1*300 + 0.4*300 + 0.5*300 = 300
      每個Item要收縮的寬度為:
      紅色:300(超出的寬度) * 0.1 (flex-shrink) * 300(Item的寬度) / 300(權重) = 30
      綠色:300(超出的寬度) * 0.4 (flex-shrink) * 300(Item的寬度) / 300(權重) = 120
      藍色:300(超出的寬度) * 0.5 (flex-shrink) * 300(Item的寬度) / 300(權重) = 150
      因此三個元素最終的寬度為:
      紅色:300 - 30 = 270
      綠色:300 - 120 = 180
      藍色:300 - 150 = 150
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <style>
      .container6 {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      width: 600px;
      color: white;
      }
      </style>
      <div class="container6 width:600px">
      <div class="red" style="width:300px; flex-shrink:0.1;">270</div>
      <div class="green" style="width:300px; flex-shrink:0.4;">180</div>
      <div class="blue" style="width:300px; flex-shrink:0.5;">150</div>
      </div>
      270
      180
      150


  • flex-basis:設定Item佔用空間。如果沒設定或設為auto則Item的值為width(flex-direction:row)或是height(flex-direction:column)的值。

    • 以下是範例:
      紅色Item的style="width:300px; flex-basis:auto;"。
      綠色Item的style="width:300px; flex-basis:100px;"。
      藍色Item的style="width:300px;"。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <style>
      .container6 {
      display: flex;
      flex-direction: row;
      background-color: yellow;
      width: 600px;
      color: white;
      }
      </style>
      <div class="container6 width:600px">
      <div class="red" style="width:300px; flex-basis:auto;">300</div>
      <div class="green" style="width:300px; flex-basis:100px;">100</div>
      <div class="blue" style="width:300px;">300</div>
      </div>
      300
      100
      300


Reference: https://docs.unity3d.com/2020.1/Documentation/Manual/UIE-LayoutEngine.html
https://discussions.unity.com/t/ui-toolkit-introduction-and-flexbox-layout/316856
https://css-tricks.com/snippets/css/a-guide-to-flexbox/
https://github.com/xieranmaya/blog/issues/9

使用UQuery來查找element

UQuery受到JQueryLinq啟發,UQuery被設計為會限制動態記憶體分配(dynamic memory allocation),讓手機平台上可以擁有最佳化的效能。
可以使用QueryQ(QQuery<T>.First()的縮寫)這兩個extension method來使用UQuery。

  • QQuery實際上是使用UQueryBuilder來建構一個query,這些extension method可以減少在建立UQueryBuilder時需要撰寫的模板程式碼。
  • 在使用UQuery之前,你必須要先載入並實體化UXML,然後才能用QQuery建立選取規則(selection rules)
    • 透過選擇規則返回的elements你還可以使用UQueryBuilder上的FirstLastAtIndexChildrenWhere公開方法(public method)來更進一步的過濾它們。
  • 可以透過element的nameUSS classelement type (C# type)來找到想要的element。
  • 也可以使用predicate或是組合[複雜結構的query](#Complex hierarchical queries)來找到想要的element
  • UQuery會在初始化時Cache這些選出來的Elements。
  • UI Toolkit不會自行銷毀沒用到的visual elements,它是使用C# garbage collector 來處理,因此最好不要在UIDocuments或Window之外建立這些elements的引用,避免讓C# garbage collector無法回收。

以下使用一個UXML來說明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<UXML xmlns="UnityEngine.UIElements">
<VisualElement name="container1">
<Button name="OK" text="OK" />
<Button name="Cancel" text="Cancel" />
</VisualElement>
<VisualElement name="container2">
<Button name="OK" class="yellow" text="OK" />
<Button name="Cancel" text="Cancel" />
</VisualElement>
<VisualElement name="container3">
<Button name="OK" class="yellow" text="OK" />
<Button name="Cancel" class="yellow" text="Cancel" />
</VisualElement>
</UXML>

Query by name

語法為: Query(name: "element-name") 或是 Q(name: "element-name") ;你也可以省略參數名稱name,直接使用Query("element-name")

1
2
// 這句會回傳所有name為OK的element。
List<VisualElement> result = root.Query("OK").ToList();
1
2
3
4
// 這句會回傳找到name為OK的第一個element。
VisualElement result = root.Query("OK").First();
// 你也可以直接寫Q
VisualElement result = root.Q("OK");
1
2
// 這句是選name為OK的第二個element(第一個element的index是0)
VisualElement result3 = root.Query("OK").AtIndex(1);
1
2
// 這句是選name為OK的最後一個element
VisualElement result4 = root.Query("OK").Last();

Query by USS class

語法為:Query(className: "class-name") 或是 Q(className: "class-name")

1
2
// 這句會選出所有class name為yellow的element
List<VisualElement> result = root.Query(className: "yellow").ToList();
1
2
// 這句會選class name為yellow的第一個element
VisualElement result = root.Q(className: "yellow");

Query by element type

語法為:Query<Type>() 或是 Q<Type>()

注意:你只能使用actual type來選出Element,而不能使用base type。

1
2
3
4
// 這句會選出第一個Button element
VisualElement result = root.Q<Button>();
// 將這個button的tooltip更改
result.tooltip = "This is a tooltip!";
1
2
// 這句會選出第三個Button element
VisualElement result = root.Query<Button>().AtIndex(2);

Query with a predicate

除了可以使用nameclass-nametype來選出element以外,還可以搭配Where做更進一步的篩選,Where的參數是一個VisualElement

1
2
// 這句會選出所有class name為yellow的element,然後在找出tooltip為空字串的element
List<VisualElement> result = root.Query(className: "yellow").Where(elem => elem.tooltip == "").ToList();

Complex hierarchical queries

可以將nameclass-nametype等組合在一起,做更複雜的選擇。

1
2
// 這句將name,type,class-name做組合,選出class-name為yellow,name為OK的Button element
VisualElement result = root.Query<Button>(className: "yellow", name: "OK").First();
1
2
// 這句將選出container2中所有name為Cancel的Button element
VisualElement result = root.Query<VisualElement>("container2").Children<Button>("Cancel").First();

UI Builder - 3 (使用C# Script操控UI)

UI-Builder-2-(開始使用UI-Builder)中以建立一個空白的UI,接下來我們要開始為這個UI添加內容。

建立空白UI

  1. 首先需要為這些Element命名,為了方便起見,先將這段UXML複製
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="False">
    <Style src="MainView.uss" />
    <ui:VisualElement name="background">
    <ui:VisualElement name="main-container">
    <ui:ListView focusable="true" name="character-list" />
    <ui:VisualElement name="right-container">
    <ui:VisualElement name="details-container">
    <ui:VisualElement name="details">
    <ui:VisualElement name="character-portrait" />
    </ui:VisualElement>
    <ui:Label text="Label" name="character-name" />
    <ui:Label text="Label" display-tooltip-when-elided="true" name="character-class" />
    </ui:VisualElement>
    </ui:VisualElement>
    </ui:VisualElement>
    </ui:VisualElement>
    </ui:UXML>
  2. 接著在專案中,找到MainView.uxml,滑鼠左鍵點選旁邊的小箭頭,把它點開,會出現會出現inlineStyle
  3. 對著inlineStyle滑鼠左鍵點兩下,就會用IDE開啟MainView.uxml
  4. 接著把上面的UXML內容複製進去,存檔。
  5. 再開啟MainView.uss,把下面這一段內容複製進去,存檔
    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
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    #background {
    flex-grow: 1;
    align-items: center;
    justify-content: center;
    background-color: rgb(115, 37, 38);
    }

    #main-container {
    flex-direction: row;
    height: 350px;
    }

    #character-list {
    width: 230px;
    border-color: rgb(49, 26, 17);
    border-width: 4px;
    background-color: rgb(110, 57, 37);
    border-radius: 15px;
    margin-right: 6px;
    }

    #character-name {
    -unity-font-style: bold;
    font-size: 18px;
    }

    #CharacterClass {
    margin-top: 2px;
    margin-bottom: 8px;
    padding-top: 0;
    padding-bottom: 0;
    }

    #right-container{
    justify-content: space-between;
    align-items: flex-end;
    }

    #details-container{
    align-items: center;
    background-color: rgb(170, 89, 57);
    border-width: 4px;
    border-color: rgb(49, 26, 17);
    border-radius: 15px;
    width: 252px;
    justify-content: center;
    padding: 8px;
    height: 163px;
    }

    #details{
    border-color: rgb(49, 26, 17);
    border-width: 2px;
    height: 120px;
    width: 120px;
    border-radius: 13px;
    padding: 4px;
    background-color: rgb(255, 133, 84);
    }

    #character-portrait{
    flex-grow: 1;
    -unity-background-scale-mode: scale-to-fit;
    }
  6. 接著再打開UI Builder,就可以看到一個空白的UI了。

建立一個List Enrty

  1. 在專案畫面,建立一個ListEntry.uxmlCreate > UI Toolkit > UI Document
  2. 將以下內容貼上
    1
    2
    3
    4
    5
    6
    <ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="False">
    <Style src="ListEntry.uss" />
    <ui:VisualElement name="list-entry">
    <ui:Label text="Label" display-tooltip-when-elided="true" name="character-name" />
    </ui:VisualElement>
    </ui:UXML>
  • 其中<Style src="ListEntry.uss" />會引用等一下建立的ListEntry.uss
  1. 使用類似的方式,Create > UI Toolkit > Style Sheet,建立一個ListEntry.uss,並將以下內容貼上
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #list-entry {
    height: 41px;
    align-items: flex-start;
    justify-content: center;
    padding-left: 10px;
    background-color: rgb(170, 89, 57);
    border-color: rgb(49, 26, 17);
    border-width: 2px;
    border-radius: 15px;
    }

    #character-name {
    -unity-font-style: bold;
    font-size: 18px;
    color: rgb(49, 26, 17);
    }
  2. 可以使用UI Builder打開這個ListEntry.uxml來觀看UI的樣貌

建立 C# Script與其互動

  1. Asset資料夾下面,建立一個Scripts資料夾
  2. 建立一個CharacterData.cs
  3. 將以下內容複製到CharacterData.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using UnityEngine;

public enum ECharacterClass
{
Knight, Ranger, Wizard
}

[CreateAssetMenu] // 這個Attribute會在Create選單中加入這個CharacterData
public class CharacterData : ScriptableObject
{
public string CharacterName;
public ECharacterClass Class;
public Sprite PortraitImage;
}
  1. Asset資料夾下面,建立一個Resources資料夾
  2. Resources資料夾下面,建立一個Characters資料夾
  3. Characters資料夾中,選擇Create > Character Data建立一個ScriptableObject

將UI放到Sence中

  1. Scene選擇GameObject > UI Toolkit > UI Document,建立一個UI Document GameObject
  2. Hierarchy視窗中選擇UIDocument Game Object,將MainView.uxml拖放到右側的Source Asset

建立Controllers

在專案中建立兩個C# controller,與一個MainView

  1. CharacterListEntryController:負責更新ListEntry.uxmlLabel
  2. CharacterListController:負責處理MainView.uxml
  3. MainView繼承MonoBehaviour,主要用來與UIDocument GameObject連結,取得UIDocument Game Object後,把它放入CharacterListController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using UnityEngine.UIElements;

public class CharacterListEntryController
{
Label NameLabel;

// 這個function用來設定VisualElement,讓這個controller可以更改label
public void SetVisualElement(VisualElement visualElement)
{
NameLabel = visualElement.Q<Label>("character-name");
}

// 使用CharacterData設定Label的Text
public void SetCharacterData(CharacterData characterData)
{
NameLabel.text = characterData.CharacterName;
}
}
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;

public class CharacterListController
{
// UXML template for list entries
VisualTreeAsset ListEntryTemplate;

// UI element references
ListView CharacterList;
Label CharClassLabel;
Label CharNameLabel;
VisualElement CharPortrait;

public void InitializeCharacterList(VisualElement root, VisualTreeAsset listElementTemplate)
{
EnumerateAllCharacters();

// Store a reference to the template for the list entries
ListEntryTemplate = listElementTemplate;

// Store a reference to the character list element
CharacterList = root.Q<ListView>("character-list");

// Store references to the selected character info elements
CharClassLabel = root.Q<Label>("character-class");
CharNameLabel = root.Q<Label>("character-name");
CharPortrait = root.Q<VisualElement>("character-portrait");

FillCharacterList();

// Register to get a callback when an item is selected
CharacterList.onSelectionChange += OnCharacterSelected;
}

List<CharacterData> AllCharacters;

void EnumerateAllCharacters()
{
AllCharacters = new List<CharacterData>();
AllCharacters.AddRange(Resources.LoadAll<CharacterData>("Characters"));
}

void FillCharacterList()
{
// Set up a make item function for a list entry
CharacterList.makeItem = () =>
{
// Instantiate the UXML template for the entry
var newListEntry = ListEntryTemplate.Instantiate();

// Instantiate a controller for the data
var newListEntryLogic = new CharacterListEntryController();

// Assign the controller script to the visual element
newListEntry.userData = newListEntryLogic;

// Initialize the controller script
newListEntryLogic.SetVisualElement(newListEntry);

// Return the root of the instantiated visual tree
return newListEntry;
};

// Set up bind function for a specific list entry
CharacterList.bindItem = (item, index) =>
{
(item.userData as CharacterListEntryController).SetCharacterData(AllCharacters[index]);
};

// Set a fixed item height
CharacterList.fixedItemHeight = 45;

// Set the actual item's source list/array
CharacterList.itemsSource = AllCharacters;
}

void OnCharacterSelected(IEnumerable<object> selectedItems)
{
// Get the currently selected item directly from the ListView
var selectedCharacter = CharacterList.selectedItem as CharacterData;

// Handle none-selection (Escape to deselect everything)
if (selectedCharacter == null)
{
// Clear
CharClassLabel.text = "";
CharNameLabel.text = "";
CharPortrait.style.backgroundImage = null;

return;
}

// Fill in character details
CharClassLabel.text = selectedCharacter.Class.ToString();
CharNameLabel.text = selectedCharacter.CharacterName;
CharPortrait.style.backgroundImage = new StyleBackground(selectedCharacter.PortraitImage);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using UnityEngine;
using UnityEngine.UIElements;

public class MainView : MonoBehaviour
{
[SerializeField]
VisualTreeAsset ListEntryTemplate;

void OnEnable()
{
// The UXML is already instantiated by the UIDocument component
var uiDocument = GetComponent<UIDocument>();

// Initialize the character list controller
var characterListController = new CharacterListController();
characterListController.InitializeCharacterList(uiDocument.rootVisualElement, ListEntryTemplate);
}
}
  1. MainView拖入UIDocument GameObject。
  2. ListEntry.uxml拖入ListEntryTemplate

Reference:https://docs.unity3d.com/Manual/UIE-HowTo-CreateRuntimeUI.html


上一篇:UI-Builder-2-(開始使用UI-Builder)