버튼을 눌렀을때 화면의 하단에서 창이 올라오는 UI를 만들어보자. 이런 창을 Bottom Sheet 또는 Action Sheet라고 하는데 이런 UI의 특징은 기존 UI 레이아웃을 건들지 않고 그 위에 오버레이되는 방식으로 그려진다. 이러한 Bottom Sheet를 만들기 위해서 그룹 전체의 부모를 만들고 그 아래 가림막, 그 위에 창을 만든다.

모든 창을 덮는 부모를 만들기 위해 Visual Element를 넣고 100% * 100%로 하면 아래와 같이 창이 밀어 올라가지면서 깨진다.

그 이유가 저번에 설명했던 Flex와 Align 때문인데, UI의 포지션을 바꿔도 레이아웃에서 차지하고 있는 공간은 남아있는데, 이는 레이아웃의 Flex와 Align 때문이다.

UGUI에서는 이러한 레이아웃의 영향을 피하기 위해 Layout Element를 추가해수 Ignore Layout을 체크해주었었다.UI Tool Kit에서도 레이아웃의 영향을 피할 수 있는데 Position > Position을 Absolute로 해주면된다.

이러한 Absolute Positioning을 사용하여 상하좌우 값을 설정하면 레이아웃의 영향을 받지 않고 원하는 곳에 오버레이로 UI를 그릴 수 있다.

즉, 상대적, 절대적이라는 이름이라 직관성은 없지만 Position이 레이아웃의 영향을 받는지 말지를 결정해준다고 생각하면 된다.

Shrink와 Grow를 다시 한 번 알아보자.
Grow : 부모 요소의 넓이보다 자식 요소들의 모든 넓이가 적을 때 사용할 수 있다. 즉, 자식들을 모두 넣었는데도 넓이가 남으면 입력한 숫자의 비율에 맞게 나눠 가진다. 모두 같은 숫자면 당연히 모두 같은 넓이로 확장시켜 채워진다.

Shrink : Grow와 반대 개념으로 부모 요소의 넓이보다 자식 요소들의 모든 넓이의 합이 더 클 때 자식들을 축소시켜서 부모 요소 안에 모두 넣어준다.

즉, 자식이 자동으로 아이템 너비가 확대되거나 축소되지 않도록 하려면 반드시, Shrink와 Grow를 0으로 설정해야한다.
최근 추세를 보면 16:9를 표준 해상도로 부르기 어려울 정도로 다양한 해상도가 출시되고 거기에 대응해야하는 상황인데 웹 기술 기반의 UI Tool Kit의 이러한 기능을 적절히 사용한다면 웹의 반응형 레이아웃과 같이 여러 해상도에 훨씬 유연하게 대처할 수 있을 것이다.
아까 만든 VisualElement의 Position을 Absolute로 해주고 이름을 적절히 설정해준다. 아까와는 달리 VisualElement가 레이아웃의 영향을 받지 않고 모든 창을 덮는다.

그런 다음 Visual Element를 하나 추가해서 가림막으로 사용하자.

그런 다음 Bottom-Sheet 역할을 해줄 Visual Element를 하나 추가해준다.

이제 적절히 UI 요소를 추가하고 기존에 만들었던 Label의 스타일을 클래스로 만들어서 Label에 공통된 스타일이 적용되도록 한다.
Unity의 공식문서에 따르면 uxml의 재사용성과 성능 상의 이유로 인라인 스타일 대신 스타일 시트의 사용을 권장하고 있다.

스타일 클라스를 그대로 적용시키면 같은 폰트, 사이즈가 적용된다.

이후에 Align과 Padding을 적절히 설정해주고 텍스트를 레이블에 입력해준다.

그 다음 Bottom-Sheet를 닫을 닫기 버튼을 추가해준다. 이 때 레이아웃의 영향을 안 받게 하기 위해 Absolute로 해주고 Position > Right를 50으로 해준다. 적절한 Image를 넣고 Background, Border를 잘 설정해주면 아래와 같이 표현된다.

