Files
EnglewoodLAB/Assets/Scripts/NHN/InfiniteScroll.ItemData.cs

542 lines
21 KiB
C#

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