2021. 3. 9. 16:59
https://blog.naver.com/nagne2011/222269642490
유니티 무한스크롤 (infinite scrollview)
구현 목표 기존 스크롤뷰 구현시, 그때그때 생성or미리 만들어서 사용 두가지 방식을 사용했으나 전자는 생...
blog.naver.com

사진 설명을 입력하세요.
구현 목표
기존 스크롤뷰 구현시, 그때그때 생성or미리 만들어서 사용 두가지 방식을 사용했으나
전자는 생성시 병목현상이 발생하고
후자는 오브젝트가많아질 경우 관리가 어렵다는 단점이 발생함
그래서 일정갯수만 미리 만들어두고 만든 오브젝트를 재사용하는 방식을 구현
안드로이드의 리사이클뷰에서 원리를 가져옴

사진 설명을 입력하세요.
(출처 : https://hwanine.github.io/android/Android-RecyclerView/)
많이들 쓸줄알았는데 의외로 자료가 없어서..
직접 만들었다
구현 기능은 최대한 단순하게 구성했다
1. 하위 항목 갯수와, 전체 항목 갯수를 지정해두고 스크롤 이동시 하위 항목 개수가
특정 계산식에 맞춰 재사용되고 내부 항목은 갱신된다
2. 원할경우 항목을 원하는 갯수만큼 추가
3. 흔히 사용하는, 스크롤 최하단에서 아래로 추가로 스크롤 할 경우 로딩이미지가 출현하고
일정 시간 이후(로딩이 끝나면) 일정 갯수의 항목이 추가됨
본격적으로 시작하자
1. 스크롤 뷰 오브젝트 생성
기본적인 canvas와 scrollview 오브젝트는 미리 만들어줘야한다
사진 설명을 입력하세요.
사진 설명을 입력하세요.
그리고 위 스샷처럼 내부 항목을 변경해준다
정확하게는 세로스크롤만 사용할 예정이므로
Horizontal Scrollbar를 제거하고, 체크박스도 해제해준다
그다음 중요한건 view영역에 표시될 항목을 미리 만들어야한다
항목의 갯수는 표시될 갯수 + 2
그래야 상하로 이동시 삐져나오는?부분이 잘 보여서 자연스러운 스크롤이 가능하다
사진 설명을 입력하세요.
이런느낌이다
포스팅에서는 view영역에 3개만 표시될 예정이라
총 5개의 오브젝트를 미리 만들었다
오브젝트는 일단 text가 포함된 image로 구성했으나
프리펩등으로 구현하는게 데이터 삽입 및 갱신에 유리하다
여기서 중요한게 하나 있는데,
사진 설명을 입력하세요.
사진 설명을 입력하세요.
오브젝트 위치 계산을 위해 Pivot을 (0.5, 1)로 잡아줘야한다
기본값은 (0.5, 0.5)인데, 스크롤뷰 사용시에는 상단으로 고정해줘야 위치가 제대로 잡힌다
스크립트쪽으로 넘어가자
//로딩 오브젝트
public GameObject m_loadingImage;
//스크롤뷰 관련 오브젝트
public Scrollbar m_scrollbar;
public GameObject m_scrollview;
public GameObject m_contentObject;
//스크롤뷰 view영역 상하 마진
public float m_scrollview_margin_top;
public float m_scrollview_margin_bottom;
//하위 항목 박스 사이즈 관련
public float m_rectSize = 50;
public float m_spacing = 10;
//시작 박스 항목 박스 갯수 관련
public int m_rectCount = 12;
//한번에 보여지는 박스 갯수
/*보이는 박스 갯수 +2개 오브젝트가 미리 생성되어있어야함(예외처리 없음)*/
public int m_showRectCount = 3;
private List<GameObject> m_objs = new List<GameObject>();
private List<float> m_rectPositions = new List<float>();
private float pastPos = 0;
private int totalObjectCount;
float m_srollbarSize = 0;
사용할 변수는 이정도
주석을 달아놨고 간단하게 인스펙터에서 조절 가능한 변수는 public으로 빼놨다
(개인적으로는 [serialize]private를 추천..)
주석을 참고해서 scrollview와 관련된 항목을 갱신하고
중요한 부분은 '한번에 보여지는 박스 갯수'인 m_showRectCount다
위에서 오브젝트를 만든 갯수대로 맞춰야된다
즉, 아까 3+2개 해서 5개로 맞췄으니까
m_showRectCount는 3으로 맞추면된다
인스펙터창에서 조절하자
그리고 전체 박스 갯수(m_rectCount)를 지정해주면 끝
위 변수를 기반으로 초기화를 진행하자
void Start()
{
pastPos = m_scrollbar.value;
//초기 박스 크기 갱신
float deltay = m_rectSize * m_showRectCount + m_spacing * (m_showRectCount - 1);
deltay += m_scrollview_margin_top;
deltay += m_scrollview_margin_bottom;
m_scrollview.GetComponent<RectTransform>().sizeDelta = new Vector2(m_scrollview.GetComponent<RectTransform>().sizeDelta.x, deltay);
float contenty = (m_rectSize + m_spacing) * m_rectCount - m_spacing;
m_contentObject.GetComponent<RectTransform>().sizeDelta = new Vector2(m_contentObject.GetComponent<RectTransform>().sizeDelta.x, contenty);
//초기 항목 위치 갱신
totalObjectCount = m_showRectCount + 2;
for (int i = 0; i < m_rectCount; i++)
{
float ypos = -1 * ((m_rectSize * i) + m_spacing * (i));
m_rectPositions.Add(ypos);
//초기 항목 데이터 갱신
if (i < totalObjectCount)
{
GameObject obj = m_contentObject.transform.GetChild(i).gameObject;
obj.GetComponent<RectTransform>().localPosition = new Vector3(obj.GetComponent<RectTransform>().localPosition.x, ypos);
//데이터 갱신
SetContents(obj, i.ToString());
m_objs.Add(obj);
}
}
}
//내부 데이터 갱신 함수
private void SetContents(GameObject obj, string str)
{
obj.GetComponentInChildren<Text>().text = str;
}
스크롤바 움직임을 감지하기 위해 pastpos 초기화부터 시작한다
그리고 입력한 데이터(rectsize, spacing, rectcount 등등)를 통해
스크롤뷰 박스 크기 및 view영역을 갱신해준다
이부분은 사실 고민을 많이했는데,
UI제작시 디자인 기획에 맞춰서 view영역 크기를 유니티 상에서 맞추는 경우가 많다
그런데 스크립트에서 오브젝트 크기를 조절할 경우 디자인 기획과 어긋나는 경우가 많아서
해당 부분은 필요에 따라 제거하거나 하자
이하에는 m_rectPositions 리스트를 갱신해주는데,
해당 리스트는 'n번째 오브젝트의 y축 좌표값'을 의미한다
즉 0번째 오브젝트는 0, 1번째 오브젝트는 0번째 오브젝트의 크기 + spacing간격 위치에 위치하게된다
해당 데이터를 가지고 다음에 출현할 항목의 위치를 잡아주는 방식이 핵심
추가로, 초기 생성해둔 3+2개의 오브젝트의 초기 데이터를 갱신해주고
spacing에 맞게 위치도 잡아준다
void Update()
{
int pagecount = m_rectCount - m_showRectCount;
int pageoffset = pagecount - 2;
//스크롤 방향 감지
float delta = pastPos - m_scrollbar.value;
if (delta == 0)
{
}
else if (delta > 0)
{
//스크롤 내려갈때 계산
pastPos = m_scrollbar.value;
}
else
{
//스크롤 올라갈때 계산
pastPos = m_scrollbar.value;
}
그리고 update함수에서 스크롤 위치 이동을 감지한다
초기 스크롤 위치를 시작으로, 스크롤 변화량을 감지해서
음수, 양수로 구분해서 진행한다
스크롤이 가만히 있으면 변화량이 0이므로, 아무 동작을 하지 않는다
이제 본격적으로 스크롤계산을 하자
먼저 '페이지'숫자를 계산해야되는데
여기서 '페이지'숫자란, 현재 총 12개의 항목이 내부에 있고, 한번에 3개씩 보여지니까
전체 '페이지'갯수는 9개다
(pagecount)
왜냐하면 2개는 항상 보이고, 1개씩 페이지가 넘어간다고 했을때
넘길수 있는 페이지는 9번이라는 의미다
이렇게 '페이지'를 알수있으면 아래 함수를 통해 스크롤바의 위치에 따른 '현재 페이지'를 계산할수있다
(int)Math.Round(m_scrollbar.value * pagecount)
scrollbar는 0~1의 데이터를 가지고있고
현재 페이지는 1~9페이지다
(이때, 스크롤바를 맨 위로 올렸을때 페이지는 9페이지가 맞다, 맨 아래로 내리면 1페이지)
else if (delta > 0)
{
//스크롤 내려갈때 계산
pastPos = m_scrollbar.value;
if ((int)Math.Round(m_scrollbar.value * pagecount) <= pageoffset)
{
int temp = pageoffset - (int)Math.Round(m_scrollbar.value * pagecount);
int on = m_rectCount - (int)Math.Round(m_scrollbar.value * pagecount);
if (temp >= totalObjectCount) temp %= totalObjectCount;
m_objs[temp].GetComponent<RectTransform>().localPosition
= new Vector3(m_objs[temp].GetComponent<RectTransform>().localPosition.x, m_rectPositions[on]);
}
}
추가로 pageoffset을 먼저 계산해야된다
아까 계산한 페이지 갯수(pagecount )에서 맨 위, 맨 아래 항목을 제외한 갯수를 세준다
그래야 스크롤 맨 위, 맨 아래로 이동했을때 다음 항목을 갱신하지 않는다
이제 다음에 view영역 밖으오 올라간 오브젝트를 계산(temp)해서
다음에 view영역 안으로 들어올 오브젝트의 위치로 이동시킨다
해당 오프셋 값에서 아까 계산한 현재 값을 빼면된다
이때, 맨 위와 맨 아래로 갔을때는 이동하지 오브젝트가 이동되지 않도록 조건문을 걸어준다
그리고 미리 만들어진 오브젝트를 재활용할 예정이므로
해당 값(temp)이 만들어진 오브젝트 갯수보다 늘어나면 안되니까 %계산을 활용해서 계산해준다
그러고 나면 오브젝트가 확정됬으니, 오브젝트에 맞는 y축 좌표(m_rectPositions)를 가져온다
y축 좌표 인덱스는 "다음에 view영역 안으로 들어올 오브젝트'의 인덱스를 계산하면 된다
if (delta == 0)
{
}
else if (delta > 0)
{
//스크롤 내려갈때 계산
pastPos = m_scrollbar.value;
if ((int)Math.Round(m_scrollbar.value * pagecount) <= pageoffset)
{
int temp = pageoffset - (int)Math.Round(m_scrollbar.value * pagecount);
int on = m_rectCount - (int)Math.Round(m_scrollbar.value * pagecount);
if (temp >= totalObjectCount) temp %= totalObjectCount;
{
m_objs[temp].GetComponent<RectTransform>().localPosition
= new Vector3(m_objs[temp].GetComponent<RectTransform>().localPosition.x, m_rectPositions[on]);
SetContents(m_objs[temp], on.ToString());
}
}
else
{
//스크롤 올라갈때 계산
pastPos = m_scrollbar.value;
if ((int)Math.Round(m_scrollbar.value * pagecount) <= pageoffset + 1)
{
int temp = pageoffset + 1 - (int)Math.Round(m_scrollbar.value * pagecount);
int on = m_rectCount - (int)Math.Round(m_scrollbar.value * pagecount);
if (temp >= totalObjectCount) temp %= totalObjectCount;
m_objs[temp].GetComponent<RectTransform>().localPosition
= new Vector3(m_objs[temp].GetComponent<RectTransform>().localPosition.x, m_rectPositions[on - (m_showRectCount + 1)]);
SetContents(m_objs[temp], (on - (m_showRectCount + 1)).ToString());
}
}
}
스크롤이 올라갈때 부분도 똑같이 만들어주는데,
이때 주의할게 pageoffset에 1을 추가해줘야한다
페이지가 1부터 시작하기 때문
그리고 y축 좌표값은 (showrectcount+1)을 제외시켜줘야하는데
최 상단 항목의 바로 윗 부분으로 이동해야하기 때문이다
여기까지만 해도 UI를 상하로 움직였을때 정상적인 동작을 볼수있다
만약 문제가 생길경우 각 수치를 확인해서 오류부분을 확인하자
다음 챕터로,
추가로 데이터를 추가하는 함수를 추가한다
//내부 데이터 항목 추가 함수
private void AddContents(int count)
{
//전체 데이터 크기증가
float contenty = (m_rectSize + m_spacing) * count;
m_contentObject.GetComponent<RectTransform>().sizeDelta = new Vector2(m_contentObject.GetComponent<RectTransform>().sizeDelta.x,
m_contentObject.GetComponent<RectTransform>().sizeDelta.y + contenty);
m_rectCount += count;
int pastCount = m_rectPositions.Count;
for (int i = pastCount; i < pastCount + count; i++)
{
float ypos = -1 * ((m_rectSize * i) + m_spacing * (i));
m_rectPositions.Add(ypos);
}
}
순서는 간단하다
1. 추가하는 항목의 갯수만큼 view영역의 크기를 늘려준다
2. 전체 항목 갯수에 추가 갯수를 더한다
3. 늘어난 숫자만큼 각 항목 y축 위치를 갱신하여 m_rectPositions를 갱신한다
따로 함수를 직접 구동해서 제대로 동작하는지 확인하자
언제나 디버그는 필수
다음차례는 스크롤뷰를 아래로 당겼을때를 감지해서, 로딩 이미지와 함께 항목 추가하자
void Update()
{
//아래로 끌어당긴거 감지
if (Input.GetMouseButtonDown(0))
{
m_srollbarSize = (float)Math.Truncate(m_scrollbar.size * 100) / 100;
}
if (Input.GetMouseButtonUp(0))
{
if (m_scrollbar.value <= 0)
{
float deltaSize = (float)Math.Truncate(m_scrollbar.size * 100) / 100;
if (m_srollbarSize != deltaSize)
{
Debug.Log("loading");
//Test 로딩 아이콘 생성
StartCoroutine(CoLoadData(6));
}
}
}
}
//Test
//로딩 이미지 구현 및 스크롤터치 막는 기능
private IEnumerator CoLoadData(int count)
{
m_loadingImage.SetActive(true);
this.GetComponent<GraphicRaycaster>().enabled = false;
yield return new WaitForSeconds(3.0f);
AddContents(count);
m_loadingImage.SetActive(false);
this.GetComponent<GraphicRaycaster>().enabled = true;
}
사실 CoLoadData코루틴 함수는 퍼포먼스용이다 사실 별 의미는 없다
해당 함수 대신, 서버나 기타 다른방법으로 db등을 불러오는 함수를 만들면 된다
아래로 끌어당겼을때를 감지하는 부분은 m_srollbarSize 변수를 이용했다
따로 지원해주는 함수가 없어서.. 단순히 스크롤 바를 맨 아래로 끌어내렸을때, scrollbar의 기능덕분에
스크롤바가 늘어지는 부분이 있어서 이를 활용했다
정확한건 직접 구현해서 확인을 해보자
추가설명을 하자면
Math라이브러리의 Trucate함수를 사용해서 소숫점 둘째자리 버림으로 계산한다
스크롤에 대한 민감도등을 조절하기위해 둘째자리 정도로 지정했지만
기획에 떄라 해당 공식을 수정할 필요는 있다
m_srollbarSize = (float)Math.Truncate(m_scrollbar.size * 100) / 100;
이렇게 마우스 클릭시(혹은 터치 시)에 현재 스크롤바 크기를 갱신하고,
마우스를 떼었을때 (터치가 끝났을때) 다시 스크롤바 크기를 계산하여
아까 값과 비교한다
이렇게 해서 클릭 전/후 스크롤 바의 크기를 비교할수있다
이때 주의해야할건 스크롤을 아래로 당겼을 경우와 위로 당겼을 경우 모두 스크롤 바 크기가 변경되기때문에
아래로 당겼을때만 감지하도록 m_scrollbar의 value값이 0이하일때만 동작하도록 확실하게 처리를 해주자
<코드 전문>
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
/*
* Developed by 09.03.2021 Raven (Jonghun Kim)
*/
public class ScrollviewController : MonoBehaviour
{
//로딩 오브젝트
public GameObject m_loadingImage;
//스크롤뷰 관련 오브젝트
public Scrollbar m_scrollbar;
public GameObject m_scrollview;
public GameObject m_contentObject;
//스크롤뷰 view영역 상하 마진
public float m_scrollview_margin_top;
public float m_scrollview_margin_bottom;
//하위 항목 박스 사이즈 관련
public float m_rectSize = 50;
public float m_spacing = 10;
//시작 박스 항목 박스 갯수 관련
public int m_rectCount = 12;
//한번에 보여지는 박스 갯수
/*보이는 박스 갯수 +2개 오브젝트가 미리 생성되어있어야함(예외처리 없음)*/
public int m_showRectCount = 3;
private List<GameObject> m_objs = new List<GameObject>();
private List<float> m_rectPositions = new List<float>();
private float pastPos = 0;
private int totalObjectCount;
float m_srollbarSize = 0;
void Start()
{
pastPos = m_scrollbar.value;
//초기 박스 크기 갱신
float deltay = m_rectSize * m_showRectCount + m_spacing * (m_showRectCount - 1);
deltay += m_scrollview_margin_top;
deltay += m_scrollview_margin_bottom;
m_scrollview.GetComponent<RectTransform>().sizeDelta = new Vector2(m_scrollview.GetComponent<RectTransform>().sizeDelta.x, deltay);
float contenty = (m_rectSize + m_spacing) * m_rectCount - m_spacing;
m_contentObject.GetComponent<RectTransform>().sizeDelta = new Vector2(m_contentObject.GetComponent<RectTransform>().sizeDelta.x, contenty);
//초기 항목 위치 갱신
totalObjectCount = m_showRectCount + 2;
for (int i = 0; i < m_rectCount; i++)
{
float ypos = -1 * ((m_rectSize * i) + m_spacing * (i));
m_rectPositions.Add(ypos);
//초기 항목 데이터 갱신
if (i < totalObjectCount)
{
GameObject obj = m_contentObject.transform.GetChild(i).gameObject;
obj.GetComponent<RectTransform>().localPosition = new Vector3(obj.GetComponent<RectTransform>().localPosition.x, ypos);
//데이터 갱신
SetContents(obj, i.ToString());
m_objs.Add(obj);
}
}
}
void Update()
{
//아래로 끌어당긴거 감지
if (Input.GetMouseButtonDown(0))
{
m_srollbarSize = (float)Math.Truncate(m_scrollbar.size * 100) / 100;
}
if (Input.GetMouseButtonUp(0))
{
if (m_scrollbar.value <= 0)
{
float deltaSize = (float)Math.Truncate(m_scrollbar.size * 100) / 100;
if (m_srollbarSize != deltaSize)
{
Debug.Log("loading");
//Test 로딩 아이콘 생성
StartCoroutine(CoLoadData(6));
}
}
}
//스크롤 방향 감지
int pagecount = m_rectCount - m_showRectCount;
int pageoffset = pagecount - 2;
if ((int)Math.Round(m_scrollbar.value * pagecount) == 0) return; //상하단 끄트머리 감지
float delta = pastPos - m_scrollbar.value;
if (delta == 0)
{
}
else if (delta > 0)
{
//스크롤 내려갈때 계산
pastPos = m_scrollbar.value;
if ((int)Math.Round(m_scrollbar.value * pagecount) <= pageoffset)
{
int temp = pageoffset - (int)Math.Round(m_scrollbar.value * pagecount);
int on = m_rectCount - (int)Math.Round(m_scrollbar.value * pagecount);
if (temp >= totalObjectCount) temp %= totalObjectCount;
m_objs[temp].GetComponent<RectTransform>().localPosition
= new Vector3(m_objs[temp].GetComponent<RectTransform>().localPosition.x, m_rectPositions[on]);
SetContents(m_objs[temp], on.ToString());
}
}
else
{
//스크롤 올라갈때 계산
pastPos = m_scrollbar.value;
if ((int)Math.Round(m_scrollbar.value * pagecount) <= pageoffset + 1)
{
int temp = pageoffset + 1 - (int)Math.Round(m_scrollbar.value * pagecount);
int on = m_rectCount - (int)Math.Round(m_scrollbar.value * pagecount);
if (temp >= totalObjectCount) temp %= totalObjectCount;
m_objs[temp].GetComponent<RectTransform>().localPosition
= new Vector3(m_objs[temp].GetComponent<RectTransform>().localPosition.x, m_rectPositions[on - (m_showRectCount + 1)]);
SetContents(m_objs[temp], (on - (m_showRectCount + 1)).ToString());
}
}
}
//내부 데이터 갱신 함수
private void SetContents(GameObject obj, string str)
{
obj.GetComponentInChildren<Text>().text = str;
}
//내부 데이터 항목 추가 함수
private void AddContents(int count)
{
//전체 데이터 크기증가
float contenty = (m_rectSize + m_spacing) * count;
m_contentObject.GetComponent<RectTransform>().sizeDelta = new Vector2(m_contentObject.GetComponent<RectTransform>().sizeDelta.x,
m_contentObject.GetComponent<RectTransform>().sizeDelta.y + contenty);
m_rectCount += count;
int pastCount = m_rectPositions.Count;
for (int i = pastCount; i < pastCount + count; i++)
{
float ypos = -1 * ((m_rectSize * i) + m_spacing * (i));
m_rectPositions.Add(ypos);
}
}
//Test
//로딩 이미지 구현 및 스크롤터치 막는 기능
private IEnumerator CoLoadData(int count)
{
m_loadingImage.SetActive(true);
this.GetComponent<GraphicRaycaster>().enabled = false;
yield return new WaitForSeconds(3.0f);
AddContents(count);
m_loadingImage.SetActive(false);
this.GetComponent<GraphicRaycaster>().enabled = true;
}
}
21.11.26 추가수정
실제 사용중, 스크롤을 빠르게 이동시켰을때 UI위치가 제대로 배치되지 않고 밀리는 버그 발생
버그발생 원인은 스크롤의 index가 정확히 순서대로 갱신되지 않아서, 위치값이 띄엄띄엄 잡히는 문제
수정사항
스크롤을 올리거나 내릴때, 현재 index에 맞춰서 지나친 index를 순차적으로 계산해서
for문 을 사용하여 index가 순서대로 갱신되지않았을때, 위치값을 순차대로 갱신하도록 수정
private int m_prevIndex = 0;
if (delta == 0)
{
}
else if (delta > 0)
{
//스크롤 내려갈때 계산
pastPos = m_scrollbar.value;
if ((int)Math.Round(m_scrollbar.value * pagecount) <= pageoffset)
{
int temp = pageoffset - (int)Math.Round(m_scrollbar.value * pagecount);
for(int i= m_prevIndex; i<temp+1;i++)
{
//int on = m_rectCount - (int)Math.Round(m_scrollbar.value * pagecount);
int on = m_rectCount - pageoffset + i;
int objIndex = i % totalObjectCount;
m_objs[objIndex].GetComponent<RectTransform>().localPosition
= new Vector3(m_objs[objIndex].GetComponent<RectTransform>().localPosition.x, m_rectPositions[on]);
SetContents(m_objs[objIndex], on);
}
m_prevIndex = temp;
}
}
else
{
//스크롤 올라갈때 계산
pastPos = m_scrollbar.value;
if ((int)Math.Round(m_scrollbar.value * pagecount) <= pageoffset + 1)
{
int temp = pageoffset+ 1 - (int)Math.Round(m_scrollbar.value * pagecount);
for (int i = m_prevIndex; i >= temp; i--)
{
//int on = m_rectCount - (int)Math.Round(m_scrollbar.value * pagecount);
int on = m_rectCount - pageoffset + i;
int objIndex = i % totalObjectCount;
m_objs[objIndex].GetComponent<RectTransform>().localPosition
= new Vector3(m_objs[objIndex].GetComponent<RectTransform>().localPosition.x, m_rectPositions[on - (m_showRectCount + 2)]);
SetContents(m_objs[objIndex], (on - (m_showRectCount + 2)));
}
m_prevIndex = temp;
}
}
'유니티' 카테고리의 다른 글
ZenJect - 의존성 주입 - Factory (0) | 2022.02.15 |
---|---|
ZenJect - 의존성 주입 (0) | 2022.02.15 |
JsonUtility 사용법 (0) | 2022.02.15 |
페이드인 이펙트 (0) | 2022.02.15 |
(NGUI) 숫자 비밀번호 만들기 (0) | 2022.02.14 |