Component List 개발 중

This commit is contained in:
logonkhi
2025-08-08 18:33:29 +09:00
parent 3297a5d1f3
commit 165c3a709f
60 changed files with 7809 additions and 3072 deletions

View File

@@ -1,16 +1,38 @@
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
namespace Gpm.Ui
{
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
[ExecuteAlways]
[RequireComponent(typeof(RectTransform))]
/// <summary>
/// 이 컴포넌트가 부착된 RectTransform의 크기가 변경될 때, 지정된 'target' RectTransform들의 크기를 동일하게 맞추는 역할을 합니다.
/// 주로 ContentSizeFitter와 함께 사용되어 동적으로 변하는 콘텐츠의 크기에 맞춰 배경 이미지 등의 크기를 조절할 때 유용합니다.
/// </summary>
/// <example>
/// <b>UI 계층 구조 예시:</b>
/// <code>
/// - Background (RectTransform)
/// - Content (RectTransform, VerticalLayoutGroup, ContentSizeFitter, ContentSizeSetter)
/// - Item1 (Image)
/// - Item2 (Image)
/// - ...
/// </code>
/// 위와 같은 구조에서 'Content' GameObject에 이 스크립트를 추가하고,
/// 'target' 배열에 'Background'의 RectTransform을 할당하면,
/// 'Content'의 크기가 내부 아이템(Item1, Item2)에 의해 변경될 때마다 'Background'의 크기도 함께 조절됩니다.
/// </example>
[ExecuteAlways] // 에디터 모드에서도 스크립트가 실행되도록 하여, UI 변경을 실시간으로 확인할 수 있게 합니다.
[RequireComponent(typeof(RectTransform))] // 이 컴포넌트는 RectTransform이 필수적으로 필요함을 명시합니다.
public class ContentSizeSetter : UIBehaviour
{
// 성능 최적화를 위해 RectTransform 컴포넌트를 캐싱하는 변수입니다.
[System.NonSerialized]
private RectTransform m_Rect;
/// <summary>
/// 이 컴포넌트가 부착된 GameObject의 RectTransform에 대한 참조입니다.
/// 처음 접근할 때 GetComponent를 통해 초기화하고, 이후에는 캐시된 값을 사용합니다.
/// </summary>
private RectTransform rectTransform
{
get
@@ -23,30 +45,54 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 크기를 조절할 때 추가할 여백(margin)입니다.
/// x, y 값을 설정하여 target의 너비와 높이에 각각 추가적인 공간을 줄 수 있습니다.
/// </summary>
public Vector2 margin;
/// <summary>
/// 이 컴포넌트의 크기에 맞춰 함께 크기가 조절될 RectTransform(들)의 배열입니다.
/// 인스펙터 창에서 크기를 동기화할 UI 요소들을 여기에 할당합니다.
/// </summary>
public RectTransform[] target;
/// <summary>
/// 컴포넌트가 활성화될 때 호출되는 Unity 생명주기 함수입니다.
/// 레이아웃을 다시 계산하도록 시스템에 요청합니다.
/// </summary>
protected override void OnEnable()
{
SetDirty();
}
/// <summary>
/// 이 컴포넌트가 부착된 RectTransform의 크기나 앵커 등이 변경될 때 호출됩니다.
/// 이 스크립트의 핵심 로직으로, target 배열에 있는 모든 RectTransform의 크기를 현재 RectTransform의 크기에 맞게 업데이트합니다.
/// </summary>
protected override void OnRectTransformDimensionsChange()
{
base.OnRectTransformDimensionsChange();
// 현재 RectTransform의 크기에 margin 값을 더하여 최종 크기를 계산합니다.
Vector2 sizeDelta = new Vector2(rectTransform.sizeDelta.x + margin.x, rectTransform.sizeDelta.y + margin.y);
if (target != null)
{
// target 배열의 모든 RectTransform에 대해 계산된 크기를 적용합니다.
for (int i = 0; i < target.Length; i++)
{
target[i].sizeDelta = sizeDelta;
// target의 레이아웃을 갱신하도록 표시하여 UI가 올바르게 다시 그려지도록 합니다.
LayoutRebuilder.MarkLayoutForRebuild(target[i]);
}
}
}
/// <summary>
/// UI 레이아웃을 다시 계산해야 함을 시스템에 알립니다.
/// </summary>
/// <param name="force">true일 경우, 즉시 레이아웃을 다시 계산합니다. false일 경우, 다음 프레임에 다시 계산하도록 예약합니다.</param>
protected void SetDirty(bool force = false)
{
if (IsActive() == false)
@@ -56,15 +102,21 @@ namespace Gpm.Ui
if (force == true)
{
// 즉시 레이아웃을 강제로 다시 계산합니다.
LayoutRebuilder.ForceRebuildLayoutImmediate(rectTransform);
}
else
{
// 다음 레이아웃 계산 주기에 맞춰 다시 계산하도록 표시만 해둡니다. (일반적인 경우)
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
}
}
#if UNITY_EDITOR
/// <summary>
/// (Unity 에디터 전용) 스크립트가 로드되거나 인스펙터에서 값이 변경될 때 호출됩니다.
/// 에디터에서 margin과 같은 값을 변경했을 때, 실시간으로 UI에 반영되도록 레이아웃 업데이트를 요청합니다.
/// </summary>
protected override void OnValidate()
{
SetDirty(false);

View File

@@ -1,51 +1,100 @@
using UnityEngine;
using UnityEngine.EventSystems;
namespace Gpm.Ui
{
using UnityEngine;
using UnityEngine.EventSystems;
/// <summary>
/// UI 요소를 마우스 드래그로 이동할 수 있게 만드는 컴포넌트입니다.
/// 이 컴포넌트를 UI GameObject에 추가하면 해당 UI를 화면 내에서 자유롭게 움직일 수 있습니다.
/// </summary>
/// <example>
/// <b>사용 방법:</b>
/// <code>
/// 1. 움직이게 하고 싶은 UI GameObject(예: Image, Panel)를 선택합니다.
/// 2. 인스펙터 창에서 [Add Component] 버튼을 누르고 'DraggableRect'를 검색하여 추가합니다.
/// 3. 'Drag Rect Transform' 필드에 움직일 대상을 지정합니다. 비워두면 컴포넌트가 부착된 GameObject 자신이 움직입니다.
/// 4. 게임을 실행하고 UI를 마우스로 드래그하면 이동하는 것을 확인할 수 있습니다.
/// </code>
public class DraggableRect : DragaEventHandler
{
/// <summary>
/// 드래그 시 실제로 위치가 변경될 UI의 RectTransform입니다.
/// 만약 비어있다면, 이 스크립트가 부착된 GameObject의 RectTransform이 기본값으로 사용됩니다.
/// </summary>
public RectTransform dragRectTransform;
// 성능 최적화를 위해 Canvas 컴포넌트를 캐싱하는 변수입니다.
private Canvas m_Canvas;
/// <summary>
/// 이 UI가 속해 있는 최상위 Canvas에 대한 참조입니다.
/// 드래그 이동량을 계산할 때 Canvas의 scaleFactor를 적용하여,
/// Canvas Scaler의 설정과 관계없이 일관된 속도로 움직이게 합니다.
/// </summary>
public Canvas canvas
{
get
{
if(m_Canvas == null)
{
// 컴포넌트의 부모 계층에서 Canvas를 찾아서 할당합니다.
m_Canvas = GetComponentInParent<Canvas>();
}
return m_Canvas;
}
}
/// <summary>
/// 컴포넌트가 활성화될 때 호출되는 Unity 생명주기 함수입니다.
/// 필요한 변수를 초기화하고, 드래그 이벤트를 리스너에 등록합니다.
/// </summary>
protected override void OnEnable()
{
if (dragRectTransform == null)
{
// dragRectTransform이 설정되지 않았다면, 현재 GameObject의 RectTransform으로 설정합니다.
dragRectTransform = gameObject.GetComponent<RectTransform>();
}
// 필수적인 RectTransform이나 Canvas가 없으면 컴포넌트를 비활성화합니다.
if (dragRectTransform == null || canvas == null)
{
enabled = false;
}
// 부모 클래스(DragaEventHandler)의 onDrag 이벤트에 OnDragMove 함수를 연결합니다.
// 이제 드래그가 발생할 때마다 OnDragMove 함수가 호출됩니다.
onDrag.AddListener(OnDragMove);
}
/// <summary>
/// 컴포넌트가 비활성화될 때 호출되는 Unity 생명주기 함수입니다.
/// 메모리 누수를 방지하기 위해 등록했던 이벤트 리스너를 제거합니다.
/// </summary>
protected override void OnDisable()
{
// OnEnable에서 연결했던 OnDragMove 함수를 onDrag 이벤트에서 제거합니다.
onDrag.RemoveListener(OnDragMove);
}
/// <summary>
/// 드래그 이벤트가 발생할 때 호출되는 함수입니다.
/// 마우스의 움직임에 따라 dragRectTransform의 위치를 업데이트합니다.
/// </summary>
/// <param name="eventData">드래그 이벤트와 관련된 정보(예: 마우스 이동량)를 담고 있습니다.</param>
public void OnDragMove(PointerEventData eventData)
{
// eventData.delta는 이번 프레임에서 마우스가 움직인 거리(픽셀 단위)입니다.
// 이를 canvas.scaleFactor로 나누어주어 Canvas 좌표계에 맞는 이동량으로 변환합니다.
// 변환된 값을 anchoredPosition에 더하여 UI를 이동시킵니다.
dragRectTransform.anchoredPosition += eventData.delta / canvas.scaleFactor;
}
/// <summary>
/// 이 컴포넌트의 부모 계층 구조가 변경될 때 호출됩니다.
/// (예: 다른 Canvas 하위로 이동될 경우)
/// 캐시된 Canvas 정보를 다시 최신 상태로 갱신합니다.
/// </summary>
protected override void OnCanvasHierarchyChanged()
{
m_Canvas = GetComponentInParent<Canvas>();

View File

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

View File

@@ -2,72 +2,169 @@ namespace Gpm.Ui
{
using System;
using System.Collections.Generic;
using UnityEngine.Events;
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)
@@ -81,34 +178,53 @@ namespace Gpm.Ui
});
}
/// <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>
/// 스크롤에 있는 모든 원본 데이터(`InfiniteScrollData`)를 리스트로 복사하여 반환합니다.
/// </summary>
/// <returns>모든 데이터가 담긴 새로운 리스트</returns>
public List<InfiniteScrollData> GetDataList()
{
List<InfiniteScrollData> list = new List<InfiniteScrollData>();
for(int index = 0; index < dataList.Count; index++)
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++)
{
if(dataList[index].itemIndex != -1)
// itemIndex가 -1이 아니라는 것은 필터링을 통과한 표시 가능한 아이템임을 의미합니다.
if (dataList[index].itemIndex != -1)
{
list.Add(dataList[index].data);
}
@@ -116,17 +232,47 @@ namespace Gpm.Ui
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)
@@ -137,6 +283,10 @@ namespace Gpm.Ui
selectCallback += callback;
}
/// <summary>
/// 등록했던 아이템 선택 콜백 함수를 제거합니다.
/// </summary>
/// <param name="callback">제거할 콜백 함수입니다.</param>
public void RemoveSelectCallback(Action<InfiniteScrollData> callback)
{
if (isInitialize == false)
@@ -147,11 +297,21 @@ namespace Gpm.Ui
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)
@@ -165,6 +325,11 @@ namespace Gpm.Ui
});
}
/// <summary>
/// 표시되는 아이템의 인덱스(`itemIndex`)를 사용하여 `DataContext`를 찾습니다.
/// </summary>
/// <param name="itemIndex">찾고자 하는 아이템의 인덱스</param>
/// <returns>찾은 `DataContext` 객체. 없으면 null을 반환합니다.</returns>
protected DataContext GetContextFromItem(int itemIndex)
{
if (isInitialize == false)
@@ -182,6 +347,10 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 데이터 리스트의 맨 끝에 새로운 데이터를 추가합니다.
/// </summary>
/// <param name="data">추가할 데이터</param>
protected void AddData(InfiniteScrollData data)
{
DataContext addData = new DataContext(data, dataList.Count);
@@ -192,17 +361,28 @@ namespace Gpm.Ui
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)
@@ -216,19 +396,25 @@ namespace Gpm.Ui
addData.itemIndex = itemIndex;
itemCount++;
for (int dataIndex = addData.index+1; dataIndex < dataList.Count; dataIndex++)
// 새로 추가된 아이템보다 뒤에 있는 모든 아이템들의 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)
@@ -240,7 +426,8 @@ namespace Gpm.Ui
{
DataContext addData = new DataContext(data, insertIndex);
InitFitContext(addData);
// 삽입 위치보다 뒤에 있는 모든 데이터들의 index를 1씩 증가시킵니다.
for (int dataIndex = insertIndex; dataIndex < dataList.Count; dataIndex++)
{
dataList[dataIndex].index++;
@@ -252,10 +439,16 @@ namespace Gpm.Ui
}
else
{
// 삽입 위치가 맨 끝이면 AddData와 동일합니다.
AddData(data);
}
}
/// <summary>
/// `DataContext`의 아이템 크기를 초기화합니다.
/// `dynamicItemSize`가 true이면 데이터에 저장된 크기를 사용하고, 아니면 기본 크기를 사용합니다.
/// </summary>
/// <param name="context">초기화할 `DataContext`</param>
protected void InitFitContext(DataContext context)
{
float size = layout.GetMainSize(defaultItemPrefabSize);
@@ -271,37 +464,58 @@ namespace Gpm.Ui
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);

View File

@@ -1,19 +1,37 @@
namespace Gpm.Ui
{
using UnityEngine;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 이 partial 클래스는 InfiniteScroll의 레이아웃 관련 로직을 담당합니다.
/// 아이템의 배치, 줄 계산, 스크롤 영역 업데이트 등 UI 레이아웃에 필요한 모든 계산을 처리합니다.
/// InfiniteScroll.cs의 일부로, 코드 관리를 용이하게 하기 위해 별도 파일로 분리되었습니다.
/// </summary>
public partial class InfiniteScroll
{
[Header("Layout", order = 3)]
[Tooltip("스크롤 뷰의 레이아웃 설정을 담고 있는 객체입니다. 인스펙터에서 스크롤 방향, 간격, 그리드 등을 설정할 수 있습니다.")]
public ScrollLayout layout = new ScrollLayout();
/// <summary>
/// 스크롤 뷰 내에서 한 줄(Row 또는 Column)의 레이아웃 정보를 관리하는 클래스입니다.
/// 예: 수직 스크롤에서는 하나의 가로 줄(Row)을 의미합니다.
/// </summary>
public class LineLayout
{
/// <summary>
/// LineLayout 생성자입니다.
/// </summary>
/// <param name="index">해당 줄의 인덱스입니다.</param>
public LineLayout(int index)
{
this.index = index;
}
/// <summary>
/// 줄의 모든 데이터를 초기화합니다.
/// </summary>
public void Clear()
{
dataList.Clear();
@@ -21,57 +39,144 @@ namespace Gpm.Ui
size = 0;
}
/// <summary>
/// 현재 줄에 데이터(아이템)를 추가하고, 줄의 전체 크기를 갱신합니다.
/// </summary>
/// <param name="context">추가할 아이템의 데이터 컨텍스트입니다.</param>
/// <returns>아이템 추가 후 갱신된 줄의 끝 위치를 반환합니다.</returns>
public float Add(DataContext context)
{
context.offset = offset;
dataList.Add(context);
// 아이템의 크기를 가져옵니다.
float contextSize = context.GetItemSize();
// 현재 줄의 크기(size)는 줄에 포함된 아이템들 중 가장 큰 아이템의 크기를 따릅니다.
if (size < contextSize)
{
size = contextSize;
}
// 이 줄의 시작 위치(offset)에 크기(size)를 더하여 이 줄이 차지하는 최종 위치를 반환합니다.
return offset + size;
}
/// <summary>
/// 이 줄의 인덱스입니다.
/// </summary>
internal int index = 0;
/// <summary>
/// 이 줄에 포함된 아이템들의 데이터 리스트입니다.
/// </summary>
internal List<DataContext> dataList = new List<DataContext>();
/// <summary>
/// 스크롤의 주 축(main axis)을 기준으로 이 줄의 시작 위치(offset)입니다.
/// </summary>
internal float offset = 0;
/// <summary>
/// 스크롤의 주 축을 기준으로 이 줄의 크기입니다.
/// 이 줄에 포함된 아이템 중 가장 큰 아이템의 크기와 같습니다.
/// </summary>
internal float size = 0;
/// <summary>
/// 이 줄에 포함된 아이템의 개수를 반환합니다.
/// </summary>
/// <returns>아이템 개수</returns>
public int GetCount()
{
return dataList.Count;
}
}
/// <summary>
/// 스크롤 뷰의 레이아웃 관련 상태를 캐싱하는 클래스입니다.
/// 매 프레임마다 모든 값을 다시 계산하는 것을 피하고, 변경이 있을 때만 레이아웃을 업데이트하여 성능을 최적화합니다.
/// </summary>
public class CachedScrollData
{
/// <summary>
/// 스크롤 콘텐츠의 현재 위치입니다.
/// </summary>
public float contentPosition = 0;
/// <summary>
/// 스크롤 콘텐츠의 전체 크기입니다.
/// </summary>
public float contentSize = 0;
/// <summary>
/// 스크롤 뷰포트(보여지는 영역)의 크기입니다.
/// </summary>
public float viewportSize = 0;
/// <summary>
/// 캐시된 콘텐츠 여백(padding)입니다.
/// </summary>
public Vector2 padding;
/// <summary>
/// 캐시된 아이템 간 간격(space)입니다.
/// </summary>
public Vector2 space;
/// <summary>
/// 캐시된 그리드의 수입니다. (예: 3열 그리드이면 3)
/// </summary>
public int gridCount = 0;
/// <summary>
/// 캐시된 스크롤 축 정보입니다.
/// </summary>
public ScrollAxis axis;
/// <summary>
/// 캐시된 수직 스크롤 방향입니다. (true: 위에서 아래로)
/// </summary>
public bool topToBotton = true;
/// <summary>
/// 캐시된 수평 스크롤 방향입니다. (true: 왼쪽에서 오른쪽으로)
/// </summary>
public bool leftToRight = true;
/// <summary>
/// 캐시된 그리드 레이아웃 값 리스트입니다.
/// </summary>
public List<ScrollLayout.LayoutValue> values = new List<ScrollLayout.LayoutValue>();
/// <summary>
/// 현재 스크롤이 수직인지 여부입니다.
/// </summary>
public bool IsVertical = false;
/// <summary>
/// 캐시된 아이템의 앵커 최소값입니다.
/// </summary>
public Vector2 anchorMin = Vector2.zero;
/// <summary>
/// 캐시된 아이템의 앵커 최대값입니다.
/// </summary>
public Vector2 anchorMax = Vector2.zero;
/// <summary>
/// 캐시된 아이템의 피벗입니다.
/// </summary>
public Vector2 itemPivot = Vector2.zero;
/// <summary>
/// 캐시된 콘텐츠의 피벗입니다.
/// </summary>
public Vector2 contentPivot = Vector2.zero;
/// <summary>
/// 캐시된 모든 데이터를 초기화합니다.
/// </summary>
public void Clear()
{
contentPosition = 0;
@@ -80,40 +185,50 @@ namespace Gpm.Ui
}
}
internal List<LineLayout> lineLayout = new List<LineLayout>();
internal float layoutSize = 0;
internal int lineCount = 0;
// 레이아웃 계산에 사용되는 내부 변수들
internal List<LineLayout> lineLayout = new List<LineLayout>(); // 전체 라인들의 레이아웃 정보를 담는 리스트
internal float layoutSize = 0; // 모든 라인들의 크기를 합한 총 레이아웃 크기
internal int lineCount = 0; // 현재 데이터로 계산된 총 라인 수
private int showLineIndex = 0;
private int showLineCount = 0;
private int showLineIndex = 0; // 화면에 보이는 첫 번째 라인의 인덱스
private int showLineCount = 0; // 화면에 보이는 총 라인의 수
private int firstItemIndex = 0;
private int lastItemIndex = 0;
private int firstItemIndex = 0; // 화면에 보이는 첫 번째 아이템의 인덱스
private int lastItemIndex = 0; // 화면에 보이는 마지막 아이템의 인덱스
private bool isUpdateArea = false;
private bool isRebuildLayout = false;
// 레이아웃 업데이트 상태를 관리하는 플래그
private bool isUpdateArea = false; // 스크롤 영역(위치, 크기)이 변경되었는지 여부
private bool isRebuildLayout = false; // 레이아웃을 처음부터 다시 빌드했는지 여부
protected bool needReBuildLayout = true;
protected bool needReBuildLayout = true; // 레이아웃을 다시 빌드해야 하는지 여부 (데이터 변경, 설정 변경 시 true)
private bool isStartLine = true;
private bool isEndLine = true;
private bool isStartLine = true; // 스크롤이 시작 지점에 있는지 여부
private bool isEndLine = true; // 스크롤이 끝 지점에 있는지 여부
internal bool processing = false;
internal bool processing = false; // 현재 레이아웃 업데이트가 진행 중인지 여부 (중복 실행 방지)
// 레이아웃 상태 캐싱을 위한 객체
private CachedScrollData cachedData = new CachedScrollData();
// 앵커 업데이트가 필요한지 여부
internal bool anchorUpdate = false;
// 구버전과의 호환성을 위한 필드. 현재는 layout.padding을 사용합니다.
[SerializeField]
[HideInInspector]
[System.Obsolete("padding is obsolete. Use GetMainPadding() instead (UnityUpgradable) -> GetMainPadding()", false)]
private int padding = 0;
// 구버전과의 호환성을 위한 필드. 현재는 layout.space를 사용합니다.
[SerializeField]
[HideInInspector]
[System.Obsolete("space is obsolete. Use GetMainSpace() instead (UnityUpgradable) -> GetMainSpace()", false)]
private int space = 0;
/// <summary>
/// 스크롤 축(방향)을 설정합니다.
/// </summary>
/// <param name="axis">설정할 스크롤 축</param>
public void SetScrollAxis(ScrollAxis axis)
{
layout.axis = axis;
@@ -122,33 +237,59 @@ namespace Gpm.Ui
CheckScrollData();
}
/// <summary>
/// 현재 설정된 스크롤 축을 가져옵니다.
/// </summary>
/// <returns>현재 스크롤 축</returns>
public ScrollAxis GetScrollAxis()
{
return layout.axis;
}
/// <summary>
/// 콘텐츠의 여백(padding)을 설정합니다.
/// </summary>
/// <param name="padding">x: 가로 여백, y: 세로 여백</param>
public void SetPadding(Vector2 padding)
{
layout.padding = padding;
}
/// <summary>
/// 아이템 간의 간격(space)을 설정합니다.
/// </summary>
/// <param name="space">x: 가로 간격, y: 세로 간격</param>
public void SetSpace(Vector2 space)
{
layout.space = space;
}
/// <summary>
/// 현재 설정된 여백(padding)을 가져옵니다.
/// </summary>
/// <returns>현재 여백</returns>
public Vector2 GetPadding()
{
return layout.padding;
}
/// <summary>
/// 현재 설정된 아이템 간 간격(space)을 가져옵니다.
/// </summary>
/// <returns>현재 간격</returns>
public Vector2 GetSpace()
{
return layout.space;
}
/// <summary>
/// 스크롤 주 축 방향의 여백을 가져옵니다.
/// </summary>
/// <returns>주 축 여백</returns>
public float GetMainPadding()
{
#pragma warning disable 618
// 구버전 호환성 코드
if (padding > 0)
{
layout.MainPadding = padding;
@@ -158,9 +299,14 @@ namespace Gpm.Ui
return layout.MainPadding;
}
/// <summary>
/// 스크롤 주 축 방향의 아이템 간 간격을 가져옵니다.
/// </summary>
/// <returns>주 축 간격</returns>
public float GetMainSpace()
{
#pragma warning disable 618
// 구버전 호환성 코드
if (space > 0)
{
layout.MainSpace = space;
@@ -170,27 +316,39 @@ namespace Gpm.Ui
return layout.MainSpace;
}
/// <summary>
/// 스크롤 교차 축 방향의 여백을 가져옵니다.
/// </summary>
/// <returns>교차 축 여백</returns>
public float GetCrossPadding()
{
return layout.CrossPadding;
}
/// <summary>
/// 스크롤 교차 축 방향의 아이템 간 간격을 가져옵니다.
/// </summary>
/// <returns>교차 축 간격</returns>
public float GetCrossSpace()
{
return layout.CrossSpace;
}
/// <summary>
/// 레이아웃 관련 데이터의 변경 사항을 확인하고, 업데이트가 필요한 경우 관련 플래그를 설정합니다.
/// 이 메서드는 매 프레임 호출되어 스크롤 위치, 뷰포트 크기, 레이아웃 설정 등의 변경을 감지합니다.
/// </summary>
protected void CheckScrollData()
{
float contentPosition = GetContentPosition();
float viewportSize = GetViewportSize();
float contentSize = GetContentSize();
// 스크롤 위치, 콘텐츠 크기, 뷰포트 크기 변경 감지
if (cachedData.contentPosition != contentPosition)
{
cachedData.contentPosition = contentPosition;
isUpdateArea = true;
isUpdateArea = true; // 보이는 영역이 변경되었으므로 업데이트 필요
}
if (cachedData.contentSize != contentSize)
@@ -205,12 +363,14 @@ namespace Gpm.Ui
isUpdateArea = true;
}
// 간격(space) 변경 감지
if (cachedData.space != layout.space)
{
cachedData.space = layout.space;
needReBuildLayout = true;
needReBuildLayout = true; // 레이아웃 구조가 변경되었으므로 재계산 필요
}
// 그리드 수 또는 그리드 설정값 변경 감지
if (cachedData.padding != layout.padding)
{
cachedData.padding = layout.padding;
@@ -226,15 +386,15 @@ namespace Gpm.Ui
if (layout.values.Count > 0)
{
for(int i=0;i< layout.values.Count;i++)
for (int i = 0; i < layout.values.Count; i++)
{
if(cachedData.values.Count < i+1)
if (cachedData.values.Count < i + 1)
{
cachedData.values.Add(new ScrollLayout.LayoutValue());
needReBuildLayout = true;
}
if( cachedData.values[i].valueType != layout.values[i].valueType ||
if (cachedData.values[i].valueType != layout.values[i].valueType ||
cachedData.values[i].value != layout.values[i].value)
{
cachedData.values[i].valueType = layout.values[i].valueType;
@@ -246,6 +406,7 @@ namespace Gpm.Ui
}
bool isUpdateLayout = false;
// 스크롤 축, 방향 등 레이아웃의 근본적인 구조 변경 감지
if (cachedData.axis != layout.axis)
{
cachedData.axis = layout.axis;
@@ -255,6 +416,7 @@ namespace Gpm.Ui
{
cachedData.IsVertical = IsVertical;
// ScrollRect의 스크롤 방향을 동기화
if (layout.IsVertical() == true)
{
scrollRect.vertical = true;
@@ -280,10 +442,12 @@ namespace Gpm.Ui
isUpdateLayout = true;
}
// 레이아웃 구조가 변경되었다면, 앵커와 피벗을 다시 계산하고 적용
if (isUpdateLayout == true)
{
anchorUpdate = true;
// 아이템과 콘텐츠의 앵커, 피벗 값을 새로 계산하여 캐시
Rect anchor = layout.GetItemAnchor();
cachedData.anchorMin = new Vector2(anchor.xMin, anchor.yMin);
cachedData.anchorMax = new Vector2(anchor.xMax, anchor.yMax);
@@ -291,58 +455,81 @@ namespace Gpm.Ui
cachedData.itemPivot = layout.GetItemPivot();
cachedData.contentPivot = layout.GetAxisPivot();
// 변경된 축 정보를 실제 RectTransform에 적용
UpdateAxis();
isUpdateArea = true;
isUpdateArea = true; // 영역 업데이트 필요
isUpdateLayout = false;
anchorUpdate = false;
}
}
/// <summary>
/// 캐시된 앵커 및 피벗 값을 콘텐츠와 아이템들의 RectTransform에 적용합니다.
/// 스크롤 방향이 변경될 때 호출됩니다.
/// </summary>
private void UpdateAxis()
{
// 스크롤 위치를 보존하기 위해 현재 normalizedPosition을 저장
Vector2 normalizedPosition = scrollRect.normalizedPosition;
// 콘텐츠(Content)의 앵커와 피벗을 설정
content.anchorMin = cachedData.anchorMin;
content.anchorMax = cachedData.anchorMax;
content.pivot = cachedData.contentPivot;
// 현재 활성화된 모든 아이템 객체들의 앵커와 피벗도 동일하게 설정
for (int index = 0; index < itemObjectList.Count; ++index)
{
itemObjectList[index].SetAxis(cachedData.anchorMin, cachedData.anchorMax, cachedData.itemPivot);
}
// 스크롤 위치 복원
scrollRect.normalizedPosition = normalizedPosition;
}
/// <summary>
/// ScrollRect의 스크롤 가능 여부(vertical/horizontal)를 현재 레이아웃 축 설정에 맞게 조정합니다.
/// </summary>
public void CheckScrollAxis()
{
layout.CheckAxis(scrollRect);
}
/// <summary>
/// 모든 아이템을 포함하는 데 필요한 총 거리를 계산합니다.
/// </summary>
protected float GetItemTotalSize()
{
return GetTotalDistance();
}
/// <summary>
/// 특정 아이템까지의 거리를 계산합니다.
/// </summary>
/// <param name="itemIndex">아이템의 인덱스</param>
/// <returns>스크롤 시작점부터 해당 아이템이 속한 라인까지의 거리</returns>
protected float GetItemDistance(int itemIndex)
{
int lineIndex = GetLineIndex(itemIndex);
return GetLineDistance(GetLineOffset(lineIndex), lineIndex);
}
/// <summary>
/// 전체 데이터 리스트를 기반으로 레이아웃을 처음부터 다시 계산하고 빌드합니다.
/// 데이터가 변경되거나 레이아웃 설정이 변경될 때 호출됩니다.
/// </summary>
protected void BuildLayout()
{
ClearLayout();
// 모든 데이터에 대해 반복
for (int dataIndex = 0; dataIndex < dataList.Count; dataIndex++)
{
var context = dataList[dataIndex];
// 유효한 아이템만 레이아웃에 추가
if (context.itemIndex == -1)
{
continue;
@@ -351,11 +538,16 @@ namespace Gpm.Ui
AddItem(context);
}
// 계산된 레이아웃 크기에 맞게 콘텐츠(Content)의 크기를 조절
ResizeContent();
isRebuildLayout = true;
isRebuildLayout = true; // 재빌드가 완료되었음을 표시
}
/// <summary>
/// 현재 스크롤 위치를 기준으로 화면에 보여야 할 라인(line)의 범위를 계산합니다.
/// `showLineIndex`와 `showLineCount`를 설정합니다.
/// </summary>
protected void CheckShowLine()
{
bool showLine = false;
@@ -363,14 +555,16 @@ namespace Gpm.Ui
showLineIndex = 0;
showLineCount = 0;
// 모든 라인을 순회하며 화면에 보이는지 검사
for (int lineIndex = 0; lineIndex < GetLineCount(); lineIndex++)
{
var line = GetLine(lineIndex);
if (showLine == false)
{
// 뷰포트 상단 이전에 끝나는 라인은 건너뛴다.
if (IsShowBeforePosition(GetLineDistance(line.offset + line.size, lineIndex), cachedData.contentPosition) == false)
{
showLineIndex = lineIndex;
showLineIndex = lineIndex; // 화면에 보이기 시작하는 첫 라인
showLine = true;
showLineCount++;
@@ -378,6 +572,7 @@ namespace Gpm.Ui
}
else
{
// 뷰포트 하단을 벗어나는 라인이 나오면 중단
if (IsShowAfterPosition(GetLineDistance(line.offset, lineIndex), cachedData.contentPosition, cachedData.viewportSize) == true)
{
break;
@@ -387,17 +582,25 @@ namespace Gpm.Ui
}
}
// 스크롤바 크기 조절
ResizeScrollView();
}
/// <summary>
/// 화면에 보이는 아이템들을 업데이트하고, 보이지 않는 아이템은 비활성화(재활용)합니다.
/// 스크롤이 움직일 때 주로 호출됩니다.
/// </summary>
/// <param name="forceUpdateData">true이면 보이는 모든 아이템의 데이터를 강제로 새로고침합니다.</param>
protected void UpdateShowItem(bool forceUpdateData = false)
{
// 이미 처리 중이면 중복 실행 방지
if (forceUpdateData == false &&
processing == true)
{
return;
}
// 아이템 업데이트가 필요 없으면 종료
if (NeedUpdateItem() == false)
{
return;
@@ -405,6 +608,7 @@ namespace Gpm.Ui
processing = true;
// 레이아웃 재빌드가 필요하면 실행
if (forceUpdateData == true ||
needReBuildLayout == true)
{
@@ -412,6 +616,7 @@ namespace Gpm.Ui
needReBuildLayout = false;
}
// 스크롤 영역이 변경되었거나 레이아웃이 재빌드되었다면, 보이는 라인을 다시 계산
if (forceUpdateData == true ||
isUpdateArea == true ||
isRebuildLayout == true)
@@ -422,6 +627,7 @@ namespace Gpm.Ui
isRebuildLayout = false;
}
// 보이는 아이템의 첫 인덱스와 마지막 인덱스를 계산
int prevFirstItemIndex = firstItemIndex;
firstItemIndex = GetLineFirstItemIndex(showLineIndex);
@@ -436,12 +642,15 @@ namespace Gpm.Ui
lastItemIndex = firstItemIndex;
}
// 보이는 아이템 범위가 변경되었는지 확인
if (prevFirstItemIndex < firstItemIndex ||
prevLastItemIndex > lastItemIndex)
{
changeValue = true;
}
// ** 아이템 비활성화 (재활용) **
// 현재 활성화된 아이템 객체들 중, 새로 계산된 보이는 범위(first ~ last)를 벗어난 것들을 비활성화
for (int index = 0; index < itemObjectList.Count; ++index)
{
int linkedIndex = itemObjectList[index].GetItemIndex();
@@ -455,6 +664,8 @@ namespace Gpm.Ui
}
}
// ** 아이템 활성화 및 업데이트 **
// 화면에 보여야 할 라인들을 순회
for (int lineIndex = showLineIndex; lineIndex < showLineIndex + showLineCount; lineIndex++)
{
if (lineIndex >= GetLineCount())
@@ -463,14 +674,17 @@ namespace Gpm.Ui
}
var line = GetLine(lineIndex);
// 해당 라인에 속한 모든 아이템에 대해
for (int i = 0; i < line.GetCount(); i++)
{
var context = line.dataList[i];
// 아이템 풀에서 재활용할 아이템 객체를 가져옴
InfiniteScrollItem item = PullItem(context);
bool needUpdateItemData = false;
// 아이템이 비활성 상태이거나, 데이터가 변경되었으면 데이터 업데이트 필요
if (item.IsActive() == false ||
item.GetDataIndex() != context.index ||
context.IsNeedUpdateItemData() == true)
@@ -479,11 +693,13 @@ namespace Gpm.Ui
changeValue = true;
}
// 데이터 업데이트가 필요하거나, 강제 업데이트 모드일 때
if (needUpdateItemData == true || forceUpdateData == true)
{
item.UpdateItem(context);
}
// 아이템이 비활성 상태였다면 활성화
if (item.IsActive() == false)
{
item.SetActive(true, true);
@@ -491,10 +707,12 @@ namespace Gpm.Ui
RectTransform itemTransform = (RectTransform)item.transform;
// 앵커와 피벗 설정
itemTransform.anchorMin = cachedData.anchorMin;
itemTransform.anchorMax = cachedData.anchorMax;
itemTransform.pivot = cachedData.itemPivot;
// 동적 아이템 크기 모드에서 크기 업데이트가 필요하면 적용
if (item.needUpdateItemSize == true)
{
float size = context.GetItemSize();
@@ -504,6 +722,7 @@ namespace Gpm.Ui
item.needUpdateItemSize = false;
}
// 최종 위치 계산 및 적용
FitItemPosition(itemTransform, context.itemIndex);
}
}
@@ -519,23 +738,37 @@ namespace Gpm.Ui
processing = false;
}
/// <summary>
/// 아이템의 RectTransform 위치를 계산하고 적용합니다.
/// </summary>
/// <param name="rectTransform">위치를 설정할 아이템의 RectTransform</param>
/// <param name="itemIndex">아이템의 인덱스</param>
protected void FitItemPosition(RectTransform rectTransform, int itemIndex)
{
// 교차 축 위치 설정 (예: 수직 스크롤에서 좌/우 위치)
layout.FitItemInlinePosition(rectTransform, itemIndex, GetCrossSize());
// 주 축 위치 설정 (예: 수직 스크롤에서 상/하 위치)
float itemPosition = GetItemPosition(itemIndex);
rectTransform.anchoredPosition = layout.GetAxisVector(Vector2.zero, itemPosition);
}
/// <summary>
/// 오프셋 값을 실제 anchoredPosition 값으로 변환합니다.
/// </summary>
protected float ItemPostionFromOffset(float offset)
{
float postion = layout.GetAxisPostionFromOffset(offset);
// 콘텐츠와 아이템의 피벗 차이를 보정
postion += GetPivotPostion();
return postion;
}
/// <summary>
/// 콘텐츠와 아이템의 피벗 차이로 인해 발생하는 위치 오차를 계산합니다.
/// </summary>
protected float GetPivotPostion()
{
float contentSize = GetContentSize();
@@ -549,12 +782,19 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 레이아웃 계산에 사용되는 변수들을 초기화합니다.
/// </summary>
protected void ClearLayout()
{
layoutSize = 0;
lineCount = 0;
}
/// <summary>
/// 데이터를 레이아웃에 추가합니다. 그리드 설정에 따라 자동으로 줄바꿈을 처리합니다.
/// </summary>
/// <param name="context">추가할 아이템의 데이터 컨텍스트</param>
protected void AddItem(DataContext context)
{
bool newLine = false;
@@ -563,11 +803,13 @@ namespace Gpm.Ui
int lineIndex = lineCount - 1;
if (lineCount == 0)
{
// 첫 아이템은 항상 새 라인에서 시작
newLine = true;
}
else if (lineIndex < lineCount)
{
currentLine = lineLayout[lineIndex];
// 현재 라인이 그리드 수만큼 꽉 찼으면 새 라인으로
if (currentLine.GetCount() >= layout.GridCount())
{
newLine = true;
@@ -582,6 +824,7 @@ namespace Gpm.Ui
{
lineIndex = lineCount;
// 기존에 생성된 LineLayout 객체가 있으면 재사용, 없으면 새로 생성
if (lineIndex < lineLayout.Count)
{
currentLine = lineLayout[lineIndex];
@@ -593,7 +836,7 @@ namespace Gpm.Ui
}
currentLine.Clear();
currentLine.offset = layoutSize;
currentLine.offset = layoutSize; // 새 라인의 시작 위치는 현재까지의 총 레이아웃 크기
newLine = false;
lineCount++;
@@ -603,36 +846,50 @@ namespace Gpm.Ui
currentLine = lineLayout[lineIndex];
}
// 현재 라인에 아이템을 추가하고, 레이아웃 전체 크기를 갱신
layoutSize = currentLine.Add(context);
}
/// <summary>
/// 아이템 인덱스로 해당 아이템의 데이터 컨텍스트를 가져옵니다.
/// </summary>
protected DataContext GetItem(int itemIndex)
{
if (layout.IsGrid() == true)
{
int gridCount = layout.GridCount();
int line = itemIndex / gridCount;
int index = itemIndex % gridCount;
int line = itemIndex / gridCount; // 아이템이 속한 라인 인덱스
int index = itemIndex % gridCount; // 라인 내에서의 인덱스
return lineLayout[line].dataList[index];
}
else
{
// 그리드가 아니면 아이템 인덱스가 라인 인덱스와 동일
return lineLayout[itemIndex].dataList[0];
}
}
/// <summary>
/// 라인 인덱스로 해당 라인의 LineLayout 객체를 가져옵니다.
/// </summary>
protected LineLayout GetLine(int lineIndex)
{
return lineLayout[lineIndex];
}
/// <summary>
/// 라인 인덱스로 해당 라인의 시작 위치(offset)를 가져옵니다.
/// </summary>
protected float GetLineOffset(int lineIndex)
{
return lineLayout[lineIndex].offset;
}
/// <summary>
/// 아이템 인덱스로 해당 아이템이 속한 라인의 시작 위치(offset)를 가져옵니다.
/// </summary>
protected float GetItemOffset(int itemIndex)
{
int lineIndex = GetLineIndex(itemIndex);
@@ -640,15 +897,21 @@ namespace Gpm.Ui
return lineLayout[lineIndex].offset;
}
/// <summary>
/// 아이템 인덱스로 해당 아이템이 속한 라인의 인덱스를 계산합니다.
/// </summary>
protected int GetLineIndex(int itemIndex)
{
if (layout.IsGrid() == true)
{
return itemIndex / layout.GridCount();
}
return itemIndex;
return itemIndex; // 그리드가 아니면 아이템 인덱스 = 라인 인덱스
}
/// <summary>
/// 해당 아이템이 라인의 마지막 아이템인지 확인합니다.
/// </summary>
protected bool IsLast(int itemIndex)
{
if (layout.IsGrid() == true)
@@ -661,27 +924,41 @@ namespace Gpm.Ui
return false;
}
return true;
return true; // 그리드가 아니면 모든 아이템이 마지막 아이템
}
/// <summary>
/// 현재 레이아웃의 총 라인 수를 가져옵니다.
/// </summary>
protected int GetLineCount()
{
return lineCount;
}
/// <summary>
/// 모든 아이템, 여백, 간격을 포함한 콘텐츠의 총 거리를 계산합니다.
/// </summary>
protected float GetTotalDistance()
{
int lineCount = GetLineCount();
// 총 라인 크기 + 상하/좌우 여백
float size = layoutSize + GetMainPadding() * UPDOWN_MULTIPLY;
if (lineCount > 1)
{
// 라인 간의 간격 추가
size += ((lineCount - 1) * GetMainSpace());
}
return size;
}
/// <summary>
/// 특정 라인까지의 실제 거리를 계산합니다. (여백, 간격 포함)
/// </summary>
/// <param name="offset">라인의 기본 오프셋</param>
/// <param name="lineIndex">라인의 인덱스</param>
/// <returns>스크롤 시작점부터의 실제 거리</returns>
protected float GetLineDistance(float offset, int lineIndex)
{
float distance = offset + GetMainPadding();
@@ -693,7 +970,9 @@ namespace Gpm.Ui
return distance;
}
/// <summary>
/// 특정 라인에 포함된 첫 번째 아이템의 인덱스를 가져옵니다.
/// </summary>
protected int GetLineFirstItemIndex(int lineIndex)
{
int lineCount = GetLineCount();
@@ -721,10 +1000,13 @@ namespace Gpm.Ui
return firstItemIndex;
}
/// <summary>
/// 특정 라인에 포함된 마지막 아이템의 인덱스를 가져옵니다.
/// </summary>
protected int GetLineLastItemIndex(int lineIndex)
{
int lineCount = GetLineCount();
if(lineCount == 0)
if (lineCount == 0)
{
return 0;
}
@@ -740,7 +1022,7 @@ namespace Gpm.Ui
{
lastItemIndex = lineIndex * layout.GridCount();
}
lastItemIndex += lineLayout[lineIndex].GetCount() - 1;
}
else
@@ -751,6 +1033,9 @@ namespace Gpm.Ui
return lastItemIndex;
}
/// <summary>
/// 특정 라인의 크기(주 축 기준)를 가져옵니다.
/// </summary>
protected float GetLineSize(int lineIndex)
{
int lineCount = GetLineCount();

View File

@@ -2,36 +2,74 @@ namespace Gpm.Ui
{
using System;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using UnityEngine.UI;
// 이 partial 클래스는 InfiniteScroll의 스크롤 이동 관련 로직을 담당합니다.
// 특정 아이템으로 이동하거나, 스크롤 위치를 직접 제어하는 기능을 구현합니다.
public partial class InfiniteScroll : IMoveScroll
{
/// <summary>
/// 특정 아이템으로 이동할 때, 뷰포트 내에서 해당 아이템을 정렬할 위치를 지정합니다.
/// </summary>
public enum MoveToType
{
/// <summary>
/// 아이템을 뷰포트의 상단(Vertical) 또는 왼쪽(Horizontal)에 맞춥니다.
/// </summary>
MOVE_TO_TOP = 0,
/// <summary>
/// 아이템을 뷰포트의 중앙에 맞춥니다.
/// </summary>
MOVE_TO_CENTER,
/// <summary>
/// 아이템을 뷰포트의 하단(Vertical) 또는 오른쪽(Horizontal)에 맞춥니다.
/// </summary>
MOVE_TO_BOTTOM
}
/// <summary>
/// 스크롤 이동 애니메이션을 제어하는 정적 클래스입니다.
/// ScrollMoveTo 컴포넌트를 사용하여 부드러운 스크롤 효과를 구현합니다.
/// </summary>
public static class Control
{
/// <summary>
/// 지정된 아이템 인덱스로 일정 시간 동안 부드럽게 스크롤합니다.
/// </summary>
/// <param name="scroll">대상 InfiniteScroll 인스턴스</param>
/// <param name="itemIndex">이동할 아이템의 인덱스</param>
/// <param name="moveToType">뷰포트 내 정렬 방식</param>
/// <param name="time">애니메이션 시간 (0이면 즉시 이동)</param>
public static void MoveTo(InfiniteScroll scroll, int itemIndex, MoveToType moveToType, float time = 0)
{
// ScrollMoveTo 컴포넌트가 없으면 동적으로 추가합니다.
ScrollMoveTo moveto = scroll.gameObject.GetComponent<ScrollMoveTo>();
if (moveto == null)
{
moveto = scroll.gameObject.AddComponent<ScrollMoveTo>();
}
// 이동 목표 설정
moveto.Set(itemIndex, moveToType, time);
// 애니메이션 커브 설정 (부드러운 시작과 끝)
moveto.curve = AnimationCurve.EaseInOut(0, 0, 1, 1);
// 이동 완료 후 컴포넌트 자동 파괴 설정
moveto.autoDestory = true;
// 애니메이션 재생
moveto.Play();
}
/// <summary>
/// 지정된 스크롤 비율(0.0 ~ 1.0)로 일정 시간 동안 부드럽게 스크롤합니다.
/// </summary>
/// <param name="scroll">대상 InfiniteScroll 인스턴스</param>
/// <param name="scrollRate">이동할 스크롤 비율 (0.0 = 시작, 1.0 = 끝)</param>
/// <param name="time">애니메이션 시간 (0이면 즉시 이동)</param>
public static void MoveTo(InfiniteScroll scroll, float scrollRate, float time = 0)
{
ScrollMoveTo moveto = scroll.gameObject.GetComponent<ScrollMoveTo>();
@@ -49,6 +87,8 @@ namespace Gpm.Ui
}
}
// 스크롤 위치 비교 시 오차를 보정하기 위한 값입니다. (콘텐츠 크기의 0.01%)
// 부동 소수점 연산의 정밀도 문제로 인해 정확히 0 또는 1이 되지 않는 경우를 처리합니다.
private const float UPDOWN_MULTIPLY = 2.0f;
private const int NEEDITEM_MORE_LINE = 1;
private const int NEEDITEM_EXTRA_ADD = 2;
@@ -57,13 +97,39 @@ namespace Gpm.Ui
protected RectTransform viewport = null;
protected float sizeInterpolationValue = 0.0001f; // 0.01%
/// <summary>
/// 특정 데이터(InfiniteScrollData)에 해당하는 아이템으로 스크롤을 이동합니다.
/// </summary>
/// <param name="data">이동할 대상 데이터</param>
/// <param name="moveToType">뷰포트 내 정렬 방식</param>
/// <param name="time">이동에 걸리는 시간 (0이면 즉시 이동)</param>
/// <example>
/// <code>
/// InfiniteScrollData targetData = myScroll.GetData(10);
/// if (targetData != null)
/// {
/// myScroll.MoveTo(targetData, InfiniteScroll.MoveToType.MOVE_TO_CENTER, 0.5f);
/// }
/// </code>
/// </example>
public void MoveTo(InfiniteScrollData data, MoveToType moveToType, float time = 0)
{
MoveTo(GetItemIndex(data), moveToType, time);
}
/// <summary>
/// 현재 화면에 보이는 아이템 목록 기준의 인덱스로 스크롤을 이동합니다.
/// </summary>
/// <param name="itemIndex">이동할 아이템의 인덱스 (화면에 보이는 아이템 기준)</param>
/// <param name="moveToType">뷰포트 내 정렬 방식</param>
/// <param name="time">이동에 걸리는 시간 (0이면 즉시 이동)</param>
/// <example>
/// <code>
/// // 50번째 아이템으로 0.3초 동안 스크롤 (아이템이 뷰포트 상단에 오도록)
/// myScroll.MoveTo(50, InfiniteScroll.MoveToType.MOVE_TO_TOP, 0.3f);
/// </code>
/// </example>
public void MoveTo(int itemIndex, MoveToType moveToType, float time = 0)
{
if (isInitialize == false)
@@ -75,16 +141,31 @@ namespace Gpm.Ui
{
if (time > 0)
{
// 시간이 지정된 경우 Control 클래스를 통해 부드럽게 이동
Control.MoveTo(this, itemIndex, moveToType, time);
}
else
{
// 즉시 이동
SetScrollPosition(GetMovePosition(itemIndex, moveToType));
Control.MoveTo(this, itemIndex, moveToType, 0);
}
}
}
/// <summary>
/// 전체 데이터 목록 기준의 인덱스로 스크롤을 이동합니다.
/// 필터링이 적용된 상태에서, 필터링과 상관없이 원본 데이터의 인덱스를 사용하고 싶을 때 유용합니다.
/// </summary>
/// <param name="dataIndex">이동할 데이터의 인덱스 (전체 데이터 리스트 기준)</param>
/// <param name="moveToType">뷰포트 내 정렬 방식</param>
/// <param name="time">이동에 걸리는 시간 (0이면 즉시 이동)</param>
/// <example>
/// <code>
/// // 원본 데이터 리스트의 100번째 데이터로 즉시 이동
/// myScroll.MoveToFromDataIndex(100, InfiniteScroll.MoveToType.MOVE_TO_CENTER);
/// </code>
/// </example>
public void MoveToFromDataIndex(int dataIndex, MoveToType moveToType, float time = 0)
{
if (isInitialize == false)
@@ -98,6 +179,17 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 전체 스크롤 범위의 특정 비율(0.0 ~ 1.0)로 스크롤을 이동합니다.
/// </summary>
/// <param name="scrollRate">이동할 비율 (0.0 = 시작, 0.5 = 중간, 1.0 = 끝)</param>
/// <param name="time">이동에 걸리는 시간 (0이면 즉시 이동)</param>
/// <example>
/// <code>
/// // 스크롤을 80% 위치로 1초 동안 이동
/// myScroll.MoveTo(0.8f, 1.0f);
/// </code>
/// </example>
public void MoveTo(float scrollRate, float time = 0)
{
if (isInitialize == false)
@@ -112,10 +204,22 @@ namespace Gpm.Ui
else
{
SetScrollPosition(GetMovePosition(scrollRate));
Control.MoveTo(this, scrollRate, 0);
Control.MoveTo(this, scrollRate, 0); // 위치 보정을 위해 호출
}
}
/// <summary>
/// 스크롤을 가장 처음 위치로 이동합니다.
/// </summary>
/// <example>
/// <code>
/// // '맨 위로' 버튼 클릭 시 호출
/// public void OnClick_MoveToTop()
/// {
/// myScroll.MoveToFirstData();
/// }
/// </code>
/// </example>
public void MoveToFirstData()
{
if (isInitialize == false)
@@ -128,11 +232,11 @@ namespace Gpm.Ui
Vector2 normalizedPosition;
if (layout.IsVertical() == true)
{
normalizedPosition = Vector2.one;
normalizedPosition = Vector2.one; // 세로 스크롤의 시작은 (0, 1)
}
else
{
normalizedPosition = Vector2.zero;
normalizedPosition = Vector2.zero; // 가로 스크롤의 시작은 (0, 0)
}
if (scrollRect.normalizedPosition != normalizedPosition)
@@ -141,9 +245,21 @@ namespace Gpm.Ui
isUpdateArea = true;
}
MoveTo(0);
MoveTo(0); // 0% 위치로 이동
}
/// <summary>
/// 스크롤을 가장 마지막 위치로 이동합니다.
/// </summary>
/// <example>
/// <code>
/// // '맨 아래로' 버튼 클릭 시 호출
/// public void OnClick_MoveToBottom()
/// {
/// myScroll.MoveToLastData();
/// }
/// </code>
/// </example>
public void MoveToLastData()
{
if (isInitialize == false)
@@ -156,11 +272,11 @@ namespace Gpm.Ui
Vector2 normalizedPosition;
if (layout.IsVertical() == true)
{
normalizedPosition = Vector2.zero;
normalizedPosition = Vector2.zero; // 세로 스크롤의 끝은 (0, 0)
}
else
{
normalizedPosition = Vector2.one;
normalizedPosition = Vector2.one; // 가로 스크롤의 끝은 (1, 0)
}
if (scrollRect.normalizedPosition != normalizedPosition)
{
@@ -168,9 +284,21 @@ namespace Gpm.Ui
isUpdateArea = true;
}
MoveTo(1);
MoveTo(1); // 100% 위치로 이동
}
/// <summary>
/// 스크롤이 가장 처음에 도달했는지 확인합니다.
/// </summary>
/// <returns>처음 위치에 있으면 true, 아니면 false를 반환합니다.</returns>
/// <example>
/// <code>
/// if (myScroll.IsMoveToFirstData())
/// {
/// Debug.Log("스크롤이 맨 위에 있습니다.");
/// }
/// </code>
/// </example>
public bool IsMoveToFirstData()
{
if (isInitialize == false)
@@ -190,8 +318,19 @@ namespace Gpm.Ui
return IsMoveToFirstData(contentPosition, viewportSize, contentSize);
}
/// <summary>
/// 스크롤이 가장 마지막에 도달했는지 확인합니다.
/// </summary>
/// <returns>마지막 위치에 있으면 true, 아니면 false를 반환합니다.</returns>
/// <example>
/// <code>
/// if (myScroll.IsMoveToLastData())
/// {
/// Debug.Log("스크롤이 맨 아래에 있습니다.");
/// // 추가 데이터 로딩 로직 호출 가능
/// }
/// </code>
/// </example>
public bool IsMoveToLastData()
{
if (isInitialize == false)
@@ -206,11 +345,17 @@ namespace Gpm.Ui
return IsMoveToLastData(contentPosition, viewportSize, contentSize);
}
/// <summary>
/// 지정된 스크롤 비율에 해당하는 content의 anchoredPosition을 계산합니다.
/// </summary>
/// <param name="scrollRate">계산할 스크롤 비율 (0.0 ~ 1.0)</param>
/// <returns>계산된 스크롤 위치(Vector2)</returns>
public Vector2 GetMovePosition(float scrollRate)
{
float viewportSize = GetViewportSize();
float contentSize = GetContentSize();
// 전체 스크롤 가능 범위에서 비율만큼의 위치를 계산
float move = (contentSize - viewportSize) * Mathf.Clamp01(scrollRate);
move = Math.Max(0.0f, move);
@@ -219,6 +364,12 @@ namespace Gpm.Ui
return layout.GetAxisVector(content.anchoredPosition, move);
}
/// <summary>
/// 지정된 아이템 인덱스로 이동하기 위한 content의 anchoredPosition을 계산합니다.
/// </summary>
/// <param name="itemIndex">대상 아이템의 인덱스</param>
/// <param name="moveToType">뷰포트 내 정렬 방식</param>
/// <returns>계산된 스크롤 위치(Vector2)</returns>
public Vector2 GetMovePosition(int itemIndex, MoveToType moveToType)
{
float move = GetMoveOffset(itemIndex, moveToType);
@@ -226,11 +377,20 @@ namespace Gpm.Ui
return layout.GetAxisVector(content.anchoredPosition, move);
}
/// <summary>
/// 현재 스크롤 content의 anchoredPosition을 가져옵니다.
/// </summary>
/// <returns>현재 스크롤 위치(Vector2)</returns>
public Vector2 GetScrollPosition()
{
return content.anchoredPosition;
}
/// <summary>
/// 스크롤 content의 anchoredPosition을 지정된 위치로 설정합니다.
/// 이 메서드는 스크롤 위치를 즉시 변경합니다.
/// </summary>
/// <param name="position">설정할 위치(Vector2)</param>
public void SetScrollPosition(Vector2 position)
{
content.anchoredPosition = position;
@@ -239,10 +399,14 @@ namespace Gpm.Ui
if (cachedData.contentPosition != contentPosition)
{
cachedData.contentPosition = contentPosition;
isUpdateArea = true;
isUpdateArea = true; // 위치가 변경되었으므로 업데이트 필요 플래그 설정
}
}
/// <summary>
/// 스크롤의 주 축(Vertical이면 Y, Horizontal이면 X) 위치를 설정합니다.
/// </summary>
/// <param name="movePosition">설정할 주 축의 위치 값</param>
public void SetScrollPosition(float movePosition)
{
Vector2 prevPosition = GetScrollPosition();
@@ -250,6 +414,10 @@ namespace Gpm.Ui
SetScrollPosition(layout.GetAxisVector(prevPosition, movePosition));
}
/// <summary>
/// 스크롤 content의 위치와 크기를 초기화합니다.
/// 데이터는 그대로 유지됩니다.
/// </summary>
public void ClearScrollContent()
{
content.anchoredPosition = Vector2.zero;
@@ -260,6 +428,25 @@ namespace Gpm.Ui
CheckScrollData();
}
/// <summary>
/// 내부 ScrollRect의 onValueChanged 이벤트에 리스너를 추가합니다.
/// 사용자가 스크롤을 움직일 때마다 콜백을 받고 싶을 때 사용합니다.
/// </summary>
/// <param name="listener">추가할 리스너(콜백 함수)</param>
/// <example>
/// <code>
/// void Start()
/// {
/// myScroll.AddScrollValueChangedLisnter(OnScrollMoved);
/// }
///
/// void OnScrollMoved(Vector2 position)
/// {
/// // position은 0.0 ~ 1.0 사이의 정규화된 값입니다.
/// Debug.Log($"스크롤 위치 변경: {position}");
/// }
/// </code>
/// </example>
public void AddScrollValueChangedLisnter(UnityAction<Vector2> listener)
{
if (isInitialize == false)
@@ -270,16 +457,19 @@ namespace Gpm.Ui
scrollRect.onValueChanged.AddListener(listener);
}
// 스크롤이 시작 지점에 있는지 내부적으로 계산하는 함수
private bool IsMoveToFirstData(float position, float viewportSize, float contentSize)
{
bool isShow = false;
// 콘텐츠 크기가 뷰포트보다 작으면 항상 시작/끝에 있는 것으로 간주
if (viewportSize > contentSize)
{
isShow = true;
}
else
{
// 부동 소수점 오차를 감안하여 비교
float interpolation = contentSize * sizeInterpolationValue;
if (-position > -interpolation)
{
@@ -290,6 +480,7 @@ namespace Gpm.Ui
return isShow;
}
// 스크롤이 끝 지점에 있는지 내부적으로 계산하는 함수
private bool IsMoveToLastData(float position, float viewportSize, float contentSize)
{
bool isShow = false;
@@ -302,6 +493,7 @@ namespace Gpm.Ui
{
float interpolation = contentSize * sizeInterpolationValue;
// 끝 지점 계산 (뷰포트 크기 + 현재 위치 - 콘텐츠 크기)
if (-(viewportSize + position - contentSize) <= interpolation)
{
isShow = true;
@@ -311,6 +503,12 @@ namespace Gpm.Ui
return isShow;
}
/// <summary>
/// 특정 아이템으로 이동하기 위한 스크롤 content의 오프셋(offset)을 계산합니다.
/// </summary>
/// <param name="itemIndex">대상 아이템 인덱스</param>
/// <param name="moveToType">뷰포트 내 정렬 방식</param>
/// <returns>계산된 오프셋 값</returns>
protected float GetMoveOffset(int itemIndex, MoveToType moveToType)
{
float viewportSize = GetViewportSize();
@@ -318,33 +516,38 @@ namespace Gpm.Ui
float move = 0.0f;
float itemSize = GetItemSize(itemIndex);
float distance = GetItemDistance(itemIndex);
float distance = GetItemDistance(itemIndex); // 아이템의 시작 위치
move = distance;
// 정렬 방식에 따라 오프셋 조정
switch (moveToType)
{
case MoveToType.MOVE_TO_CENTER:
{
// (뷰포트 중앙) - (아이템 중앙)
move -= viewportSize * 0.5f - itemSize * 0.5f;
break;
}
case MoveToType.MOVE_TO_BOTTOM:
{
// (뷰포트 하단) - (아이템 하단)
move -= viewportSize - itemSize;
break;
}
}
// 계산된 위치가 스크롤 가능 범위를 벗어나지 않도록 제한
move = Mathf.Clamp(move, 0.0f, contentSize - viewportSize);
move = Math.Max(0.0f, move);
return ItemPostionFromOffset(move);
}
// ScrollRect의 onValueChanged 이벤트에 연결되어 호출되는 메서드
private void OnValueChanged(Vector2 value)
{
// 스크롤이 시작/끝 지점에 도달했는지 확인하고 이벤트를 발생시킵니다.
bool prevIsStartLine = isStartLine;
isStartLine = IsMoveToFirstData();
if (prevIsStartLine != isStartLine)
@@ -363,6 +566,7 @@ namespace Gpm.Ui
changeValue = true;
}
// 스크롤이 움직였으므로 화면에 보이는 아이템을 갱신합니다.
UpdateShowItem();
}
}

View File

@@ -1,24 +1,83 @@
using System;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
namespace Gpm.Ui
{
using System;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
/// <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()
@@ -26,6 +85,10 @@ namespace Gpm.Ui
Initialize();
}
/// <summary>
/// InfiniteScroll을 초기화합니다.
/// ScrollRect 컴포넌트를 가져오고, 이벤트를 설정하며, 초기 아이템을 생성합니다.
/// </summary>
protected void Initialize()
{
if (isInitialize == false)
@@ -55,6 +118,17 @@ namespace Gpm.Ui
}
}
/// <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)
@@ -67,6 +141,18 @@ namespace Gpm.Ui
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)
@@ -84,6 +170,18 @@ namespace Gpm.Ui
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)
@@ -98,6 +196,20 @@ namespace Gpm.Ui
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)
@@ -118,6 +230,19 @@ namespace Gpm.Ui
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)
@@ -130,6 +255,16 @@ namespace Gpm.Ui
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)
@@ -182,6 +317,15 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 모든 데이터와 아이템 상태를 초기화하지만, 생성된 아이템 객체는 유지합니다.
/// </summary>
/// <param name="immediately">true이면 즉시 레이아웃을 업데이트합니다.</param>
/// <example>
/// <code>
/// infiniteScroll.ClearData(true);
/// </code>
/// </example>
public void ClearData(bool immediately = false)
{
if (isInitialize == false)
@@ -221,6 +365,14 @@ namespace Gpm.Ui
UpdateAllData(immediately);
}
/// <summary>
/// 모든 데이터와 아이템 객체를 완전히 제거하고 스크롤을 초기 상태로 되돌립니다.
/// </summary>
/// <example>
/// <code>
/// infiniteScroll.Clear();
/// </code>
/// </example>
public void Clear()
{
if (isInitialize == false)
@@ -257,6 +409,19 @@ namespace Gpm.Ui
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)
@@ -273,6 +438,15 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 모든 데이터를 업데이트하고 스크롤 뷰를 새로 고칩니다.
/// </summary>
/// <param name="immediately">true이면 즉시 레이아웃을 업데이트합니다.</param>
/// <example>
/// <code>
/// infiniteScroll.UpdateAllData(true);
/// </code>
/// </example>
public void UpdateAllData(bool immediately = true)
{
if (isInitialize == false)
@@ -291,17 +465,38 @@ namespace Gpm.Ui
}
}
/// <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();
@@ -309,11 +504,18 @@ namespace Gpm.Ui
return layout.GetMainSize(content);
}
/// <summary>
/// 콘텐츠의 현재 위치를 가져옵니다.
/// </summary>
/// <returns>콘텐츠의 주 축 위치입니다.</returns>
public float GetContentPosition()
{
return layout.GetAxisPosition(content);
}
/// <summary>
/// 스크롤 뷰의 크기를 다시 계산하고 업데이트합니다.
/// </summary>
public void ResizeScrollView()
{
if (isInitialize == false)
@@ -323,7 +525,12 @@ namespace Gpm.Ui
UpdateContentSize();
}
/// <summary>
/// 지정된 아이템 인덱스의 위치를 가져옵니다.
/// </summary>
/// <param name="itemIndex">위치를 가져올 아이템의 인덱스입니다.</param>
/// <returns>아이템의 위치입니다.</returns>
public float GetItemPosition(int itemIndex)
{
float distance = GetItemDistance(itemIndex);
@@ -331,6 +538,10 @@ namespace Gpm.Ui
return -layout.GetAxisPostionFromOffset(distance);
}
/// <summary>
/// 스크롤 뷰를 수동으로 새로 고칩니다.
/// 아이템 목록이나 레이아웃에 변경이 있을 때 호출합니다.
/// </summary>
public void RefreshScroll()
{
if (isInitialize == false)
@@ -350,17 +561,26 @@ namespace Gpm.Ui
}
}
/// <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)
@@ -370,6 +590,9 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 아이템을 업데이트해야 하는지 확인합니다.
/// </summary>
protected bool NeedUpdateItem()
{
CheckScrollData();
@@ -384,6 +607,9 @@ namespace Gpm.Ui
return false;
}
/// <summary>
/// 지정된 위치가 현재 콘텐츠 위치보다 이전에 있는지 확인합니다. (화면 위/왼쪽)
/// </summary>
protected bool IsShowBeforePosition(float position, float contentPosition)
{
float viewPosition = position - contentPosition;
@@ -395,6 +621,9 @@ namespace Gpm.Ui
return false;
}
/// <summary>
/// 지정된 위치가 현재 뷰포트 크기를 벗어나는지 확인합니다. (화면 아래/오른쪽)
/// </summary>
protected bool IsShowAfterPosition(float position, float contentPosition, float viewportSize)
{
float viewPosition = position - contentPosition;
@@ -414,12 +643,15 @@ namespace Gpm.Ui
}
}
// Inspector에서 값이 변경될 때 호출됩니다.
private void OnValidate()
{
layout.SetDefaults();
}
/// <summary>
/// 스크롤 값 변경 시 발생하는 이벤트를 위한 클래스입니다.
/// </summary>
[Serializable]
public class ChangeValueEvent : UnityEvent<int, int, bool, bool>
{
@@ -428,6 +660,9 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 아이템 활성 상태 변경 시 발생하는 이벤트를 위한 클래스입니다.
/// </summary>
[Serializable]
public class ItemActiveEvent : UnityEvent<int, bool>
{
@@ -436,6 +671,9 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 스크롤 상태 변경(시작/끝 도달) 시 발생하는 이벤트를 위한 클래스입니다.
/// </summary>
[Serializable]
public class StateChangeEvent : UnityEvent<bool>
{

View File

@@ -4,13 +4,33 @@ namespace Gpm.Ui
using UnityEngine;
using DataContext = InfiniteScroll.DataContext;
public class InfiniteScrollData
{
}
/// <summary>
/// InfiniteScroll에 표시될 데이터의 기반 클래스입니다.
/// 이 클래스를 상속하여 스크롤 아이템에 필요한 데이터를 정의할 수 있습니다.
/// </summary>
/// <example>
/// <code>
/// // 사용자 정의 데이터 클래스 예시
/// public class MyCustomData : InfiniteScrollData
/// {
/// public string title;
/// public string description;
/// public Sprite icon;
/// }
/// </code>
/// </example>
public class InfiniteScrollData {}
/// <summary>
/// InfiniteScroll의 각 아이템을 나타내는 기본 클래스입니다.
/// 이 클래스를 상속하여 아이템의 UI와 동작을 구현합니다.
/// </summary>
public class InfiniteScrollItem : MonoBehaviour
{
/// <summary>
/// 아이템의 크기가 변경될 때 자동으로 스크롤에 적용할지 여부를 결정합니다.
/// 주로 UI 요소(예: Text)의 내용에 따라 크기가 동적으로 변할 때 유용합니다.
/// </summary>
public bool autoApplySize = false;
protected RectTransform cachedRectTransform = null;
@@ -18,6 +38,10 @@ namespace Gpm.Ui
protected bool activeItem;
protected InfiniteScroll scroll = null;
/// <summary>
/// 이 아이템의 RectTransform 컴포넌트를 가져옵니다.
/// 성능을 위해 처음 접근할 때 캐시됩니다.
/// </summary>
public RectTransform rectTransform
{
get
@@ -31,6 +55,10 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 이 아이템에 바인딩된 데이터를 가져옵니다.
/// 데이터는 InfiniteScrollData를 상속한 사용자 정의 클래스의 인스턴스입니다.
/// </summary>
protected InfiniteScrollData scrollData
{
get
@@ -52,14 +80,23 @@ namespace Gpm.Ui
protected int itemObjectIndex = -1;
internal bool needUpdateItemSize = true;
/// <summary>
/// InfiniteScroll에 의해 아이템이 생성될 때 호출되는 초기화 메서드입니다.
/// </summary>
/// <param name="scroll">부모 InfiniteScroll 인스턴스</param>
/// <param name="itemObjectIndex">아이템 객체 풀 내에서의 인덱스</param>
public void Initalize(InfiniteScroll scroll, int itemObjectIndex)
{
this.scroll = scroll;
this.itemObjectIndex = itemObjectIndex;
this.needUpdateItemSize = true;
}
/// <summary>
/// 현재 화면에 보이는 아이템들 중 이 아이템의 인덱스를 반환합니다.
/// </summary>
/// <returns>화면 내 아이템 인덱스. 데이터가 없으면 -1을 반환합니다.</returns>
public int GetItemIndex()
{
if (dataContext != null)
@@ -72,6 +109,10 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 전체 데이터 목록에서 이 아이템이 사용하는 데이터의 인덱스를 반환합니다.
/// </summary>
/// <returns>전체 데이터 인덱스. 데이터가 없으면 -1을 반환합니다.</returns>
public int GetDataIndex()
{
if (dataContext != null)
@@ -84,25 +125,86 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 아이템이 현재 활성화 상태인지 확인합니다.
/// </summary>
/// <returns>활성화 상태이면 true, 아니면 false를 반환합니다.</returns>
public bool IsActive()
{
return activeItem;
}
/// <summary>
/// 아이템이 선택되었을 때 호출될 콜백을 추가합니다.
/// </summary>
/// <param name="callback">실행할 콜백 함수</param>
/// <example>
/// <code>
/// void OnEnable()
/// {
/// var item = GetComponent&lt;InfiniteScrollItem&gt;();
/// item.AddSelectCallback(OnItemSelected);
/// }
///
/// void OnItemSelected(InfiniteScrollData data)
/// {
/// MyCustomData myData = data as MyCustomData;
/// if (myData != null)
/// {
/// Debug.Log("Selected: " + myData.title);
/// }
/// }
/// </code>
/// </example>
public void AddSelectCallback(Action<InfiniteScrollData> callback)
{
selectCallback += callback;
}
/// <summary>
/// 등록된 선택 콜백을 제거합니다.
/// </summary>
/// <param name="callback">제거할 콜백 함수</param>
public void RemoveSelectCallback(Action<InfiniteScrollData> callback)
{
selectCallback -= callback;
}
/// <summary>
/// 새로운 데이터로 아이템의 내용을 업데이트합니다.
/// 이 메서드를 상속하여 아이템의 UI(텍스트, 이미지 등)를 데이터에 맞게 설정해야 합니다.
/// </summary>
/// <param name="scrollData">표시할 데이터</param>
/// <example>
/// <code>
/// // 사용자 정의 아이템 클래스에서 UpdateData를 재정의하는 예시
/// public class MyScrollItem : InfiniteScrollItem
/// {
/// public Text titleText;
/// public Image iconImage;
///
/// public override void UpdateData(InfiniteScrollData scrollData)
/// {
/// base.UpdateData(scrollData);
///
/// MyCustomData myData = scrollData as MyCustomData;
/// if (myData != null)
/// {
/// titleText.text = myData.title;
/// iconImage.sprite = myData.icon;
/// }
/// }
/// }
/// </code>
/// </example>
public virtual void UpdateData(InfiniteScrollData scrollData)
{
}
/// <summary>
/// 스크롤 방향에 따라 아이템의 RectTransform 앵커와 피벗을 설정합니다. (내부용)
/// </summary>
internal void SetAxis(Vector2 anchorMin, Vector2 anchorMax, Vector2 pivot)
{
bool autoApplySize = this.autoApplySize;
@@ -118,6 +220,10 @@ namespace Gpm.Ui
this.autoApplySize = autoApplySize;
}
/// <summary>
/// 아이템이 선택되었을 때 호출됩니다.
/// 주로 버튼의 OnClick 이벤트에 연결하여 사용합니다.
/// </summary>
protected void OnSelect()
{
if (selectCallback != null)
@@ -126,6 +232,12 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 아이템의 활성 상태를 설정합니다.
/// GameObject를 활성화/비활성화하고, 필요한 경우 부모 스크롤에 상태 변경을 알립니다.
/// </summary>
/// <param name="active">활성화 여부</param>
/// <param name="notifyEvent">부모 InfiniteScroll에 상태 변경 이벤트를 보낼지 여부</param>
public virtual void SetActive(bool active, bool notifyEvent = true)
{
activeItem = active;
@@ -141,6 +253,12 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 동적 아이템 크기 모드에서 아이템의 크기를 설정합니다.
/// InfiniteScroll의 dynamicItemSize가 true일 때만 작동합니다.
/// </summary>
/// <param name="itemSize">새로운 아이템 크기 (주 축 기준)</param>
/// <param name="notity">크기 변경을 즉시 스크롤에 알릴지 여부</param>
public void SetSize(float itemSize, bool notity = true)
{
if (scrollData == null)
@@ -168,6 +286,12 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 동적 아이템 크기 모드에서 아이템의 크기를 설정합니다.
/// InfiniteScroll의 dynamicItemSize가 true일 때만 작동합니다.
/// </summary>
/// <param name="sizeDelta">새로운 아이템의 sizeDelta</param>
/// <param name="notity">크기 변경을 즉시 스크롤에 알릴지 여부</param>
public void SetSize(Vector2 sizeDelta, bool notity = true)
{
if (scrollData == null)
@@ -185,6 +309,9 @@ namespace Gpm.Ui
SetSize(itemSize, notity);
}
/// <summary>
/// 아이템의 데이터를 초기화하고 비활성화합니다. (내부용)
/// </summary>
internal void ClearData(bool notifyEvent = true)
{
SetActive(false, activeItem && notifyEvent);
@@ -196,6 +323,9 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 아이템의 모든 데이터와 콜백을 초기화합니다. (내부용)
/// </summary>
internal void Clear()
{
ClearData(false);
@@ -203,6 +333,10 @@ namespace Gpm.Ui
selectCallback = null;
updateSizeCallback = null;
}
/// <summary>
/// 새로운 데이터 컨텍스트로 아이템을 업데이트합니다. (내부용)
/// </summary>
internal void UpdateItem(DataContext context)
{
this.dataContext = context;
@@ -215,17 +349,29 @@ namespace Gpm.Ui
this.needUpdateItemSize = true;
}
/// <summary>
/// 아이템의 크기가 업데이트될 때 호출될 콜백을 추가합니다.
/// `dynamicItemSize`가 활성화된 경우에 유용합니다.
/// </summary>
/// <param name="callback">실행할 콜백 함수. `InfiniteScrollData`와 `RectTransform`을 인자로 받습니다.</param>
public void AddUpdateSizeCallback(Action<InfiniteScrollData, RectTransform> callback)
{
updateSizeCallback += callback;
}
/// <summary>
/// 등록된 크기 업데이트 콜백을 제거합니다.
/// </summary>
/// <param name="callback">제거할 콜백 함수.</param>
public void RemoveUpdateSizeCallback(Action<InfiniteScrollData, RectTransform> callback)
{
updateSizeCallback -= callback;
}
/// <summary>
/// 아이템의 크기가 변경되었을 때 호출됩니다.
/// 부모 `InfiniteScroll`에 크기 변경을 알려 레이아웃을 다시 계산하도록 하고, 등록된 콜백을 실행합니다.
/// </summary>
protected void OnUpdateItemSize()
{
if (scroll != null &&
@@ -245,6 +391,10 @@ namespace Gpm.Ui
}
}
/// <summary>
/// `autoApplySize` 기능이 현재 실행될 수 있는 상태인지 확인합니다.
/// 아이템이 활성화되어 있고, 스크롤이 처리 중이 아닐 때 true를 반환합니다.
/// </summary>
protected bool CanAutoSizeCheck()
{
if (autoApplySize == false ||
@@ -271,6 +421,12 @@ namespace Gpm.Ui
return true;
}
/// <summary>
/// Unity의 UI 시스템에 의해 RectTransform의 크기가 변경될 때 호출됩니다.
/// autoApplySize가 true일 때, 이 메서드는 변경된 크기를 InfiniteScroll에 자동으로 전달하여
/// 스크롤 레이아웃을 다시 계산하도록 합니다.
/// </summary>
protected void OnRectTransformDimensionsChange()
{
if (CanAutoSizeCheck() == true)

View File

@@ -1,14 +1,37 @@
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
namespace Gpm.Ui
{
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
[ExecuteAlways]
[RequireComponent(typeof(RectTransform))]
/// <summary>
/// 이 컴포넌트가 부착된 UI 요소의 크기가 변경될 때, 부모 UI 요소의 레이아웃을 강제로 다시 계산하도록 요청하는 역할을 합니다.
/// 주로 중첩된 레이아웃 그룹(Layout Group)이나 ContentSizeFitter 사용 시, 자식의 크기 변경이 부모에게 제대로 전달되지 않아
/// UI가 깨져 보일 때 사용하면 유용합니다.
/// </summary>
/// <example>
/// <b>UI 계층 구조 예시:</b>
/// <code>
/// - VerticalPanel (VerticalLayoutGroup)
/// - HorizontalPanel (HorizontalLayoutGroup, ContentSizeFitter, LayoutUpdater)
/// - Item1
/// - Item2
/// </code>
/// 위 구조에서 'HorizontalPanel'에 아이템이 추가되어 너비가 변경될 때, 'ContentSizeFitter'가 'HorizontalPanel'의 크기를 조절합니다.
/// 이때 'HorizontalPanel'에 부착된 'LayoutUpdater'는 자신의 크기 변경을 감지하고,
/// 부모인 'VerticalPanel'에게 "자식 크기가 바뀌었으니 너의 레이아웃을 다시 정렬해!"라고 알려주는 역할을 합니다.
/// 결과적으로 'VerticalPanel'은 변경된 'HorizontalPanel'의 크기에 맞춰 레이아웃을 올바르게 갱신하게 됩니다.
/// </example>
[ExecuteAlways] // 에디터 모드에서도 스크립트가 실행되어, UI 변경을 실시간으로 확인할 수 있게 합니다.
[RequireComponent(typeof(RectTransform))] // 이 컴포넌트는 RectTransform이 필수적으로 필요함을 명시합니다.
public class LayoutUpdater : UIBehaviour
{
// 성능 최적화를 위해 부모 RectTransform 컴포넌트를 캐싱하는 변수입니다.
private RectTransform m_Parent;
/// <summary>
/// 이 컴포넌트가 부착된 GameObject의 부모 RectTransform에 대한 참조입니다.
/// </summary>
public RectTransform rectParent
{
get
@@ -25,6 +48,10 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 이 컴포넌트가 부착된 RectTransform의 크기나 앵커 등이 변경될 때 호출됩니다.
/// 부모의 레이아웃을 즉시 다시 계산하도록 강제합니다.
/// </summary>
protected override void OnRectTransformDimensionsChange()
{
base.OnRectTransformDimensionsChange();
@@ -32,6 +59,10 @@ namespace Gpm.Ui
SetDirty(true);
}
/// <summary>
/// 부모 레이아웃을 다시 계산해야 함을 시스템에 알립니다.
/// </summary>
/// <param name="force">true일 경우, 즉시 레이아웃을 다시 계산합니다. false일 경우, 다음 프레임에 다시 계산하도록 예약합니다.</param>
protected void SetDirty(bool force = false)
{
RectTransform parent = rectParent;
@@ -42,10 +73,12 @@ namespace Gpm.Ui
if (force == true)
{
// 즉시 레이아웃을 강제로 다시 계산합니다.
LayoutRebuilder.ForceRebuildLayoutImmediate(parent);
}
else
{
// 다음 레이아웃 계산 주기에 맞춰 다시 계산하도록 표시만 해둡니다.
LayoutRebuilder.MarkLayoutForRebuild(parent);
}
}
@@ -56,6 +89,10 @@ namespace Gpm.Ui
}
#if UNITY_EDITOR
/// <summary>
/// (Unity 에디터 전용) 스크립트가 로드되거나 인스펙터에서 값이 변경될 때 호출됩니다.
/// 에디터에서 실시간으로 레이아웃 변경을 확인할 수 있도록 업데이트를 요청합니다.
/// </summary>
protected override void OnValidate()
{
SetDirty(false);

View File

@@ -1,49 +1,100 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace Gpm.Ui
{
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 스크롤 뷰의 스크롤 방향과 콘텐츠 정렬 방식을 정의합니다.
/// </summary>
public enum ScrollAxis
{
DEFAULT = 0,
VERTICAL_TOP,
VERTICAL_CENTER,
VERTICAL_BOTTOM,
HORIZONTAL_LEFT,
HORIZONTAL_CENTER,
HORIZONTAL_RIGHT,
DEFAULT = 0, // ScrollRect의 설정을 따름
VERTICAL_TOP, // 수직, 위쪽 정렬
VERTICAL_CENTER, // 수직, 중앙 정렬
VERTICAL_BOTTOM, // 수직, 아래쪽 정렬
HORIZONTAL_LEFT, // 수평, 왼쪽 정렬
HORIZONTAL_CENTER, // 수평, 중앙 정렬
HORIZONTAL_RIGHT, // 수평, 오른쪽 정렬
}
/// <summary>
/// 이 클래스는 InfiniteScroll과 같은 스크롤 컴포넌트의 레이아웃 규칙을 정의합니다.
/// 스크롤 방향, 정렬, 간격, 그리드 설정 등 UI 항목을 어떻게 배치할지에 대한 모든 정보를 담고 있습니다.
/// MonoBehaviour가 아니며, 다른 컴포넌트의 필드로 선언되어 인스펙터에서 값을 설정하는 방식으로 사용됩니다.
/// </summary>
/// <example>
/// <b>사용 예시 (InfiniteScroll.cs 내에서):</b>
/// <code>
/// public class InfiniteScroll : MonoBehaviour
/// {
/// // ScrollLayout을 필드로 선언하여 인스펙터에서 레이아웃을 상세하게 설정할 수 있습니다.
/// public ScrollLayout layout;
///
/// // ... 스크롤 로직 ...
/// }
/// </code>
/// 위와 같이 `InfiniteScroll` 컴포넌트가 `ScrollLayout`을 멤버 변수로 가지고,
/// 사용자는 Unity 인스펙터에서 `layout` 변수의 세부 항목(축, 간격, 그리드 등)을 설정하여 스크롤 뷰의 동작을 커스터마이징할 수 있습니다.
/// </example>
[Serializable]
public class ScrollLayout
{
/// <summary>
/// 그리드 레이아웃에서 각 열 또는 행의 크기 정보를 담는 클래스입니다.
/// </summary>
[Serializable]
public class LayoutValue
{
public enum ValueType
{
DEFAULT,
RATE,
DEFAULT, // 기본값
RATE, // 비율
}
[HideInInspector]
public ValueType valueType;
public float value;
public float value; // 크기 값 (비율)
}
/// <summary>
/// 스크롤의 주 축(수직 또는 수평)과 정렬 기준을 설정합니다.
/// </summary>
public ScrollAxis axis;
/// <summary>
/// 콘텐츠 전체의 상하좌우 여백입니다.
/// Vector2(가로 여백, 세로 여백)
/// </summary>
public Vector2 padding;
/// <summary>
/// UI 항목들 사이의 간격입니다.
/// Vector2(가로 간격, 세로 간격)
/// </summary>
public Vector2 space;
/// <summary>
/// true일 경우, 수직 스크롤에서 위에서 아래로 항목을 채웁니다.
/// </summary>
public bool topToBotton = true;
/// <summary>
/// true일 경우, 수평 스크롤에서 왼쪽에서 오른쪽으로 항목을 채웁니다.
/// </summary>
public bool leftToRight = true;
/// <summary>
/// 그리드 레이아웃을 설정하는 데 사용됩니다.
/// 리스트에 항목이 2개 이상이면 그리드로 동작하며, 각 항목의 `value`는 그리드 행/열의 상대적 크기 비율을 나타냅니다.
/// 예: 수직 스크롤에서 values 리스트에 3개의 항목을 넣으면 3열 그리드가 됩니다.
/// </summary>
public List<LayoutValue> values = new List<LayoutValue>();
/// <summary>
/// 주어진 축이 수직 스크롤인지 확인합니다.
/// </summary>
static public bool IsVertical(ScrollAxis axis)
{
return axis == ScrollAxis.VERTICAL_TOP ||
@@ -51,6 +102,10 @@ namespace Gpm.Ui
axis == ScrollAxis.VERTICAL_BOTTOM;
}
/// <summary>
/// 설정된 `axis`에 맞게 `ScrollRect`의 스크롤 방향(vertical/horizontal)을 강제로 설정합니다.
/// `axis`가 `DEFAULT`이면 `ScrollRect`의 기존 설정을 따릅니다.
/// </summary>
public void CheckAxis(ScrollRect scrollRect)
{
if (axis == ScrollAxis.DEFAULT)
@@ -75,13 +130,18 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 현재 레이아웃이 수직 스크롤인지 확인합니다.
/// </summary>
public bool IsVertical()
{
return IsVertical(axis);
}
/// <summary>
/// 설정된 `axis`에 따라 콘텐츠 `RectTransform`의 피벗(pivot)을 계산하여 반환합니다.
/// 이는 콘텐츠 정렬의 기준점이 됩니다.
/// </summary>
public Vector2 GetAxisPivot()
{
Vector2 pivot = Vector2.zero;
@@ -113,6 +173,10 @@ namespace Gpm.Ui
return pivot;
}
/// <summary>
/// 그리드 레이아웃 값(`values`)을 기본 상태(비율 1)로 초기화합니다.
/// </summary>
public void SetDefaults()
{
foreach (LayoutValue layoutValue in values)
@@ -125,6 +189,9 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 콘텐츠 RectTransform의 현재 스크롤 위치를 주 축 기준으로 가져옵니다.
/// </summary>
public float GetAxisPosition(RectTransform content)
{
if (IsVertical() == true)
@@ -137,6 +204,9 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 주어진 오프셋 값을 스크롤 방향에 맞는 위치 값으로 변환합니다.
/// </summary>
public float GetAxisPostionFromOffset(float offset)
{
if (IsVertical() == true)
@@ -149,11 +219,21 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 스크롤 아이템의 주 축 방향 크기를 설정합니다.
/// </summary>
/// <param name="rectTransform">크기를 설정할 아이템의 RectTransform</param>
/// <param name="itemIndex">아이템의 인덱스 (현재 사용되지 않음)</param>
/// <param name="size">설정할 크기 값</param>
public void FitItemSize(RectTransform rectTransform, int itemIndex, float size)
{
rectTransform.sizeDelta = GetAxisVector(size);
}
/// <summary>
/// 그리드 레이아웃에서 각 항목이 교차 축(cross axis) 방향으로 올바른 위치에 배치되도록 `RectTransform`의 앵커(anchor)를 조절합니다.
/// 예를 들어, 수직 스크롤의 3열 그리드라면 각 항목이 1, 2, 3번째 열 중 어디에 위치할지를 계산합니다.
/// </summary>
public void FitItemInlinePosition(RectTransform rectTransform, int itemIndex, float crossSize)
{
float min = 0;
@@ -220,11 +300,17 @@ namespace Gpm.Ui
rectTransform.anchorMax = anchorMax;
}
/// <summary>
/// 그리드 레이아웃의 행 또는 열의 개수를 반환합니다.
/// </summary>
public int GridCount()
{
return values.Count;
}
/// <summary>
/// 현재 레이아웃이 그리드인지 확인합니다. (행/열 개수가 2개 이상)
/// </summary>
public bool IsGrid()
{
if (GridCount() > 1)
@@ -235,11 +321,19 @@ namespace Gpm.Ui
return false;
}
/// <summary>
/// 스크롤 아이템의 피벗(pivot)을 계산하여 반환합니다.
/// 아이템 정렬 방향(topToBottom, leftToRight)에 따라 결정됩니다.
/// </summary>
public Vector2 GetItemPivot()
{
return new Vector2(leftToRight ? 0.0f : 1.0f, topToBotton ? 1.0f : 0.0f);
}
/// <summary>
/// 스크롤 아이템의 앵커(anchor)를 계산하여 반환합니다.
/// 주 축 방향으로 앵커를 피벗 위치에 고정시켜 아이템이 해당 라인을 따라 배치되도록 합니다.
/// </summary>
public Rect GetItemAnchor()
{
Vector2 pivot = GetItemPivot();
@@ -259,11 +353,17 @@ namespace Gpm.Ui
return anchor;
}
/// <summary>
/// 주어진 값을 주 축 방향의 값으로 하는 Vector2를 생성합니다. 교차 축의 값은 0이 됩니다.
/// </summary>
public Vector2 GetAxisVector(float value)
{
return GetAxisVector(Vector2.zero, value);
}
/// <summary>
/// 주어진 Vector2에서 주 축에 해당하는 값을 새로운 값으로 설정하여 반환합니다.
/// </summary>
public Vector2 GetAxisVector(Vector2 vector, float value)
{
if (IsVertical() == true)
@@ -278,40 +378,67 @@ namespace Gpm.Ui
return vector;
}
/// <summary>
/// 주어진 RectTransform에서 스크롤의 주 축(Main Axis)에 해당하는 크기를 반환합니다.
/// (예: 수직 스크롤이면 높이, 수평 스크롤이면 너비)
/// </summary>
public float GetMainSize(RectTransform transform)
{
return GetMainSize(transform.rect);
}
/// <summary>
/// 주어진 Rect에서 스크롤의 주 축에 해당하는 크기를 반환합니다.
/// </summary>
public float GetMainSize(Rect rect)
{
return IsVertical() == true ? rect.height : rect.width;
}
/// <summary>
/// 주어진 Rect에서 스크롤의 교차 축(Cross Axis)에 해당하는 크기를 반환합니다.
/// (예: 수직 스크롤이면 너비, 수평 스크롤이면 높이)
/// </summary>
public float GetCrossSize(Rect rect)
{
return IsVertical() == true ? rect.width : rect.height;
}
/// <summary>
/// 주어진 Vector2에서 스크롤의 주 축에 해당하는 크기를 반환합니다.
/// </summary>
public float GetMainSize(Vector2 delta)
{
return IsVertical() == true ? delta.y : delta.x;
}
/// <summary>
/// 주어진 Vector2에서 스크롤의 교차 축에 해당하는 크기를 반환합니다.
/// </summary>
public float GetCrossSize(Vector2 delta)
{
return IsVertical() == true ? delta.x : delta.y;
}
/// <summary>
/// 주 축에 해당하는 Vector2의 인덱스를 반환합니다. (x=0, y=1)
/// </summary>
public int GetMainIndex()
{
return IsVertical() ? 1 : 0;
}
/// <summary>
/// 교차 축에 해당하는 Vector2의 인덱스를 반환합니다. (x=0, y=1)
/// </summary>
public int GetCrossIndex()
{
return IsVertical() ? 0 : 1;
}
/// <summary>
/// 주 축 방향의 여백(Padding)입니다.
/// </summary>
public float MainPadding
{
get
@@ -325,6 +452,9 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 교차 축 방향의 여백(Padding)입니다.
/// </summary>
public float CrossPadding
{
get
@@ -338,6 +468,9 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 주 축 방향의 항목 간 간격(Space)입니다.
/// </summary>
public float MainSpace
{
get
@@ -351,6 +484,9 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 교차 축 방향의 항목 간 간격(Space)입니다.
/// </summary>
public float CrossSpace
{
get

View File

@@ -1,26 +1,78 @@
namespace Gpm.Ui
{
using UnityEngine;
using MoveToType = InfiniteScroll.MoveToType;
/// <summary>
/// 스크롤 위치를 제어하기 위한 인터페이스입니다.
/// InfiniteScroll과 같이 스크롤 기능이 있는 클래스에서 이 인터페이스를 구현합니다.
/// </summary>
public interface IMoveScroll
{
/// <summary>
/// 현재 스크롤 위치를 반환합니다.
/// </summary>
Vector2 GetScrollPosition();
/// <summary>
/// 스크롤 위치를 설정합니다.
/// </summary>
void SetScrollPosition(Vector2 position);
/// <summary>
/// 특정 아이템 인덱스로 이동할 위치를 계산하여 반환합니다.
/// </summary>
/// <param name="itemIndex">이동할 아이템의 인덱스</param>
/// <param name="moveToType">아이템을 정렬할 위치(상단, 중앙, 하단)</param>
Vector2 GetMovePosition(int itemIndex, MoveToType moveToType);
/// <summary>
/// 전체 스크롤 범위의 특정 비율로 이동할 위치를 계산하여 반환합니다.
/// </summary>
/// <param name="scrollRate">이동할 비율 (0.0 ~ 1.0)</param>
Vector2 GetMovePosition(float scrollRate);
}
/// <summary>
/// 특정 위치로 스크롤을 애니메이션과 함께 부드럽게 이동시키는 컴포넌트입니다.
/// InfiniteScroll과 같이 IMoveScroll 인터페이스를 구현한 컴포넌트가 있는 게임 오브젝트에 추가하여 사용합니다.
///
/// --- 샘플 코드 ---
/// <code>
/// // 이동을 원하는 스크롤이 있는 GameObject를 찾습니다.
/// GameObject scrollObject = GameObject.Find("InfiniteScroll");
///
/// // ScrollMoveTo 컴포넌트가 없으면 추가하고, 있으면 가져옵니다.
/// ScrollMoveTo moveTo = scrollObject.GetComponent<ScrollMoveTo>();
/// if (moveTo == null)
/// {
/// moveTo = scrollObject.AddComponent<ScrollMoveTo>();
/// }
///
/// // 예제 1: 10번 아이템의 중앙으로 0.5초 동안 이동
/// moveTo.Set(10, MoveToType.MOVE_TO_CENTER, 0.5f);
/// moveTo.Play();
///
/// // 예제 2: 스크롤 전체의 80% 위치로 0.3초 동안 이동
/// moveTo.Set(0.8f, 0.3f);
/// moveTo.Play();
/// </code>
/// </summary>
[DisallowMultipleComponent]
public class ScrollMoveTo : MonoBehaviour
{
/// <summary>
/// 스크롤 이동 목표를 지정하는 방식입니다.
/// </summary>
public enum ScrollType
{
/// <summary>
/// 아이템의 인덱스를 기준으로 이동합니다.
/// </summary>
INDEX,
/// <summary>
/// 전체 스크롤의 비율(0.0~1.0)을 기준으로 이동합니다.
/// </summary>
RATE
}
@@ -34,23 +86,59 @@ namespace Gpm.Ui
}
}
/// <summary>
/// 스크롤 이동 목표를 설정합니다. (인덱스 또는 비율)
/// </summary>
[Tooltip("스크롤 이동 목표를 설정합니다. (인덱스 또는 비율)")]
public ScrollType scrollType;
/// <summary>
/// 이동할 대상 아이템의 인덱스입니다. (scrollType이 INDEX일 때 사용)
/// </summary>
[Tooltip("이동할 대상 아이템의 인덱스입니다. (scrollType이 INDEX일 때 사용)")]
public int itemIndex;
/// <summary>
/// 아이템을 화면의 어느 위치에 맞출지 결정합니다. (scrollType이 INDEX일 때 사용)
/// </summary>
[Tooltip("아이템을 화면의 어느 위치에 맞출지 결정합니다. (scrollType이 INDEX일 때 사용)")]
public MoveToType moveToType;
/// <summary>
/// 이동할 스크롤의 비율입니다. (0.0 = 시작, 1.0 = 끝) (scrollType이 RATE일 때 사용)
/// </summary>
[Tooltip("이동할 스크롤의 비율입니다. (0.0 = 시작, 1.0 = 끝) (scrollType이 RATE일 때 사용)")]
[Range(0f, 1f)]
public float scrollRate = 0;
/// <summary>
/// 목표 위치까지 이동하는 데 걸리는 시간(초)입니다.
/// </summary>
[Tooltip("목표 위치까지 이동하는 데 걸리는 시간(초)입니다.")]
public float time = 0.3f;
/// <summary>
/// 이동 애니메이션의 속도 변화를 제어하는 커브입니다.
/// </summary>
[Tooltip("이동 애니메이션의 속도 변화를 제어하는 커브입니다.")]
public AnimationCurve curve = AnimationCurve.EaseInOut(0, 0, 1, 1);
/// <summary>
/// 이동이 완료된 후 이 컴포넌트를 자동으로 파괴할지 여부입니다.
/// </summary>
[Tooltip("이동이 완료된 후 이 컴포넌트를 자동으로 파괴할지 여부입니다.")]
public bool autoDestory = false;
private float rate = 0;
private Vector2 start;
/// <summary>
/// 특정 아이템 인덱스로 이동하도록 설정합니다.
/// </summary>
/// <param name="itemIndex">이동할 아이템 인덱스</param>
/// <param name="moveToType">아이템 정렬 위치</param>
/// <param name="time">이동 시간(초)</param>
public void Set(int itemIndex, MoveToType moveToType, float time)
{
this.scrollType = ScrollType.INDEX;
@@ -60,6 +148,11 @@ namespace Gpm.Ui
this.time = time;
}
/// <summary>
/// 특정 비율 위치로 이동하도록 설정합니다.
/// </summary>
/// <param name="scrollRate">이동할 비율 (0.0 ~ 1.0)</param>
/// <param name="time">이동 시간(초)</param>
public void Set(float scrollRate, float time)
{
this.scrollType = ScrollType.RATE;
@@ -68,6 +161,9 @@ namespace Gpm.Ui
this.time = time;
}
/// <summary>
/// 설정된 값에 따라 스크롤 이동 애니메이션을 시작합니다.
/// </summary>
public void Play()
{
rate = 0;