[Unity] UI PageView Class
Unity UI에서 페이지 단위로 View를 구성하는 PageView Class를 제작해보자.
Unity에서 페이지 단위로 UI를 나누고 싶을 때 활용하면 좋은 PageView Class를 제작해보자. 해당 기능은 사진첩을 커버 플로우 형태로 보거나 책을 넘기는 기능 등에 사용하면 좋다. Unity에서 기본적으로 제공하는 ScrollView를 활용하여 쉽게 구현이 가능하다.
1. UIPagingViewController Class 작성
- 해당 클래스는 Page를 관리하고 View를 드래그하여 컨트롤할 수 있는 클래스이다.
- Unity에서 제공하는 Scroll 컴포넌트를 필수 컴포넌트로 가지고 있어야 한다.
- Unity EventSystem에서 제공하는 IBeginDragHandler 및 IEndDragHandler 인터페이스를 상속받는다.
- Scripts 폴더를 만든 후 UIPagingViewController.cs C# Script를 생성하고 아래와 같이 작성하자.
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
namespace UI
{
[RequireComponent(typeof(RectTransform))]
[RequireComponent(typeof(ScrollRect))]
public class UIPagingViewController : MonoBehaviour, IBeginDragHandler, IEndDragHandler
{
[SerializeField]
protected GameObject gbj_ContentRoot = null;
[SerializeField]
protected UIPageControl pageControl;
[SerializeField]
private float animationDuration = 0.3f;
private float key1InTangent = 0f;
private float key1OutTangent = 1f;
private float key2InTangent = 1f;
private float key2OutTangent = 0f;
private bool isAnimating = false; // 애니메이션 재생 중임을 나타내는 플래그
private Vector2 destPosition; // 최종적인 스크롤 위치
private Vector2 initialPosition; // 자동 스크롤을 시작할 때의 스크롤 위치
private AnimationCurve animationCurve; // 자동 스크롤에 관련된 애니메이션 커브
private int prevPageIndex = 0; // 이전 페이지의 인덱스
private Rect currentViewRect; // 스크롤 뷰의 사각형 크기
public RectTransform CachedRectTransform
{
get
{
return GetComponent<RectTransform>();
}
}
public ScrollRect CachedScrollRect
{
get
{
return GetComponent<ScrollRect>();
}
}
// 드래그가 시작될 때 호출된다
public void OnBeginDrag(PointerEventData eventData)
{
// 애니메이션 도중에 플래그를 리셋한다
isAnimating = false;
}
// 드래그가 끝날 때 호출된다
public void OnEndDrag(PointerEventData eventData)
{
GridLayoutGroup grid = CachedScrollRect.content.GetComponent<GridLayoutGroup>();
// 현재 동작 중인 스크롤 뷰를 멈춘다
CachedScrollRect.StopMovement();
// GridLayoutGroup의 cellSize와 spacing을 이용하여 한 페이지의 폭을 계산한다
float pageWidth = -(grid.cellSize.x + grid.spacing.x);
// 스크롤의 현재 위치로부터 맞출 페이지의 인덱스를 계산한다
int pageIndex =
Mathf.RoundToInt((CachedScrollRect.content.anchoredPosition.x) / pageWidth);
if (pageIndex == prevPageIndex && Mathf.Abs(eventData.delta.x) >= 4)
{
// 일정 속도 이상으로 드래그할 경우 해당 방향으로 한 페이지 진행시킨다
CachedScrollRect.content.anchoredPosition += new Vector2(eventData.delta.x, 0.0f);
pageIndex += (int)Mathf.Sign(-eventData.delta.x);
}
// 첫 페이지 또는 끝 페이지일 경우에는 그 이상 스크롤하지 않도록 한다
if (pageIndex < 0)
{
pageIndex = 0;
}
else if (pageIndex > grid.transform.childCount - 1)
{
pageIndex = grid.transform.childCount - 1;
}
prevPageIndex = pageIndex; // 현재 페이지의 인덱스를 유지한다
// 최종적인 스크롤 위치를 계산한다
float destX = pageIndex * pageWidth;
destPosition = new Vector2(destX, CachedScrollRect.content.anchoredPosition.y);
// 시작할 때의 스크롤 위치를 저장해둔다
initialPosition = CachedScrollRect.content.anchoredPosition;
// 애니메이션 커브를 작성한다
Keyframe keyFrame1 = new Keyframe(Time.time, 0.0f, key1InTangent, key1OutTangent);
Keyframe keyFrame2 = new Keyframe(Time.time + animationDuration, 1.0f, key2InTangent, key2OutTangent);
animationCurve = new AnimationCurve(keyFrame1, keyFrame2);
// 애니메이션 재생 중임을 나타내는 플래그를 설정한다
isAnimating = true;
// 페이지 컨트롤 표시를 갱신한다.
if(pageControl != null)
pageControl.SetCurrentPage(pageIndex);
}
// 매 프레임마다 Update 메서드가 처리된 다음에 호출된다
void LateUpdate()
{
if (isAnimating)
{
if (Time.time >= animationCurve.keys[animationCurve.length - 1].time)
{
// 애니메이션 커브의 마지막 키프레임을 지나가면 애니메이션을 끝낸다
CachedScrollRect.content.anchoredPosition = destPosition;
isAnimating = false;
return;
}
// 애니메이션 커브를 사용하여 현재 스크롤 위치를 계산해서 스크롤 뷰를 이동시킨다
Vector2 newPosition = initialPosition + (destPosition - initialPosition) * animationCurve.Evaluate(Time.time);
CachedScrollRect.content.anchoredPosition = newPosition;
}
}
// 인스턴스를 로드할 때 Awake 메서드가 처리된 다음에 호출된다
void Start()
{
UpdateView();
if (pageControl != null)
{
if(gbj_ContentRoot != null)
pageControl.SetNumberOfPages(gbj_ContentRoot.transform.childCount);
pageControl.SetCurrentPage(0); // 페이지 컨트롤 표시를 초기화한다
}
}
// 매 프레임마다 호출된다
void Update()
{
if (CachedRectTransform.rect.width != currentViewRect.width || CachedRectTransform.rect.height != currentViewRect.height)
{
// 스크롤 뷰의 폭이나 높이가 변화하면 Scroll Content의 Padding을 갱신한다
UpdateView();
}
}
// Scroll Content의 Padding을 갱신한는 메서드
private void UpdateView()
{
// 스크롤 뷰의 사각형 크기를 보존해둔다
currentViewRect = CachedRectTransform.rect;
// GridLayoutGroup의 cellSize를 사용하여 Scroll Content의 Padding을 계산하여 설정한다
GridLayoutGroup grid = CachedScrollRect.content.GetComponent<GridLayoutGroup>();
int paddingH = Mathf.RoundToInt((currentViewRect.width - grid.cellSize.x) / 2.0f);
int paddingV = Mathf.RoundToInt((currentViewRect.height - grid.cellSize.y) / 2.0f);
grid.padding = new RectOffset(paddingH, paddingH, paddingV, paddingV);
}
}
}
2. UIPageControl Class 작성
- 해당 클래스는 필수로 필요하진 않지만, 현재 View가 몇번째 View인지 표시(인지)를 위해서 같이 사용하면 유용하다.
- Unity UI의 Toggle을 활용한다.
- Scripts 폴더를 만든 후 UIPageControl.cs C# Script를 생성하고 아래와 같이 작성하자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace UI
{
public class UIPageControl : MonoBehaviour
{
[SerializeField]
private Toggle toggle_Base;// 복사 원본 페이지 인디케이터
private List<Toggle> list_Toggles = new List<Toggle>();// 페이지 인디케이터를 저장
void Awake()
{
// 복사 원본 페이지 인디케이터는 비활성화시켜 둔다
toggle_Base.gameObject.SetActive(false);
}
// 페이지 수를 설정하는 메서드
public void SetNumberOfPages(int number)
{
if (list_Toggles.Count < number)
{
// 페이지 인디케이터 수가 지정된 페이지 수보다 적으면
// 복사 원본 페이지 인디케이터로부터 새로운 페이지 인디케이터를 작성한다
for (int i = list_Toggles.Count; i < number; i++)
{
Toggle indicator = Instantiate(toggle_Base) as Toggle;
indicator.gameObject.SetActive(true);
indicator.transform.SetParent(toggle_Base.transform.parent);
indicator.transform.localScale = toggle_Base.transform.localScale;
indicator.isOn = false;
list_Toggles.Add(indicator);
}
}
else if (list_Toggles.Count > number)
{
// 페이지 인디케이터 수가 지정된 페이지 수보다 많으면 삭제한다
for (int i = list_Toggles.Count - 1; i >= number; i--)
{
Destroy(list_Toggles[i].gameObject);
list_Toggles.RemoveAt(i);
}
}
}
// 현재 페이지를 설정하는 메서드
public void SetCurrentPage(int index)
{
if (index >= 0 && index <= list_Toggles.Count - 1)
{
// 지정된 페이지에 대응하되는 페이지 인디케이터를 ON으로 지정한다
// 토글 그룹을 설정해두었으므로 다른 인디케이터는 자동으로 OFF가 된다
list_Toggles[index].isOn = true;
}
}
}
}
3. UI 구성
- Scene에서 UI Canvas를 생성 후 Panel을 하나 만든 후 이름을 PagingView으로 변경해준다.
- PagingView 오브젝트에 ScrollRect 컴포넌트와 UIPagingViewController 컴포넌트를 붙여준다.
- PagingView 밑에 Viewport 및 Content 오브젝트를 생성해준다.
- Content 오브젝트에 GridLayoutGrop 컴포넌트와 ContentSizeFitter 컴포넌트를 붙여준다.
- PagingView 구성은 아래 그림을 참고하자.
- Contents 오브젝트 구성은 아래 그림을 참고하자.
- GridLayoutGrop 컴포넌트를 사용하여 CellSize(페이지 사이즈)를 설정한다.
- 하위 Image를 생성하여 Page를 만들어 준다.
- PageControl 오브젝트 밑에 Indicator/Background/Checkmark 오브젝트를 생성한다.
- PageControl 오브젝트에 ToggleGroup, GridLayoutGroup, UIPageControl 컴포넌트를 추가한다.
- UI 구성은 아래 그림을 참고하자.
4. 결과 화면
- 마우스나 드래그를 사용하여 이동시키면, Paging 기능이 정상 동작하는 걸 확인할 수 있다.
- GridLayoutGroup 컴포넌트의 수치값을 조절해보며, Page의 크기를 조절해볼 수 있다.
댓글