686 lines
21 KiB
C#
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()
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|