상세 컨텐츠

본문 제목

1202 TIL - 재사용 스크롤

스파르타 코딩캠프/'24 Today I Learned

by lucar 2024. 12. 2. 22:18

본문

오늘은 유니티 재사용 스크롤을 구현해보았다.

 

 

필드 값

[SerializeField] private GameObject prefab;
[SerializeField] private GameObject scrollview;
[SerializeField] private GameObject content;

private float pastPos; //스크롤 뷰가 마지막으로 움직인 위치

private float prefabWidth; //프리팹 오브젝트의 너비
private float prefabHeight; //프리팹 오브젝트의 높이
public float leftMargin; //좌측 마진
public float contentSpace; //콘텐츠 사이의 간격

public int rectCnt; //콘텐츠 내부의 최대 갯수
public int showCnt; //한 페이지에 보여질 갯수 예시 오브젝트는 이 수보다 2개 많게 미리 만들어놔야함

private int prevIdx = 0; //이전 페이지 값
private int totalCnt;

public List<GameObject> objs = new List<GameObject>();
public List<float> rectPositions = new List<float>();

public Action<GameObject, int> SetContent; //내용 수정용 액션 함수

 

 

사실 필드는 뭐 따로 소개할 내용은 없고

 

RectCnt는 이 스크롤이 가질 총 오브젝트의 갯수이다.

예를 들어 이 스크롤 뷰에는 1000개의 각기다른 오브젝트를 표시해야한다 싶으면 1000을 입력해준다.

ShowCnt는 한 화면에서 항상 보이는 오브젝트의 갯수이다.

끝에서 끝까지 항상 보이는 오브젝트의 갯수는 3개이다.

 

private void Start()
{
    pastPos = 0; //마지막 위치 0으로 초기화

    prefabWidth = prefab.GetComponent<RectTransform>().sizeDelta.x;
    prefabHeight = prefab.GetComponent<RectTransform>().sizeDelta.y;

	//콘텐트 오브젝트의 크기조절
    float contentX = (prefabWidth + contentSpace) * rectCnt + leftMargin;
    content.GetComponent<RectTransform>().sizeDelta
        = new Vector2(contentX - scrollview.GetComponent<RectTransform>().sizeDelta.x, content.GetComponent<RectTransform>().sizeDelta.y);

    totalCnt = showCnt + 2;
    for (int i = 0; i < rectCnt; i++)
    {
        float xPos = i * (prefabWidth + contentSpace) + leftMargin;
        rectPositions.Add(xPos); //시작과 동시에 각 오브젝트가 위치해야할 포지션을 리스트로 저장

        if (i < totalCnt) //ShowCnt+2만큼의 기본 오브젝트가 미리 필요함
        {
            GameObject obj = content.transform.GetChild(i).gameObject;
            obj.GetComponent<RectTransform>().localPosition
                = new Vector3(xPos, obj.GetComponent<RectTransform>().localPosition.y);

            SetContent?.Invoke(obj, i);
            objs.Add(obj);
        }
    }
    //스크롤 뷰의 포지션이 변경되면 실행할 함수를 구독해줌
    scrollview.GetComponent<ScrollRect>().onValueChanged.AddListener(GetPosition);
}

스타트 문에서는 각 변수의 초기화, 위치나 크기 값 수정을 위주로 진행해주었다.

 

다음이 메인이다.