그 다음 UI 요소들을 스크립트 작업에 쓰기위해 적절한 이름을 넣어준다.
원래 바텀시트를 만들 때는 uxml을 하나 더 만들어서 바텀시트를 새롭게 만들고 필요할 때마다 그 바텀 시트를 불러오는 것이 재사용성, 관리, 성능 면에서 유리하다.
먼저 버튼을 눌렀을때 바텀 그룹(Container_Bottom)이 나타나도록 해보자. 먼저 Container_Bottom의 Display > Opacity로 투명도를 조절할 수 있다. 기본에 Canvas Group을 넣어서 alpha 값을 조절해 투명도를 조절했던 것과 비슷하다. 이 때 Opacity 값은 자식에게도 동일하게 적용된다. 실제로 Preview 해보면 버튼이 동작을 하지 않는다. 왜냐하면 보이지만 않을뿐 그 자리에 그대로 있으면서 마우스의 입력을 방해하고 있기 때문이다. 따라서 아예 바텀 시트를 숨기기 위해서는 Opacity가 아니라 그 하단의 Display 속성을 사용하면 된다.

Display를 비활성화하면 화면에서 숨겨짐과 동시에 버튼 입력을 방해하지 않는다. Display 속성이 실제로 레이아웃 상에서 Visual Element를 사라지게 만드는 역할을 하기 때문이다. 실제로 Display를 끄면 레이아웃 자체가 변하게 된다. UGUI에서 게임 오브젝트를 비활성화 할 때와 같은 느낌이다.

visibility를 hidden으로 해도 보이지 않고, 입력도 가로막지 않는 상태를 만들 수도 있다.
이제 C# 스크립트로 UI 요소를 제어해보자.
스크립트에서 UI 상의 특정 UI 엘리멘트 요소를 찾아서 접근할 때에는 UQuery라는 함수를 사용한다.
루트.Q<타입>("이름")

이 때 UQuery를 쓰기 위해서는 UI 계층 구조(Visual Tree)의 최상위 노드(Root Element)를 알아야한다.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
public class UIController : MonoBehaviour
{
// Bottom - Sheet
private VisualElement _bottomContainer;
private Button _openButton;
private Button _closeButton;
private void Start() {
// Find Root-Element
var root = GetComponent<UIDocument>().rootVisualElement;
// UQuery
_bottomContainer = root.Q<VisualElement>("Container_Bottom");
_openButton = root.Q<Button>("Button_Open");
_closeButton = root.Q<Button>("Button_Close");
// Hide bottom-sheet
_bottomContainer.style.display = DisplayStyle.None;
}
}
위와 같이 작성해서 연결할 수 있다. 이제 로직을 구현해보자.
콜백 함수 연결은 아래와 같이 해줄 수 있다.
버튼.RegisterCallback<이벤트타입>(할일)
private void Start() {
// Find Root-Element
var root = GetComponent<UIDocument>().rootVisualElement;
// UQuery
_bottomContainer = root.Q<VisualElement>("Container_Bottom");
_openButton = root.Q<Button>("Button_Open");
_closeButton = root.Q<Button>("Button_Close");
// Hide bottom-sheet
_bottomContainer.style.display = DisplayStyle.None;
// Button Callback
_openButton.RegisterCallback<ClickEvent>(OnOpenButtonClicked);
_closeButton.RegisterCallback<ClickEvent>(OnCloseButtonClicked);
}
private void OnOpenButtonClicked(ClickEvent evt) {
// Open Bottom-Sheet
_bottomContainer.style.display = DisplayStyle.Flex;
}
private void OnCloseButtonClicked(ClickEvent evt) {
// Close Bottom-Sheet
_bottomContainer.style.display = DisplayStyle.None;
}
이제 버튼 클릭으로 바텀 시트를 열고 닫을 수 있다.
밑에서 위로 올라오는 트랜잭션을 추가해보자. 기본 위치를 화면의 아래로 옮겨주고 위로 올라오도록 해준다. 이를 Position으로도 할 수 있지만 Transform > Translate 속성으로도 조절이 가능하다.

유니티에서는 정적인 요소는 Position의 상하좌우 값을 사용하고 애니메이션 같은 동적인 상황에서는 레이아웃 재계산에 포함되지 않는 Transform의 사용을 권장하고 있다.

Translate의 y를 100%로 하면 자기 높이 만큼 하단으로 이동한다.

