Files
XRLib/Assets/Sample/Scripts/NHN/InfiniteScroll.cs
2026-02-25 17:24:42 +09:00

686 lines
21 KiB
C#

using System;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
namespace Gpm.Ui
{
/// <summary>
/// 무한 스크롤 기능을 구현하는 클래스입니다.
/// 이 클래스는 UI 항목의 동적 로딩 및 재활용을 처리하여 성능을 최적화합니다.
/// </summary>
public partial class InfiniteScroll : MonoBehaviour
{
// 초기화 여부를 확인하는 플래그입니다.
protected bool isInitialize = false;
// 스크롤 뷰의 콘텐츠 RectTransform에 대한 참조입니다.
protected RectTransform content = null;
// 스크롤 값이 변경되었는지 여부를 나타내는 플래그입니다.
private bool changeValue = false;
[Header("Event", order = 4)]
/// <summary>
/// 스크롤 값이 변경될 때 발생하는 이벤트입니다.
/// 매개변수: int (첫 번째 보이는 아이템 인덱스), int (마지막 보이는 아이템 인덱스), bool (시작 라인 여부), bool (끝 라인 여부)
/// </summary>
/// <example>
/// <code>
/// infiniteScroll.onChangeValue.AddListener((first, last, isStart, isEnd) => {
/// Debug.Log($"First visible item: {first}, Last visible item: {last}, Is at start: {isStart}, Is at end: {isEnd}");
/// });
/// </code>
/// </example>
public ChangeValueEvent onChangeValue = new ChangeValueEvent();
/// <summary>
/// 아이템의 활성 상태가 변경될 때 발생하는 이벤트입니다.
/// 매개변수: int (데이터 인덱스), bool (활성 상태)
/// </summary>
/// <example>
/// <code>
/// infiniteScroll.onChangeActiveItem.AddListener((dataIndex, isActive) => {
/// Debug.Log($"Item {dataIndex} is now {(isActive ? "active" : "inactive")}");
/// });
/// </code>
/// </example>
public ItemActiveEvent onChangeActiveItem = new ItemActiveEvent();
/// <summary>
/// 스크롤이 시작 지점에 도달했을 때 발생하는 이벤트입니다.
/// 매개변수: bool (시작 라인 여부)
/// </summary>
/// <example>
/// <code>
/// infiniteScroll.onStartLine.AddListener((isAtStart) => {
/// if (isAtStart) {
/// Debug.Log("Scrolled to the beginning.");
/// }
/// });
/// </code>
/// </example>
public StateChangeEvent onStartLine = new StateChangeEvent();
/// <summary>
/// 스크롤이 끝 지점에 도달했을 때 발생하는 이벤트입니다.
/// 매개변수: bool (끝 라인 여부)
/// </summary>
/// <example>
/// <code>
/// infiniteScroll.onEndLine.AddListener((isAtEnd) => {
/// if (isAtEnd) {
/// Debug.Log("Scrolled to the end.");
/// }
/// });
/// </code>
/// </example>
public StateChangeEvent onEndLine = new StateChangeEvent();
// 데이터 필터링에 사용되는 델리게이트입니다.
private Predicate<InfiniteScrollData> onFilter = null;
private void Awake()
{
Initialize();
}
/// <summary>
/// InfiniteScroll을 초기화합니다.
/// ScrollRect 컴포넌트를 가져오고, 이벤트를 설정하며, 초기 아이템을 생성합니다.
/// </summary>
protected void Initialize()
{
if (isInitialize == false)
{
scrollRect = GetComponent<ScrollRect>();
content = scrollRect.content;
viewport = scrollRect.viewport;
CheckScrollAxis();
ClearScrollContent();
RectTransform itemTransform = (RectTransform)itemPrefab.transform;
defaultItemPrefabSize = itemTransform.sizeDelta;
itemObjectList.Clear();
dataList.Clear();
scrollRect.onValueChanged.AddListener(OnValueChanged);
CreateNeedItem();
CheckScrollData();
isInitialize = true;
needReBuildLayout = true;
}
}
/// <summary>
/// 스크롤에 단일 데이터 항목을 삽입합니다.
/// </summary>
/// <param name="data">삽입할 데이터입니다.</param>
/// <param name="immediately">true이면 즉시 레이아웃을 업데이트합니다.</param>
/// <example>
/// <code>
/// InfiniteScrollData myData = new InfiniteScrollData();
/// infiniteScroll.InsertData(myData, true);
/// </code>
/// </example>
public void InsertData(InfiniteScrollData data, bool immediately = false)
{
if (isInitialize == false)
{
Initialize();
}
AddData(data);
UpdateAllData(immediately);
}
/// <summary>
/// 지정된 인덱스에 단일 데이터 항목을 삽입합니다.
/// </summary>
/// <param name="data">삽입할 데이터입니다.</param>
/// <param name="insertIndex">데이터를 삽입할 인덱스입니다.</param>
/// <param name="immediately">true이면 즉시 레이아웃을 업데이트합니다.</param>
/// <example>
/// <code>
/// InfiniteScrollData myData = new InfiniteScrollData();
/// infiniteScroll.InsertData(myData, 0, true);
/// </code>
/// </example>
public void InsertData(InfiniteScrollData data, int insertIndex, bool immediately = false)
{
if (insertIndex < 0 || insertIndex > dataList.Count)
{
throw new ArgumentOutOfRangeException();
}
if (isInitialize == false)
{
Initialize();
}
InsertData(data, insertIndex);
UpdateAllData(immediately);
}
/// <summary>
/// 스크롤에 여러 데이터 항목을 삽입합니다.
/// </summary>
/// <param name="datas">삽입할 데이터 배열입니다.</param>
/// <param name="immediately">true이면 즉시 레이아웃을 업데이트합니다.</param>
/// <example>
/// <code>
/// InfiniteScrollData[] myDataArray = new InfiniteScrollData[10];
/// // myDataArray 초기화
/// infiniteScroll.InsertData(myDataArray, true);
/// </code>
/// </example>
public void InsertData(InfiniteScrollData[] datas, bool immediately = false)
{
if (isInitialize == false)
{
Initialize();
}
foreach (InfiniteScrollData data in datas)
{
AddData(data);
}
UpdateAllData(immediately);
}
/// <summary>
/// 지정된 인덱스부터 여러 데이터 항목을 삽입합니다.
/// </summary>
/// <param name="datas">삽입할 데이터 배열입니다.</param>
/// <param name="insertIndex">데이터 삽입을 시작할 인덱스입니다.</param>
/// <param name="immediately">true이면 즉시 레이아웃을 업데이트합니다.</param>
/// <example>
/// <code>
/// InfiniteScrollData[] myDataArray = new InfiniteScrollData[10];
/// // myDataArray 초기화
/// infiniteScroll.InsertData(myDataArray, 5, true);
/// </code>
/// </example>
public void InsertData(InfiniteScrollData[] datas, int insertIndex, bool immediately = false)
{
if (insertIndex < 0 || insertIndex > dataList.Count)
{
throw new ArgumentOutOfRangeException();
}
if (isInitialize == false)
{
Initialize();
}
foreach (InfiniteScrollData data in datas)
{
InsertData(data, insertIndex++);
}
UpdateAllData(immediately);
}
/// <summary>
/// 지정된 데이터 항목을 제거합니다.
/// </summary>
/// <param name="data">제거할 데이터입니다.</param>
/// <param name="immediately">true이면 즉시 레이아웃을 업데이트합니다.</param>
/// <example>
/// <code>
/// InfiniteScrollData dataToRemove = infiniteScroll.GetData(0);
/// if (dataToRemove != null) {
/// infiniteScroll.RemoveData(dataToRemove, true);
/// }
/// </code>
/// </example>
public void RemoveData(InfiniteScrollData data, bool immediately = false)
{
if (isInitialize == false)
{
Initialize();
}
int dataIndex = GetDataIndex(data);
RemoveData(dataIndex, immediately);
}
/// <summary>
/// 지정된 인덱스의 데이터 항목을 제거합니다.
/// </summary>
/// <param name="dataIndex">제거할 데이터의 인덱스입니다.</param>
/// <param name="immediately">true이면 즉시 레이아웃을 업데이트합니다.</param>
/// <example>
/// <code>
/// infiniteScroll.RemoveData(0, true);
/// </code>
/// </example>
public void RemoveData(int dataIndex, bool immediately = false)
{
if (isInitialize == false)
{
Initialize();
}
if (IsValidDataIndex(dataIndex) == true)
{
selectDataIndex = -1;
int removeShowIndex = -1;
if(dataList[dataIndex].itemIndex != -1)
{
removeShowIndex = dataList[dataIndex].itemIndex;
}
dataList[dataIndex].UnlinkItem(true);
dataList.RemoveAt(dataIndex);
for(int i= dataIndex; i< dataList.Count;i++)
{
dataList[i].index--;
if(removeShowIndex != -1)
{
if (dataList[i].itemIndex != -1)
{
dataList[i].itemIndex--;
}
}
}
if (removeShowIndex != -1)
{
if (removeShowIndex < firstItemIndex)
{
firstItemIndex--;
}
if (removeShowIndex < lastItemIndex)
{
lastItemIndex--;
}
itemCount--;
}
needReBuildLayout = true;
UpdateAllData(immediately);
}
}
/// <summary>
/// 모든 데이터와 아이템 상태를 초기화하지만, 생성된 아이템 객체는 유지합니다.
/// </summary>
/// <param name="immediately">true이면 즉시 레이아웃을 업데이트합니다.</param>
/// <example>
/// <code>
/// infiniteScroll.ClearData(true);
/// </code>
/// </example>
public void ClearData(bool immediately = false)
{
if (isInitialize == false)
{
Initialize();
}
itemCount = 0;
selectDataIndex = -1;
dataList.Clear();
lineLayout.Clear();
layoutSize = 0;
lineCount = 0;
ClearItemsData();
lastItemIndex = 0;
firstItemIndex = 0;
showLineIndex = 0;
showLineCount = 0;
isStartLine = false;
isEndLine = false;
needUpdateItemList = true;
needReBuildLayout = true;
isUpdateArea = true;
onFilter = null;
ClearScrollContent();
cachedData.Clear();
UpdateAllData(immediately);
}
/// <summary>
/// 모든 데이터와 아이템 객체를 완전히 제거하고 스크롤을 초기 상태로 되돌립니다.
/// </summary>
/// <example>
/// <code>
/// infiniteScroll.Clear();
/// </code>
/// </example>
public void Clear()
{
if (isInitialize == false)
{
Initialize();
}
itemCount = 0;
selectDataIndex = -1;
dataList.Clear();
lineLayout.Clear();
layoutSize = 0;
lineCount = 0;
ClearItems();
lastItemIndex = 0;
firstItemIndex = 0;
showLineIndex = 0;
showLineCount = 0;
isStartLine = false;
isEndLine = false;
needUpdateItemList = true;
needReBuildLayout = true;
isUpdateArea = true;
onFilter = null;
cachedData.Clear();
ClearScrollContent();
}
/// <summary>
/// 기존 데이터 항목을 업데이트합니다.
/// </summary>
/// <param name="data">업데이트할 데이터입니다. 기존 데이터와 동일한 키를 가져야 합니다.</param>
/// <example>
/// <code>
/// InfiniteScrollData existingData = infiniteScroll.GetData(0);
/// if (existingData != null) {
/// // 데이터 속성 수정
/// infiniteScroll.UpdateData(existingData);
/// }
/// </code>
/// </example>
public void UpdateData(InfiniteScrollData data)
{
if (isInitialize == false)
{
Initialize();
}
var context = GetDataContext(data);
if (context != null)
{
context.UpdateData(data);
needReBuildLayout = true;
}
}
/// <summary>
/// 모든 데이터를 업데이트하고 스크롤 뷰를 새로 고칩니다.
/// </summary>
/// <param name="immediately">true이면 즉시 레이아웃을 업데이트합니다.</param>
/// <example>
/// <code>
/// infiniteScroll.UpdateAllData(true);
/// </code>
/// </example>
public void UpdateAllData(bool immediately = true)
{
if (isInitialize == false)
{
Initialize();
}
needReBuildLayout = true;
isUpdateArea = true;
CreateNeedItem();
if (immediately == true)
{
UpdateShowItem(true);
}
}
/// <summary>
/// 스크롤에 표시될 데이터를 필터링하는 조건을 설정합니다.
/// </summary>
/// <param name="onFilter">필터링을 위한 Predicate입니다. true를 반환하는 데이터만 표시됩니다.</param>
/// <example>
/// <code>
/// infiniteScroll.SetFilter(data => {
/// MyCustomData myData = data as MyCustomData;
/// return myData.someValue > 10;
/// });
/// infiniteScroll.UpdateAllData(true);
/// </code>
/// </example>
public void SetFilter(Predicate<InfiniteScrollData> onFilter)
{
this.onFilter = onFilter;
needUpdateItemList = true;
}
/// <summary>
/// 뷰포트의 크기를 가져옵니다.
/// </summary>
/// <returns>뷰포트의 주 축 크기입니다.</returns>
public float GetViewportSize()
{
return layout.GetMainSize(viewport);
}
/// <summary>
/// 콘텐츠의 전체 크기를 가져옵니다.
/// </summary>
/// <returns>콘텐츠의 주 축 크기입니다.</returns>
public float GetContentSize()
{
UpdateContentSize();
return layout.GetMainSize(content);
}
/// <summary>
/// 콘텐츠의 현재 위치를 가져옵니다.
/// </summary>
/// <returns>콘텐츠의 주 축 위치입니다.</returns>
public float GetContentPosition()
{
return layout.GetAxisPosition(content);
}
/// <summary>
/// 스크롤 뷰의 크기를 다시 계산하고 업데이트합니다.
/// </summary>
public void ResizeScrollView()
{
if (isInitialize == false)
{
Initialize();
}
UpdateContentSize();
}
/// <summary>
/// 지정된 아이템 인덱스의 위치를 가져옵니다.
/// </summary>
/// <param name="itemIndex">위치를 가져올 아이템의 인덱스입니다.</param>
/// <returns>아이템의 위치입니다.</returns>
public float GetItemPosition(int itemIndex)
{
float distance = GetItemDistance(itemIndex);
return -layout.GetAxisPostionFromOffset(distance);
}
/// <summary>
/// 스크롤 뷰를 수동으로 새로 고칩니다.
/// 아이템 목록이나 레이아웃에 변경이 있을 때 호출합니다.
/// </summary>
public void RefreshScroll()
{
if (isInitialize == false)
{
Initialize();
}
if (needUpdateItemList == true)
{
BuildItemList();
needUpdateItemList = false;
}
if (NeedUpdateItem() == true)
{
UpdateShowItem();
}
}
/// <summary>
/// 콘텐츠의 교차 축 크기를 가져옵니다.
/// </summary>
protected float GetCrossSize()
{
return layout.GetCrossSize(content.rect);
}
/// <summary>
/// 모든 아이템의 총 크기에 따라 콘텐츠의 크기를 조절합니다.
/// </summary>
protected void ResizeContent()
{
cachedData.contentSize = GetItemTotalSize();
content.sizeDelta = layout.GetAxisVector(-layout.padding, cachedData.contentSize);
}
/// <summary>
/// 필요한 경우 레이아웃을 다시 빌드하여 콘텐츠 크기를 업데이트합니다.
/// </summary>
protected void UpdateContentSize()
{
if (needReBuildLayout == true)
{
BuildLayout();
needReBuildLayout = false;
}
}
/// <summary>
/// 아이템을 업데이트해야 하는지 확인합니다.
/// </summary>
protected bool NeedUpdateItem()
{
CheckScrollData();
if (needReBuildLayout == true ||
isRebuildLayout == true ||
isUpdateArea == true)
{
return true;
}
return false;
}
/// <summary>
/// 지정된 위치가 현재 콘텐츠 위치보다 이전에 있는지 확인합니다. (화면 위/왼쪽)
/// </summary>
protected bool IsShowBeforePosition(float position, float contentPosition)
{
float viewPosition = position - contentPosition;
if (viewPosition < 0)
{
return true;
}
return false;
}
/// <summary>
/// 지정된 위치가 현재 뷰포트 크기를 벗어나는지 확인합니다. (화면 아래/오른쪽)
/// </summary>
protected bool IsShowAfterPosition(float position, float contentPosition, float viewportSize)
{
float viewPosition = position - contentPosition;
if (viewPosition >= viewportSize)
{
return true;
}
return false;
}
private void Update()
{
if (isInitialize == true)
{
RefreshScroll();
}
}
// Inspector에서 값이 변경될 때 호출됩니다.
private void OnValidate()
{
layout.SetDefaults();
}
/// <summary>
/// 스크롤 값 변경 시 발생하는 이벤트를 위한 클래스입니다.
/// </summary>
[Serializable]
public class ChangeValueEvent : UnityEvent<int, int, bool, bool>
{
public ChangeValueEvent()
{
}
}
/// <summary>
/// 아이템 활성 상태 변경 시 발생하는 이벤트를 위한 클래스입니다.
/// </summary>
[Serializable]
public class ItemActiveEvent : UnityEvent<int, bool>
{
public ItemActiveEvent()
{
}
}
/// <summary>
/// 스크롤 상태 변경(시작/끝 도달) 시 발생하는 이벤트를 위한 클래스입니다.
/// </summary>
[Serializable]
public class StateChangeEvent : UnityEvent<bool>
{
public StateChangeEvent()
{
}
}
}
}