private void GetPosition(Vector2 delta) //OnValueChanged는 매개변수로 Vector2값을 가진다.
{
    int pageCnt = rectCnt - showCnt;
    int pageOffset = pageCnt - 2;
    int curPage = pageCnt - (int)Mathf.Round(delta.x * pageCnt);

    float deltaX = pastPos - delta.x;

 

우선 PageCnt인데

 

총 8개의 항목이 있고 항상 보여지는 갯수가 3개라면

오브젝트가 이동하는 횟수는 5회가 된다.

 

123 -> 234 -> 345 -> 456 -> 567 -> 678

최초    1회      2회      3회     4회      5회

 

그래서 RectCnt(전체 오브젝트) - ShowCnt(보이는 오브젝트)의 값을 넣어주고

 

첫 페이지와 마지막 페이지에서는 오브젝트를 더 로드하지 않을 것이므로

PageCnt - 2를 해준다.

 

OnValueChanged를 통해 받는 매개변수 Vector2는

0 ~ 1의 값을 가지고 있고

수평 스크롤뷰에서는 0에서 시작해서 갈 수록 커지는 값을 가진다.

 

이는 수직 스크롤 뷰 맨 위가 1, 맨 아래가 0과는 대조되는 방식이다.

 

이를 이용해서 (int)Mathf.Round(delta.x * pageCnt) 를 통해 현재 페이지가 몇 번째인지 구할 수 있다.

 

수평 스크롤 뷰이기 때문에

pageCount에서 현재 페이지 수를 빼서 curCount를 역수로 저장한다.

가장 첫 페이지가 가장 큰 값을 갖게 된다.

 

deltaX는 마지막으로 이동한 점인데 0 -> 1로 이동하는 수평 스크롤뷰의 특성 상

오른쪽으로 드래그를 하면

 

0.1 -> 0.2로 이동을 하며 0.1 - 0.2 = - 0.1로 음수값을 가진다.

if (curPage == 0) return;

    if (deltaX == 0)
    {
    }
    else if (deltaX < 0) //스크롤이 올라갈때 (Horizontal 스크롤 뷰는 오른쪽으로 이동하면
    					 // 스크롤이 올라가는 판정이다.)
    {
        pastPos = delta.x;

        if (curPage <= pageOffset)
        {
            int temp = pageOffset - curPage;
            for (int i = prevIdx; i < temp + 1; i++)
            {
                int on = rectCnt - pageOffset + i;

                int idx = i % totalCnt;
                objs[idx].GetComponent<RectTransform>().localPosition
                    = new Vector3(rectPositions[on], objs[idx].GetComponent<RectTransform>().localPosition.y);
                SetContent?.Invoke(objs[idx], on);
            }
            prevIdx = temp;
        }
    }

 

지금은 curpage == 0이라면 리턴을 했지만

스크롤 뷰에서 elastic을 사용한다면 (curpage <= 0 || curpage >= pageCnt)일때 리턴을 해주는 게 좋을 것 같다.

 

그리고 위에서 설명했듯이 deltaX가 음수라면 스크롤이 올라가고 있으니

pastpos에 현재 delta.x값을 저장해 조그마한 움직임에도 반응할 수 있도록 해주고

 

현재 페이지가 PageOffset보다 작거나 같다면(맨 끝 페이지가 아니라면)

temp라는 변수를 만들어 pageOffset - curPage를 잠시 저장해준다.

 

이 변수는 옵셋페이지가 8이고 역수로 저장된 현재 페이지가 8이라면 0

7이라면 1, 8이라면 2 순으로 점차 증가하는 걸 볼 수 있다.

if (curPage <= pageOffset)
        {
            int temp = pageOffset - curPage;
            for (int i = prevIdx; i < temp + 1; i++)
            {
                int on = rectCnt - pageOffset + i;

                int idx = i % totalCnt;
                objs[idx].GetComponent<RectTransform>().localPosition
                    = new Vector3(rectPositions[on], objs[idx].GetComponent<RectTransform>().localPosition.y);
                SetContent?.Invoke(objs[idx], on);
            }
            prevIdx = temp;
        }

 

반복문의 경우에는

움직일 때 이전페이지 보다 앞으로 움직였으면 움직인만큼 뒤에 있는 오브젝트를 앞으로 끌어와주는 역할을 한다.

 

on 변수의 경우에는 예를 들어 rectCnt가 10이고 showCnt가 3이라면 pageOffset 은 5이다.

바로 전 페이지가 7페이지 중 2페이지라면 10 - 5 + ( i = 0 또는 1) = 5 또는 6의 값을 가지게 되는데

 

아까 저장해둔 포지션 리스트에서 5번째나 6번째 값(5,6번째 오브젝트 위치 X 값)을 가져오게 되고

i % totalCnt 번째 오브젝트 (0, 1번째 오브젝트)의 위치를 각각 5, 6 번으로 옮기게 된다.

 

else //스크롤이 내려갈 때
    {
        pastPos = delta.x;

        if (curPage <= pageOffset + 1)
        {
            int temp = pageOffset + 1 - curPage;
            for (int i = prevIdx; i >= temp; i--)
            {
                int on = rectCnt - pageOffset + i;

                int idx = i % totalCnt;
                objs[idx].GetComponent<RectTransform>().localPosition
                    = new Vector3(rectPositions[on - (showCnt + 2)], objs[idx].GetComponent<RectTransform>().localPosition.y);
                SetContent?.Invoke(objs[idx], (on - (showCnt + 2)));
            }
            prevIdx = temp;
        }
    }
}

 

