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