278 lines
14 KiB
C#
278 lines
14 KiB
C#
namespace Gpm.Ui
|
|
{
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
public partial class InfiniteScroll
|
|
{
|
|
/// <summary>
|
|
/// 스크롤에 필요한 아이템의 개수를 지정합니다.
|
|
/// 이 값은 스크롤 뷰포트 크기를 기반으로 동적으로 계산될 수 있으며,
|
|
/// 화면에 보이는 아이템보다 더 많은 아이템을 미리 생성하여 부드러운 스크롤링을 보장합니다.
|
|
/// 일반적으로 (화면에 보이는 아이템 개수 * 2) 정도로 설정하는 것이 좋습니다.
|
|
/// </summary>
|
|
[Header("Scroll Item", order = 2)]
|
|
public int needItemCount = 0;
|
|
|
|
/// <summary>
|
|
/// 스크롤 리스트에 표시될 아이템의 프리팹입니다.
|
|
/// 이 프리팹은 `InfiniteScrollItem` 컴포넌트 또는 이를 상속받은 클래스를 포함해야 합니다.
|
|
/// </summary>
|
|
/// <example>
|
|
/// <code>
|
|
/// // Unity 에디터에서 `InfiniteScroll` 컴포넌트가 있는 GameObject를 선택합니다.
|
|
/// // Inspector 창에서 'Item Prefab' 필드에 프로젝트에 있는 아이템 프리팹을 드래그 앤 드롭합니다.
|
|
/// </code>
|
|
/// </example>
|
|
public InfiniteScrollItem itemPrefab = null;
|
|
|
|
/// <summary>
|
|
/// 아이템의 크기가 동적으로 변할 수 있는지 여부를 설정합니다.
|
|
/// true로 설정하면 각 아이템의 크기가 데이터에 따라 다를 수 있습니다.
|
|
/// 예를 들어, 텍스트 길이에 따라 높이가 변하는 채팅 메시지 아이템에 유용합니다.
|
|
/// 이 기능을 사용하려면 각 데이터(`DataContext`)가 `GetItemSize()`를 통해 자신의 크기를 반환해야 합니다.
|
|
/// </summary>
|
|
public bool dynamicItemSize = false;
|
|
|
|
/// <summary>
|
|
/// 화면에 보이는 영역 외에 추가로 아이템을 생성할 범위를 결정하는 비율입니다.
|
|
/// 값이 2이면, 화면 크기의 2배에 해당하는 영역만큼 아이템을 미리 생성하고 배치합니다.
|
|
/// 이를 통해 사용자가 스크롤할 때 아이템이 갑자기 나타나거나 사라지는 현상을 방지하고 부드러운 경험을 제공합니다.
|
|
/// </summary>
|
|
private const float NEED_MORE_ITEM_RATE = 2;
|
|
|
|
/// <summary>
|
|
/// `itemPrefab`의 기본 크기를 저장하는 변수입니다.
|
|
/// `dynamicItemSize`가 false일 때 모든 아이템의 크기를 동일하게 설정하기 위해 사용됩니다.
|
|
/// 스크롤 초기화 시점에 `itemPrefab`의 RectTransform에서 크기를 가져와 캐시합니다.
|
|
/// </summary>
|
|
private Vector2 defaultItemPrefabSize = Vector2.zero;
|
|
|
|
/// <summary>
|
|
/// 생성된 `InfiniteScrollItem` 객체들을 관리하는 리스트(객체 풀)입니다.
|
|
/// 스크롤이 움직일 때 새로운 아이템을 계속 생성하고 파괴하는 대신,
|
|
/// 이 리스트에 있는 아이템들을 재활용하여 성능을 최적화합니다.
|
|
/// </summary>
|
|
private List<InfiniteScrollItem> itemObjectList = new List<InfiniteScrollItem>();
|
|
|
|
/// <summary>
|
|
/// 지정된 인덱스에 해당하는 아이템의 크기를 가져옵니다.
|
|
/// 스크롤 레이아웃을 계산할 때 각 아이템의 위치를 결정하기 위해 사용됩니다.
|
|
/// </summary>
|
|
/// <param name="itemIndex">크기를 가져올 아이템의 인덱스입니다.</param>
|
|
/// <returns>아이템의 주 축(세로 스크롤이면 높이, 가로 스크롤이면 너비) 크기를 반환합니다.</returns>
|
|
public float GetItemSize(int itemIndex)
|
|
{
|
|
float size = 0;
|
|
// 동적 아이템 크기 모드가 활성화된 경우
|
|
if (dynamicItemSize == true)
|
|
{
|
|
// 유효한 아이템 인덱스인지 확인
|
|
if (itemIndex < itemCount)
|
|
{
|
|
// 해당 인덱스의 데이터 컨텍스트를 가져옴
|
|
DataContext context = GetContextFromItem(itemIndex);
|
|
if (context != null)
|
|
{
|
|
// 데이터 컨텍스트로부터 아이템 크기를 가져옴
|
|
size = context.GetItemSize();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 동적 크기 모드가 아닌 경우, 캐시된 기본 프리팹 크기를 사용
|
|
size = layout.GetMainSize(defaultItemPrefabSize);
|
|
}
|
|
|
|
return size;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 동적 아이템 크기 모드가 활성화되어 있는지 확인합니다.
|
|
/// </summary>
|
|
/// <returns>동적 아이템 크기 모드가 활성화되어 있으면 true, 그렇지 않으면 false를 반환합니다.</returns>
|
|
public bool IsDynamicItemSize()
|
|
{
|
|
return dynamicItemSize;
|
|
}
|
|
|
|
/// <summary>
|
|
/// `itemPrefab`을 사용하여 새로운 아이템 객체를 생성하고 초기화합니다.
|
|
/// 생성된 아이템은 `itemObjectList`에 추가되어 객체 풀링의 일부로 관리됩니다.
|
|
/// </summary>
|
|
/// <returns>새로 생성된 `InfiniteScrollItem` 인스턴스를 반환합니다.</returns>
|
|
private InfiniteScrollItem CreateItem()
|
|
{
|
|
// 프리팹으로부터 아이템 게임 오브젝트를 생성하고, content를 부모로 설정합니다.
|
|
InfiniteScrollItem itemObject = Instantiate(itemPrefab, content, false);
|
|
|
|
// 아이템을 초기화합니다. 부모 스크롤과 풀에서의 인덱스를 전달합니다.
|
|
itemObject.Initalize(this, itemObjectList.Count);
|
|
// 생성된 아이템은 기본적으로 비활성화 상태로 시작합니다.
|
|
itemObject.SetActive(false, false);
|
|
|
|
// 스크롤 방향에 맞게 아이템의 앵커와 피벗을 설정합니다.
|
|
itemObject.SetAxis(cachedData.anchorMin, cachedData.anchorMax, cachedData.itemPivot);
|
|
|
|
// 아이템이 선택되었을 때 호출될 콜백 함수를 등록합니다.
|
|
itemObject.AddSelectCallback(OnSelectItem);
|
|
|
|
// 아이템의 크기를 스크롤 레이아웃에 맞게 조정합니다.
|
|
RectTransform itemTransform = itemObject.rectTransform;
|
|
itemTransform.sizeDelta = layout.GetAxisVector(layout.GetMainSize(itemTransform.sizeDelta));
|
|
|
|
// 생성된 아이템을 객체 풀 리스트에 추가합니다.
|
|
itemObjectList.Add(itemObject);
|
|
|
|
return itemObject;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 부드러운 스크롤링을 위해 필요한 전체 아이템 영역의 크기를 계산합니다.
|
|
/// 뷰포트 크기에 `NEED_MORE_ITEM_RATE`를 곱하여 화면 밖의 버퍼 영역까지 포함합니다.
|
|
/// </summary>
|
|
/// <returns>필요한 총 아이템 영역의 크기 (주 축 기준)</returns>
|
|
private float GetNeedSize()
|
|
{
|
|
return layout.GetMainSize(viewport) * NEED_MORE_ITEM_RATE;
|
|
}
|
|
|
|
/// <summary>
|
|
/// `needItemCount`에 설정된 개수만큼 아이템 객체를 미리 생성합니다.
|
|
/// 스크롤 시작 시점에 호출되어 초기 아이템 풀을 구성합니다.
|
|
/// </summary>
|
|
private void CreateNeedItem()
|
|
{
|
|
for (int itemNumber = itemObjectList.Count; itemNumber < needItemCount; itemNumber++)
|
|
{
|
|
CreateItem();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 풀에 있는 모든 아이템의 데이터를 초기화합니다.
|
|
/// 아이템 게임 오브젝트는 파괴하지 않고, 데이터만 제거하여 재사용할 수 있도록 준비합니다.
|
|
/// 주로 새로운 데이터 리스트를 설정할 때 사용됩니다.
|
|
/// </summary>
|
|
private void ClearItemsData()
|
|
{
|
|
for (int index = 0; index < itemObjectList.Count; ++index)
|
|
{
|
|
itemObjectList[index].ClearData(false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 풀에 있는 모든 아이템 객체를 완전히 제거하고 리스트를 비웁니다.
|
|
/// 아이템의 게임 오브젝트를 파괴하므로, 스크롤을 더 이상 사용하지 않을 때 호출해야 합니다.
|
|
/// </summary>
|
|
private void ClearItems()
|
|
{
|
|
// 먼저 모든 아이템의 데이터를 초기화합니다.
|
|
ClearItemsData();
|
|
// 그 다음, 모든 아이템 게임 오브젝트를 파괴하고 풀에서 제거합니다.
|
|
for (int index = 0; index < itemObjectList.Count; ++index)
|
|
{
|
|
itemObjectList[index].Clear();
|
|
GameObject.Destroy(itemObjectList[index].gameObject);
|
|
}
|
|
itemObjectList.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 특정 데이터(`DataContext`)를 표시하기 위한 아이템을 객체 풀에서 가져옵니다.
|
|
/// 만약 이미 해당 데이터에 할당된 아이템이 있다면 그것을 반환하고,
|
|
/// 없다면 풀에서 비어있는 아이템을 찾아 할당하거나, 비어있는 아이템이 없으면 새로 생성합니다.
|
|
/// </summary>
|
|
/// <param name="context">표시할 데이터 정보를 담고 있는 `DataContext` 객체입니다.</param>
|
|
/// <returns>데이터를 표시할 `InfiniteScrollItem` 인스턴스를 반환합니다.</returns>
|
|
private InfiniteScrollItem PullItem(DataContext context)
|
|
{
|
|
InfiniteScrollItem item = context.itemObject;
|
|
|
|
// 컨텍스트에 아이템이 연결되어 있지 않거나, 연결된 아이템이 다른 데이터를 표시하고 있는 경우
|
|
if ( item == null ||
|
|
item.GetDataIndex() != context.index)
|
|
{
|
|
// 기존 연결을 해제하고 새로운 아이템을 찾습니다.
|
|
context.itemObject = null;
|
|
// 데이터 인덱스에 해당하는 아이템을 찾거나, 비어있는 아이템을 찾습니다.
|
|
int itemObjectIndex = GetItemIndexFromDataIndex(context.index, true);
|
|
if (itemObjectIndex == -1)
|
|
{
|
|
// 사용 가능한 아이템이 없으면 새로 생성합니다.
|
|
item = CreateItem();
|
|
}
|
|
else
|
|
{
|
|
// 사용 가능한 아이템이 있으면 풀에서 가져옵니다.
|
|
item = itemObjectList[itemObjectIndex];
|
|
}
|
|
}
|
|
|
|
return item;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 데이터 인덱스를 사용하여 객체 풀(`itemObjectList`)에서 해당 아이템의 인덱스를 찾습니다.
|
|
/// </summary>
|
|
/// <param name="dataIndex">찾고자 하는 데이터의 인덱스입니다.</param>
|
|
/// <param name="findEmptyIndex">true로 설정하면, `dataIndex`에 해당하는 아이템을 찾는 동시에 비어있는(비활성화된) 아이템의 인덱스도 함께 찾습니다.</param>
|
|
/// <returns>
|
|
/// `dataIndex`에 해당하는 아이템을 찾으면 그 아이템의 풀 인덱스를 반환합니다.
|
|
/// 찾지 못했을 경우, `findEmptyIndex`가 true이면 찾은 비어있는 아이템의 인덱스를 반환하고,
|
|
/// 비어있는 아이템도 없으면 -1을 반환합니다.
|
|
/// </returns>
|
|
private int GetItemIndexFromDataIndex(int dataIndex, bool findEmptyIndex = false)
|
|
{
|
|
int emptyIndex = -1;
|
|
for (int index = 0; index < itemObjectList.Count; ++index)
|
|
{
|
|
// 현재 아이템이 찾고 있는 데이터 인덱스를 이미 가지고 있다면 즉시 반환
|
|
if (itemObjectList[index].GetDataIndex() == dataIndex)
|
|
{
|
|
return index;
|
|
}
|
|
|
|
// 비어있는 아이템을 찾는 옵션이 켜져 있을 경우
|
|
if (findEmptyIndex == true)
|
|
{
|
|
// 아직 비어있는 아이템을 찾지 못했고, 현재 아이템이 비활성화 상태라면
|
|
if (emptyIndex == -1 &&
|
|
itemObjectList[index].IsActive() == false )
|
|
{
|
|
// 이 아이템의 인덱스를 예비로 저장
|
|
emptyIndex = index;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 루프가 끝날 때까지 dataIndex에 해당하는 아이템을 찾지 못했다면,
|
|
// 예비로 저장해 둔 비어있는 아이템의 인덱스를 반환
|
|
return emptyIndex;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 아이템의 크기가 동적으로 변경되었을 때 호출되는 내부 메서드입니다.
|
|
/// `dynamicItemSize`가 true일 때만 작동하며, 레이아웃을 다시 계산해야 함을 표시합니다.
|
|
/// </summary>
|
|
/// <param name="context">크기가 변경된 아이템의 `DataContext`입니다.</param>
|
|
internal void OnUpdateItemSize(DataContext context)
|
|
{
|
|
// 아이템이 실제로 화면에 표시되고 있는 경우, 모든 데이터의 위치를 업데이트
|
|
if (dynamicItemSize == true)
|
|
{
|
|
if(context.itemObject != null)
|
|
{
|
|
UpdateAllData(false);
|
|
}
|
|
|
|
// 다음 프레임에 레이아웃을 다시 계산하도록 플래그를 설정
|
|
needReBuildLayout = true;
|
|
}
|
|
}
|
|
}
|
|
}
|