내려갈 때도 비슷하다 for문의 구성만 바뀌었다.

on에서 showCnt+2 만큼 빼주는 이유는 항상 보여야 하는 오브젝트중 가장 왼쪽 오브젝트보다 2칸 더 아래에서 생성하기 위함이다. 1칸으로 해주면 깜박거린다.

 

마지막으로 코드 전문

using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class RecycleScrollX : MonoBehaviour
{
    [SerializeField] private GameObject prefab;
    [SerializeField] private GameObject scrollview;
    [SerializeField] private GameObject content;
    private float pastPos;

    private float prefabWidth;
    private float prefabHeight;
    public float leftMargin;
    public float contentSpace;

    public int rectCnt; //콘텐츠 내부의 최대 갯수
    public int showCnt; //한 페이지에 보여질 갯수 예시 오브젝트는 이 수보다 2개 많게 미리 만들어놔야함

    private int prevIdx = 0;
    private int totalCnt;

    public List<GameObject> objs = new List<GameObject>();
    public List<float> rectPositions = new List<float>();

    public Action<GameObject, int> SetContent;

    private void Start()
    {
        pastPos = 0;

        prefabWidth = prefab.GetComponent<RectTransform>().sizeDelta.x;
        prefabHeight = prefab.GetComponent<RectTransform>().sizeDelta.y;

        float contentX = (prefabWidth + contentSpace) * rectCnt + leftMargin;
        content.GetComponent<RectTransform>().sizeDelta
            = new Vector2(contentX - scrollview.GetComponent<RectTransform>().sizeDelta.x, content.GetComponent<RectTransform>().sizeDelta.y);

        totalCnt = showCnt + 2;
        for (int i = 0; i < rectCnt; i++)
        {
            float xPos = i * (prefabWidth + contentSpace) + leftMargin;
            rectPositions.Add(xPos);

            if (i < totalCnt)
            {
                GameObject obj = content.transform.GetChild(i).gameObject;
                obj.GetComponent<RectTransform>().localPosition
                    = new Vector3(xPos, obj.GetComponent<RectTransform>().localPosition.y);

                SetContent?.Invoke(obj, i);
                objs.Add(obj);
            }
        }
        scrollview.GetComponent<ScrollRect>().onValueChanged.AddListener(GetPosition);
    }


    private void GetPosition(Vector2 delta)
    {
        int pageCnt = rectCnt - showCnt;
        int pageOffset = pageCnt - 2;
        int curPage = pageCnt - (int)Mathf.Round(delta.x * pageCnt);

        Debug.Log(curPage);

        float deltaX = pastPos - delta.x;

        if (curPage == 0) return;

        if (deltaX == 0)
        {
        }
        else if (deltaX < 0)
        {
            pastPos = delta.x;

            if (curPage <= pageOffset)
            {
                int temp = pageOffset - curPage;
                for (int i = prevIdx; i < temp + 1; i++)
                {
                    int on = rectCnt - pageOffset + i;

                    int idx = i % totalCnt;
                    objs[idx].GetComponent<RectTransform>().localPosition
                        = new Vector3(rectPositions[on], objs[idx].GetComponent<RectTransform>().localPosition.y);
                    SetContent?.Invoke(objs[idx], on);
                }
                prevIdx = temp;
            }
        }
        else
        {
            pastPos = delta.x;

            if (curPage <= pageOffset + 1)
            {
                int temp = pageOffset + 1 - curPage;
                for (int i = prevIdx; i >= temp; i--)
                {
                    int on = rectCnt - pageOffset + i;

                    int idx = i % totalCnt;
                    objs[idx].GetComponent<RectTransform>().localPosition
                        = new Vector3(rectPositions[on - (showCnt + 2)], objs[idx].GetComponent<RectTransform>().localPosition.y);
                    SetContent?.Invoke(objs[idx], (on - (showCnt + 2)));
                }
                prevIdx = temp;
            }
        }
    }
}

 

