namespace Gpm.Ui { using System; using System.Collections.Generic; public partial class InfiniteScroll { /// /// InfiniteScroll에서 각 아이템의 데이터를 관리하는 내부 클래스입니다. /// 원본 데이터와 함께 스크롤 내에서의 상태(인덱스, 크기, 연결된 UI 객체 등)를 저장합니다. /// public class DataContext { /// /// DataContext의 생성자입니다. /// /// 아이템에 표시될 원본 데이터입니다. `InfiniteScrollData`를 상속받은 클래스의 인스턴스여야 합니다. /// 전체 데이터 리스트(`dataList`) 내에서의 인덱스입니다. public DataContext(InfiniteScrollData data, int index) { this.index = index; this.data = data; } /// /// 아이템에 표시될 원본 데이터입니다. /// 사용자가 정의한 `InfiniteScrollData`의 자식 클래스 인스턴스입니다. /// internal InfiniteScrollData data; /// /// 전체 데이터 리스트(`dataList`) 내에서 이 데이터의 인덱스입니다. /// 이 값은 데이터가 추가되거나 삭제될 때 변경될 수 있습니다. /// internal int index = -1; /// /// 현재 화면에 표시되는 아이템들 사이에서의 인덱스입니다. /// 필터링으로 인해 일부 아이템이 보이지 않는 경우 `index`와 다른 값을 가질 수 있습니다. /// 아이템이 화면에 표시되지 않으면 -1 값을 가집니다. /// internal int itemIndex = -1; /// /// 스크롤 내에서 아이템의 시작 위치(오프셋)입니다. /// 레이아웃 계산에 사용됩니다. /// internal float offset = 0; /// /// 데이터가 변경되어 연결된 UI 아이템(`itemObject`)의 내용을 갱신해야 하는지 여부를 나타냅니다. /// true이면 다음 레이아웃 업데이트 시점에 `InfiniteScrollItem.UpdateData`가 호출됩니다. /// internal bool needUpdateItemData = true; /// /// 이 데이터에 해당하는 아이템의 크기입니다. (스크롤의 주 축 기준) /// `dynamicItemSize`가 활성화된 경우 각 아이템마다 다른 크기를 가질 수 있습니다. /// internal float scrollItemSize = 0; /// /// 이 데이터에 현재 연결된 `InfiniteScrollItem` UI 객체입니다. /// 아이템이 화면 밖으로 스크롤되어 재활용되면 null이 될 수 있습니다. /// internal InfiniteScrollItem itemObject; /// /// UI 아이템의 내용을 갱신해야 하는지 확인합니다. /// /// 갱신이 필요하면 true, 그렇지 않으면 false를 반환합니다. public bool IsNeedUpdateItemData() { return needUpdateItemData; } /// /// 데이터와 연결된 UI 아이템(`itemObject`)의 연결을 끊습니다. /// 아이템은 재활용 풀로 돌아가 다른 데이터를 위해 사용될 수 있습니다. /// /// 상태 변경 이벤트를 발생시킬지 여부입니다. public void UnlinkItem(bool notifyEvent = false) { if (itemObject != null) { // 아이템의 데이터를 초기화하고 재활용 준비를 합니다. itemObject.ClearData(notifyEvent); itemObject = null; } // 화면에 표시되는 아이템이 아니므로 itemIndex를 -1로 설정합니다. itemIndex = -1; } /// /// 이 DataContext의 원본 데이터를 새로운 데이터로 교체합니다. /// /// 새로운 `InfiniteScrollData`입니다. public void UpdateData(InfiniteScrollData data) { this.data = data; // 데이터가 변경되었으므로 UI 갱신이 필요함을 표시합니다. needUpdateItemData = true; } /// /// 아이템의 캐시된 크기를 가져옵니다. /// /// 아이템의 크기 (스크롤 주 축 기준) public float GetItemSize() { return scrollItemSize; } /// /// 아이템의 크기를 설정합니다. /// 이 값은 `dynamicItemSize`가 true일 때 레이아웃 계산에 사용됩니다. /// /// 설정할 아이템의 크기입니다. public void SetItemSize(float value) { scrollItemSize = value; } } /// /// 스크롤에 추가된 모든 데이터(`DataContext`)를 관리하는 리스트입니다. /// protected List dataList = new List(); // /// 현재 필터링 조건을 만족하여 화면에 표시될 수 있는 아이템의 총 개수입니다. /// `onFilter` 델리게이트가 설정된 경우 `dataList.Count`와 다를 수 있습니다. /// protected int itemCount = 0; /// /// 필터링 조건 변경 등으로 인해 `itemCount`와 각 아이템의 `itemIndex`를 다시 계산해야 하는지 여부를 나타냅니다. /// protected bool needUpdateItemList = true; /// /// 현재 선택된 아이템의 데이터 인덱스(`index`)입니다. /// 선택된 아이템이 없으면 -1입니다. /// protected int selectDataIndex = -1; /// /// 아이템이 선택되었을 때 호출될 콜백 함수들의 체인입니다. /// protected Action selectCallback = null; /// /// 특정 `InfiniteScrollData`가 전체 데이터 리스트에서 몇 번째에 있는지 인덱스를 찾습니다. /// /// 찾고자 하는 데이터 객체입니다. /// 데이터를 찾으면 해당 인덱스를, 찾지 못하면 -1을 반환합니다. /// /// /// MyData dataToFind = new MyData("찾을 아이템"); /// int index = infiniteScroll.GetDataIndex(dataToFind); /// if (index != -1) /// { /// Debug.Log($"데이터를 찾았습니다. 인덱스: {index}"); /// } /// /// public int GetDataIndex(InfiniteScrollData data) { if (isInitialize == false) { Initialize(); } return dataList.FindIndex((context) => { return context.data.Equals(data); }); } /// /// 스크롤에 추가된 전체 데이터의 개수를 반환합니다. /// 필터링 여부와 관계없이 모든 데이터의 수를 반환합니다. /// /// 전체 데이터 개수 public int GetDataCount() { return dataList.Count; } /// /// 지정된 인덱스에 해당하는 `InfiniteScrollData`를 가져옵니다. /// /// 가져올 데이터의 인덱스 (전체 데이터 리스트 기준) /// 해당 인덱스의 `InfiniteScrollData` 객체 public InfiniteScrollData GetData(int index) { return dataList[index].data; } /// /// 스크롤에 있는 모든 원본 데이터(`InfiniteScrollData`)를 리스트로 복사하여 반환합니다. /// /// 모든 데이터가 담긴 새로운 리스트 public List GetDataList() { List list = new List(); for (int index = 0; index < dataList.Count; index++) { list.Add(dataList[index].data); } return list; } /// /// 현재 화면에 표시될 수 있는 (필터링되지 않은) 아이템들의 원본 데이터 리스트를 반환합니다. /// /// 표시 가능한 아이템의 데이터가 담긴 새로운 리스트 public List GetItemList() { List list = new List(); for (int index = 0; index < dataList.Count; index++) { // itemIndex가 -1이 아니라는 것은 필터링을 통과한 표시 가능한 아이템임을 의미합니다. if (dataList[index].itemIndex != -1) { list.Add(dataList[index].data); } } return list; } /// /// 현재 화면에 표시될 수 있는 (필터링되지 않은) 아이템의 총 개수를 반환합니다. /// /// 표시 가능한 아이템의 개수 public int GetItemCount() { return itemCount; } /// /// 특정 데이터가 현재 표시되는 아이템 리스트에서 몇 번째 인덱스인지 가져옵니다. /// /// 찾고자 하는 데이터 객체입니다. /// 표시되는 아이템 리스트 내의 인덱스. 데이터가 없거나 필터링되어 보이지 않으면 -1을 반환합니다. public int GetItemIndex(InfiniteScrollData data) { var context = GetDataContext(data); return context.itemIndex; } /// /// 아이템 선택 시 호출될 콜백 함수를 등록합니다. /// /// 등록할 콜백 함수. 선택된 아이템의 `InfiniteScrollData`를 인자로 받습니다. /// /// /// void Start() /// { /// infiniteScroll.AddSelectCallback(OnItemSelected); /// } /// /// private void OnItemSelected(InfiniteScrollData data) /// { /// MyData myData = data as MyData; /// if (myData != null) /// { /// Debug.Log($"아이템 선택됨: {myData.title}"); /// } /// } /// /// public void AddSelectCallback(Action callback) { if (isInitialize == false) { Initialize(); } selectCallback += callback; } /// /// 등록했던 아이템 선택 콜백 함수를 제거합니다. /// /// 제거할 콜백 함수입니다. public void RemoveSelectCallback(Action callback) { if (isInitialize == false) { Initialize(); } selectCallback -= callback; } /// /// 아이템의 활성/비활성 상태가 변경될 때 이벤트를 발생시킵니다. (내부 호출용) /// /// 상태가 변경된 아이템의 데이터 인덱스 /// 활성화 여부 public void OnChangeActiveItem(int dataIndex, bool active) { onChangeActiveItem.Invoke(dataIndex, active); } /// /// `InfiniteScrollData`에 해당하는 `DataContext`를 찾아서 반환합니다. /// /// 찾고자 하는 데이터 객체 /// 찾은 `DataContext` 객체. 없으면 null을 반환합니다. protected DataContext GetDataContext(InfiniteScrollData data) { if (isInitialize == false) { Initialize(); } return dataList.Find((context) => { return context.data.Equals(data); }); } /// /// 표시되는 아이템의 인덱스(`itemIndex`)를 사용하여 `DataContext`를 찾습니다. /// /// 찾고자 하는 아이템의 인덱스 /// 찾은 `DataContext` 객체. 없으면 null을 반환합니다. protected DataContext GetContextFromItem(int itemIndex) { if (isInitialize == false) { Initialize(); } if (IsValidItemIndex(itemIndex) == true) { return GetItem(itemIndex); } else { return null; } } /// /// 데이터 리스트의 맨 끝에 새로운 데이터를 추가합니다. /// /// 추가할 데이터 protected void AddData(InfiniteScrollData data) { DataContext addData = new DataContext(data, dataList.Count); InitFitContext(addData); dataList.Add(addData); CheckItemAfterAddData(addData); } /// /// 데이터가 추가된 후, 해당 데이터가 필터링되지 않고 표시되어야 하는지 확인하고 /// `itemIndex`를 재정렬하는 등의 후속 처리를 합니다. /// /// 새로 추가된 데이터의 `DataContext` /// 데이터가 성공적으로 아이템 리스트에 추가되었으면 true, 필터링되었으면 false를 반환합니다. private bool CheckItemAfterAddData(DataContext addData) { // 필터가 설정되어 있고, 필터 조건에 의해 걸러진다면 아무것도 하지 않습니다. if (onFilter != null && onFilter(addData.data) == true) { return false; } // 새로 추가된 아이템이 들어갈 위치(itemIndex)를 찾습니다. int itemIndex = 0; if (itemCount > 0) { // 추가된 데이터의 바로 앞 데이터부터 역순으로 탐색하여 // 가장 먼저 발견되는 표시 가능한 아이템의 다음 자리를 찾습니다. for (int dataIndex = addData.index - 1; dataIndex >= 0; dataIndex--) { if (dataList[dataIndex].itemIndex != -1) { itemIndex = dataList[dataIndex].itemIndex + 1; break; } } } addData.itemIndex = itemIndex; itemCount++; // 새로 추가된 아이템보다 뒤에 있는 모든 아이템들의 itemIndex를 1씩 증가시킵니다. for (int dataIndex = addData.index + 1; dataIndex < dataList.Count; dataIndex++) { if (dataList[dataIndex].itemIndex != -1) { dataList[dataIndex].itemIndex++; } } // 레이아웃을 다시 계산해야 함을 표시합니다. needReBuildLayout = true; return true; } /// /// 지정된 인덱스에 새로운 데이터를 삽입합니다. /// /// 삽입할 데이터 /// 데이터를 삽입할 위치의 인덱스 protected void InsertData(InfiniteScrollData data, int insertIndex) { if (insertIndex < 0 || insertIndex > dataList.Count) { throw new ArgumentOutOfRangeException(); } if (insertIndex < dataList.Count) { DataContext addData = new DataContext(data, insertIndex); InitFitContext(addData); // 삽입 위치보다 뒤에 있는 모든 데이터들의 index를 1씩 증가시킵니다. for (int dataIndex = insertIndex; dataIndex < dataList.Count; dataIndex++) { dataList[dataIndex].index++; } dataList.Insert(insertIndex, addData); CheckItemAfterAddData(addData); } else { // 삽입 위치가 맨 끝이면 AddData와 동일합니다. AddData(data); } } /// /// `DataContext`의 아이템 크기를 초기화합니다. /// `dynamicItemSize`가 true이면 데이터에 저장된 크기를 사용하고, 아니면 기본 크기를 사용합니다. /// /// 초기화할 `DataContext` protected void InitFitContext(DataContext context) { float size = layout.GetMainSize(defaultItemPrefabSize); if (dynamicItemSize == true) { float ItemSize = context.GetItemSize(); if (ItemSize != 0) { size = ItemSize; } } context.SetItemSize(size); } /// /// 주어진 인덱스가 전체 데이터 리스트(`dataList`) 범위 내에 있는지 확인합니다. /// /// 확인할 데이터 인덱스 /// 유효한 인덱스이면 true, 아니면 false protected bool IsValidDataIndex(int index) { return (index >= 0 && index < dataList.Count) ? true : false; } /// /// 주어진 인덱스가 표시 가능한 아이템 리스트(`itemCount`) 범위 내에 있는지 확인합니다. /// /// 확인할 아이템 인덱스 /// 유효한 인덱스이면 true, 아니면 false protected bool IsValidItemIndex(int index) { return (index >= 0 && index < itemCount) ? true : false; } /// /// 필터링 조건(`onFilter`)에 따라 전체 데이터 리스트를 순회하며 /// 각 데이터의 `itemIndex`를 재설정하고, 표시 가능한 아이템의 총 개수(`itemCount`)를 다시 계산합니다. /// protected void BuildItemList() { itemCount = 0; for (int i = 0; i < dataList.Count; i++) { DataContext context = dataList[i]; // 필터가 있고, 필터 조건에 걸러진다면 if (onFilter != null && onFilter(context.data) == true) { // UI 아이템과의 연결을 끊고, itemIndex를 -1로 만듭니다. context.UnlinkItem(false); continue; } // 필터를 통과했다면, 순서대로 itemIndex를 부여합니다. context.itemIndex = itemCount; itemCount++; } // 아이템 리스트가 변경되었으므로 레이아웃을 다시 계산해야 합니다. needReBuildLayout = true; } /// /// 아이템이 선택되었을 때 내부적으로 호출되는 메서드입니다. /// `selectDataIndex`를 갱신하고 등록된 콜백(`selectCallback`)을 실행합니다. /// /// 선택된 아이템의 데이터 private void OnSelectItem(InfiniteScrollData data) { int dataIndex = GetDataIndex(data); if (IsValidDataIndex(dataIndex) == true) { selectDataIndex = dataIndex; if (selectCallback != null) { selectCallback(data); } } } } }