오늘은 유니티 재사용 스크롤을 구현해보았다.
필드 값
[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
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 |