비슷하게 포지션 x 값을 바꿔주는 내용을 전부 포지션 y값을 바꿔주게 바꾸고

CurPage를 역수가 아닌 정수를 가지게 바꿔주면

수직 재사용스크롤을 만들 수 있다.

 

수직 재사용스크롤 전문

using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class RecycleScrollY : MonoBehaviour
{
    [SerializeField] private GameObject prefab;
    [SerializeField] private GameObject scrollview;
    [SerializeField] private GameObject content;
    private float pastPos;

    private float prefabWidth;
    private float prefabHeight;
    public float UpMargin;
    public float contentSpace;

    public int rectCnt; //콘텐츠 내부의 최대 갯수
    public int showCnt; //한 페이지에 보여질 갯수 예시 오브젝트는 이 수보다 2개 많게 미리 만들어놔야함

    private int prevIdx = 0;
    private int totalCnt;

    public List<GameObject> objs = new List<GameObject>();
    public List<float> rectPositions = new List<float>();

    public Action<GameObject, int> SetContent;

    private void Start()
    {
        pastPos = 0;

        prefabWidth = prefab.GetComponent<RectTransform>().sizeDelta.x;
        prefabHeight = prefab.GetComponent<RectTransform>().sizeDelta.y;

        float contentY = (prefabHeight + contentSpace) * rectCnt + UpMargin;
        content.GetComponent<RectTransform>().sizeDelta
            = new Vector2(content.GetComponent<RectTransform>().sizeDelta.x, contentY - scrollview.GetComponent<RectTransform>().sizeDelta.y);

        totalCnt = showCnt + 2;
        for (int i = 0; i < rectCnt; i++)
        {
            float yPos = -i * (prefabHeight + contentSpace) + UpMargin;
            rectPositions.Add(yPos);

            if (i < totalCnt)
            {
                GameObject obj = content.transform.GetChild(i).gameObject;
                obj.GetComponent<RectTransform>().localPosition
                    = new Vector3(obj.GetComponent<RectTransform>().localPosition.x, yPos);

                SetContent?.Invoke(obj, i);
                objs.Add(obj);
            }
        }
        scrollview.GetComponent<ScrollRect>().onValueChanged.AddListener(GetPosition);
    }


    private void GetPosition(Vector2 delta)
    {
        int pageCnt = rectCnt - showCnt;
        int pageOffset = pageCnt - 2;
        int curPage = (int)Mathf.Round(delta.y * pageCnt);

        float deltaY = pastPos - delta.y;

        if (delta.y >= 1 || delta.y <= 0) return;

        if (deltaY == 0)
        {
        }
        else if (deltaY > 0)
        {
            pastPos = delta.y;

            if (curPage <= pageOffset)
            {
                int temp = pageOffset - curPage;
                for (int i = prevIdx; i < temp + 1; i++)
                {
                    int on = rectCnt - pageOffset + i;

                    int idx = i % totalCnt;
                    objs[idx].GetComponent<RectTransform>().localPosition
                        = new Vector3(objs[idx].GetComponent<RectTransform>().localPosition.x, rectPositions[on]);
                    SetContent?.Invoke(objs[idx], on);
                }
                prevIdx = temp;
            }
        }
        else
        {
            pastPos = delta.y;

            if (curPage <= pageOffset + 1)
            {
                int temp = pageOffset + 1 - curPage;
                for (int i = prevIdx; i >= temp; i--)
                {
                    int on = rectCnt - pageOffset + i;

                    int idx = i % totalCnt;
                    objs[idx].GetComponent<RectTransform>().localPosition
                        = new Vector3(objs[idx].GetComponent<RectTransform>().localPosition.x, rectPositions[on - (showCnt + 2)]);
                    SetContent?.Invoke(objs[idx], (on - (showCnt + 2)));
                }
                prevIdx = temp;
            }
        }
    }
}

 

TTE

'스파르타 코딩캠프 > '24 Today I Learned' 카테고리의 다른 글

1205 TIL - 이벤트 버스  (2) 2024.12.05
1203 TIL - 에셋 탐방  (1) 2024.12.03
1129 TIL - 스크롤 뷰  (1) 2024.11.29
1120 TIL - 프로퍼티  (0) 2024.11.20
1119 TIL - 플랫포머 플레이어 이동 2  (0) 2024.11.19

관련글 더보기