이러한 픽셀 단위가 아닌 % 단위의 활용은 해상도가 바뀌어도 어느정도 대응가능하게 해준다.
지난번에는 Pseudo 클래스를 만들어서 상태에 따른 스타일을 만들어두고 트랜지션 애니메이션으로 부드럽게 연결해줬다. 차이점은 버튼에서는 Pseudo 클래스만 사용하면 마우스 포인터에 따른 상태 변화를 자동으로 처리해줬지만, 일반 VisualElement에서는 상태 변화를 스크립트로 제어한다.
일단 화면 안에 바텀 시트가 올라온 상태를 만들어주자. 일단 내려가있는 Buttom Sheet에 Duration과 Easing을 적용해준다.

현재 스타일을 추출해주고 Transform > Translate > y = 0%를 입력하고 다른 클래스로 저장해준다.

그런 다음 bottomsheet--up 스타일 클래스를 리스트에서 지워주면 하단으로 내려가는 애니메이션이 실행된다. 즉, 스타일을 추가하고 지워지는 과정에서 자연스럽게 트랜지션 애니메이션이 실행된다. 이러한 스타일의 추가, 삭제를 스크립트로 구현 해주기만 하면 된다.

추가적으로 Scrim(가림막)도 서서히 나타나게 하기 위해서 Display > Opacitiy를 0으로 해주고 Transition Aniamtion의 Duration만 1초로 설정해준다. 그 다음 Scrim으로 스타일 클래스를 추가해주고 Opacitiy를 100으로 해주고 스타일 클래스를 추가해준다. 이 Scrim도 동일하게 스타일 클래스를 추가해주고 삭제해줘서 애니메이션이 실행되도록 할 것이다.
이 때 ButtomSheet, Scrim 모두 기본 상태를 제외하고 스타일 클래스를 지워놔야 초기 상태에서부터 시작할 수 있따.
스타일 추가는 아래와 같이 실행할 수 있다.
UI 요소.AddToClassList("추가할 스타일")
private VisualElement _bottomSheet;
private VisualElement _scrim;
private void Start() {
// Find Root-Element
var root = GetComponent<UIDocument>().rootVisualElement;
// UQuery
_bottomContainer = root.Q<VisualElement>("Container_Bottom");
_openButton = root.Q<Button>("Button_Open");
_closeButton = root.Q<Button>("Button_Close");
_bottomSheet = root.Q<VisualElement>("BottomSheet");
_scrim = root.Q<VisualElement>("Scrim");
// Hide bottom-sheet
_bottomContainer.style.display = DisplayStyle.None;
// Button Callback
_openButton.RegisterCallback<ClickEvent>(OnOpenButtonClicked);
_closeButton.RegisterCallback<ClickEvent>(OnCloseButtonClicked);
}
private void OnOpenButtonClicked(ClickEvent evt) {
// Open Bottom-Sheet
_bottomContainer.style.display = DisplayStyle.Flex;
_bottomSheet.AddToClassList("buttomsheet--up");
_scrim.AddToClassList("Scrim--fadein");
}
시간이 너무 긴 거 같아서 uss로 가서 duration을 직접 바꿔줬다.
애니메이션이 안 나오면 USS로 가서 translate의 값 뒤에 %가 붙어있는지 보자. 저장할때 0%가 아니라 0으로 저장되는 버그가 있는것 같다.
왜 다시 창을 열 때 애니메이션이 실행이 안 될까? 애니메이션이 실행되는 원리는 스타일 리스트에 각각 스타일 클래스가 추가되면서 준비된 애니메이션이 재생된다.

그러나 이 때는 이미 스타일 클래스가 들어있는 상태이기 때문에 변하기 전과 변한 후의 상태 차이가 없는 것이다. 그래서 두 번째 창을 열 때 트랜지션 애니메이션이 일어나지 않는다.

따라서 닫기 버튼에서 클래스를 삭제해줘야 한다. 스타일 클래스 삭제 함수는 아래와 같다.
UI요소.RemoveFromClassList("제거할 스타일")
private void OnCloseButtonClicked(ClickEvent evt) {
// Close Bottom-Sheet
_bottomContainer.style.display = DisplayStyle.None;
_bottomSheet.RemoveFromClassList("buttomsheet--up");
_scrim.RemoveFromClassList("Scrim--fadein");
}

그러면 이제 열고 닫기를 반복해도 애니메이션이 잘 작동한다. 하지만 아쉬운 점은 창을 닫을때는 하단으로 내려가는 트랜지션 애니메이션이 없는데 이는 UI Tool Kit(3)에서 한 번 다뤄보자.