본문 바로가기

[Unity] UI PageView Class

앤디가이 2022. 6. 9.

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 구성은 아래 그림을 참고하자.

unity ui pagingview
unity ui pagingview 구성

 - Contents 오브젝트 구성은 아래 그림을 참고하자.

unity ui content 구성
unity ui content 구성

 - GridLayoutGrop 컴포넌트를 사용하여 CellSize(페이지 사이즈)를 설정한다.

 - 하위 Image를 생성하여 Page를 만들어 준다.

 - PageControl 오브젝트 밑에 Indicator/Background/Checkmark 오브젝트를 생성한다.

 - PageControl 오브젝트에 ToggleGroup, GridLayoutGroup, UIPageControl 컴포넌트를 추가한다.

 - UI 구성은 아래 그림을 참고하자.

unity ui page control 구성
unity ui page control 구성

 

4. 결과 화면

 - 마우스나 드래그를 사용하여 이동시키면, Paging 기능이 정상 동작하는 걸 확인할 수 있다.

 - GridLayoutGroup 컴포넌트의 수치값을 조절해보며, Page의 크기를 조절해볼 수 있다.

unity ui paging view 완료 화면

댓글