Files
AIExpo/Assets/Scripts/NHN/InfiniteScroll.ItemContainer.cs
geondo55 1a79c9ffe3 최종
2025-09-18 10:58:34 +09:00

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;
}
}
}
}