draggableList/Tab 개발 중
This commit is contained in:
8
Assets/Scripts/UVC/UI/List.meta
Normal file
8
Assets/Scripts/UVC/UI/List.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 01351942c80cc7042b2ec18f41173854
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
53
Assets/Scripts/UVC/UI/List/DraggableItem.cs
Normal file
53
Assets/Scripts/UVC/UI/List/DraggableItem.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace UVC.UI.List
|
||||
{
|
||||
/// <summary>
|
||||
/// 개별 드래그 가능한 아이템의 UI 컴포넌트
|
||||
/// 드래그 동작과 시각적 피드백을 담당
|
||||
/// </summary>
|
||||
public class DraggableItem : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
|
||||
{
|
||||
// 이벤트
|
||||
public Action<PointerEventData>? OnBeginDragEvent;
|
||||
public Action<PointerEventData>? OnDragEvent;
|
||||
public Action<PointerEventData>? OnEndDragEvent;
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 시작 시 호출
|
||||
/// </summary>
|
||||
public void OnBeginDrag(PointerEventData eventData)
|
||||
{
|
||||
OnBeginDragEvent?.Invoke(eventData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 중 호출
|
||||
/// </summary>
|
||||
public void OnDrag(PointerEventData eventData)
|
||||
{
|
||||
// 이벤트 발생
|
||||
OnDragEvent?.Invoke(eventData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 종료 시 호출
|
||||
/// </summary>
|
||||
public void OnEndDrag(PointerEventData eventData)
|
||||
{
|
||||
// 이벤트 발생
|
||||
OnEndDragEvent?.Invoke(eventData);
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
// 이벤트 구독 해제
|
||||
OnBeginDragEvent = null;
|
||||
OnDragEvent = null;
|
||||
OnEndDragEvent = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UI/List/DraggableItem.cs.meta
Normal file
2
Assets/Scripts/UVC/UI/List/DraggableItem.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d93b757e3738184492e84c051530130
|
||||
42
Assets/Scripts/UVC/UI/List/DraggableItemData.cs
Normal file
42
Assets/Scripts/UVC/UI/List/DraggableItemData.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UVC.UI.List
|
||||
{
|
||||
/// <summary>
|
||||
/// 드래그 가능한 목록 아이템의 데이터 모델
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class DraggableItemData
|
||||
{
|
||||
[SerializeField] protected string id;
|
||||
[SerializeField] protected int sortOrder;
|
||||
|
||||
public string Id => id;
|
||||
public int SortOrder { get => sortOrder; set => sortOrder = value; }
|
||||
|
||||
public DraggableItemData(string id, int sortOrder = 0)
|
||||
{
|
||||
this.id = id ?? throw new ArgumentNullException(nameof(id));
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 작업의 결과를 나타내는 이벤트 인자
|
||||
/// </summary>
|
||||
public class DraggableItemReorderEventArgs : EventArgs
|
||||
{
|
||||
public string ItemId { get; }
|
||||
public int OldIndex { get; }
|
||||
public int NewIndex { get; }
|
||||
|
||||
public DraggableItemReorderEventArgs(string itemId, int oldIndex, int newIndex)
|
||||
{
|
||||
ItemId = itemId;
|
||||
OldIndex = oldIndex;
|
||||
NewIndex = newIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UI/List/DraggableItemData.cs.meta
Normal file
2
Assets/Scripts/UVC/UI/List/DraggableItemData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a576b71abcd5ff41a1c3d0adf21a45c
|
||||
191
Assets/Scripts/UVC/UI/List/DraggableListItem.cs
Normal file
191
Assets/Scripts/UVC/UI/List/DraggableListItem.cs
Normal file
@@ -0,0 +1,191 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace UVC.UI.List
|
||||
{
|
||||
/// <summary>
|
||||
/// 개별 드래그 가능한 아이템의 UI 컴포넌트
|
||||
/// 드래그 동작과 시각적 피드백을 담당
|
||||
/// </summary>
|
||||
public class DraggableListItem : MonoBehaviour
|
||||
{
|
||||
[Header("UI 컴포넌트")]
|
||||
[SerializeField] protected CanvasGroup? canvasGroup;
|
||||
[SerializeField] protected RectTransform? rectTransform;
|
||||
[SerializeField] protected DraggableItem? dragAnchor;
|
||||
[SerializeField] protected TMP_InputField? inputField;
|
||||
|
||||
[Header("드래그 설정")]
|
||||
[SerializeField] protected float dragAlpha = 0.6f;
|
||||
[SerializeField] protected bool blockRaycastsWhileDragging = false;
|
||||
|
||||
// 프로퍼티
|
||||
public DraggableItemData? Data { get; private set; }
|
||||
public RectTransform? RectTransform => rectTransform;
|
||||
public bool IsDragging { get; private set; }
|
||||
|
||||
public event Action<DraggableListItem>? OnBeginDragEvent;
|
||||
public event Action<DraggableListItem, Vector2>? OnDragEvent;
|
||||
public event Action<DraggableListItem>? OnEndDragEvent;
|
||||
|
||||
private Vector2 originalPosition;
|
||||
private Transform? originalParent;
|
||||
private int originalSiblingIndex;
|
||||
|
||||
/// <summary>
|
||||
/// 컴포넌트 초기화
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
// null 체크 및 자동 할당
|
||||
if (rectTransform == null)
|
||||
rectTransform = GetComponent<RectTransform>();
|
||||
|
||||
if (canvasGroup == null)
|
||||
canvasGroup = GetComponent<CanvasGroup>();
|
||||
|
||||
// CanvasGroup이 없으면 추가
|
||||
if (canvasGroup == null)
|
||||
canvasGroup = gameObject.AddComponent<CanvasGroup>();
|
||||
|
||||
if (dragAnchor == null)
|
||||
{
|
||||
Debug.LogError("Drag Anchor is not assigned. Please assign it in the inspector.");
|
||||
return;
|
||||
}
|
||||
|
||||
dragAnchor.OnBeginDragEvent += OnBeginDrag;
|
||||
dragAnchor.OnDragEvent += OnDrag;
|
||||
dragAnchor.OnEndDragEvent += OnEndDrag;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 아이템 데이터로 UI 업데이트
|
||||
/// </summary>
|
||||
/// <param name="data">표시할 데이터</param>
|
||||
public void SetData(DraggableItemData? data)
|
||||
{
|
||||
if (data == null) return;
|
||||
|
||||
Data = data;
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UI 요소들을 데이터에 맞게 업데이트
|
||||
/// </summary>
|
||||
protected virtual void UpdateUI()
|
||||
{
|
||||
if (Data == null) return;
|
||||
if(inputField != null)
|
||||
{
|
||||
inputField.text = Data.Id;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 시작 시 호출
|
||||
/// </summary>
|
||||
public void OnBeginDrag(PointerEventData eventData)
|
||||
{
|
||||
if (rectTransform == null) return;
|
||||
|
||||
IsDragging = true;
|
||||
|
||||
// 원래 위치와 부모 저장
|
||||
originalPosition = rectTransform.anchoredPosition;
|
||||
originalParent = transform.parent;
|
||||
originalSiblingIndex = transform.GetSiblingIndex();
|
||||
|
||||
// 시각적 피드백 적용
|
||||
ApplyDragVisuals(true);
|
||||
|
||||
// 이벤트 발생
|
||||
OnBeginDragEvent?.Invoke(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 중 호출
|
||||
/// </summary>
|
||||
public void OnDrag(PointerEventData eventData)
|
||||
{
|
||||
if (rectTransform == null) return;
|
||||
|
||||
// 마우스 위치로 아이템 이동
|
||||
rectTransform.anchoredPosition += new Vector2(0, eventData.delta.y);//eventData.delta
|
||||
|
||||
// 이벤트 발생
|
||||
OnDragEvent?.Invoke(this, rectTransform.anchoredPosition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 종료 시 호출
|
||||
/// </summary>
|
||||
public void OnEndDrag(PointerEventData eventData)
|
||||
{
|
||||
IsDragging = false;
|
||||
|
||||
// 시각적 피드백 복원
|
||||
ApplyDragVisuals(false);
|
||||
|
||||
// 이벤트 발생
|
||||
OnEndDragEvent?.Invoke(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 시각적 효과 적용/해제
|
||||
/// </summary>
|
||||
/// <param name="isDragging">드래그 중인지 여부</param>
|
||||
private void ApplyDragVisuals(bool isDragging)
|
||||
{
|
||||
if (canvasGroup == null) return;
|
||||
|
||||
if (isDragging)
|
||||
{
|
||||
canvasGroup.alpha = dragAlpha;
|
||||
canvasGroup.blocksRaycasts = blockRaycastsWhileDragging;
|
||||
}
|
||||
else
|
||||
{
|
||||
canvasGroup.alpha = 1f;
|
||||
canvasGroup.blocksRaycasts = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 원래 위치로 되돌리기 (드래그 취소 시 사용)
|
||||
/// </summary>
|
||||
public void ResetToOriginalPosition()
|
||||
{
|
||||
if (rectTransform == null || originalParent == null) return;
|
||||
|
||||
transform.SetParent(originalParent);
|
||||
transform.SetSiblingIndex(originalSiblingIndex);
|
||||
rectTransform.anchoredPosition = originalPosition;
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
// 이벤트 구독 해제
|
||||
if (dragAnchor != null)
|
||||
{
|
||||
dragAnchor.OnBeginDragEvent -= OnBeginDrag;
|
||||
dragAnchor.OnDragEvent -= OnDrag;
|
||||
dragAnchor.OnEndDragEvent -= OnEndDrag;
|
||||
}
|
||||
|
||||
OnBeginDragEvent = null;
|
||||
OnDragEvent = null;
|
||||
OnEndDragEvent = null;
|
||||
|
||||
// 리소스 정리
|
||||
canvasGroup = null;
|
||||
rectTransform = null;
|
||||
dragAnchor = null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UI/List/DraggableListItem.cs.meta
Normal file
2
Assets/Scripts/UVC/UI/List/DraggableListItem.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7cdfe032ad5874e4cbc571f344516b93
|
||||
1075
Assets/Scripts/UVC/UI/List/DraggableScrollList.cs
Normal file
1075
Assets/Scripts/UVC/UI/List/DraggableScrollList.cs
Normal file
File diff suppressed because it is too large
Load Diff
2
Assets/Scripts/UVC/UI/List/DraggableScrollList.cs.meta
Normal file
2
Assets/Scripts/UVC/UI/List/DraggableScrollList.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71e6121c6103b0a4c9aeadc24c891b86
|
||||
@@ -2,101 +2,214 @@ using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
/*
|
||||
* UILoading 스크립트 사용법 예시
|
||||
*
|
||||
* 다른 스크립트에서 아래와 같이 간단하게 호출하여 로딩 화면을 표시하거나 숨길 수 있습니다.
|
||||
*
|
||||
* public class MyGameManager : MonoBehaviour
|
||||
* {
|
||||
* public void LoadNextScene()
|
||||
* {
|
||||
* // 로딩 화면 표시
|
||||
* UVC.UI.Loading.UILoading.Show();
|
||||
*
|
||||
* // 코루틴을 사용하여 비동기 씬 로딩 시작
|
||||
* StartCoroutine(LoadSceneAsync());
|
||||
* }
|
||||
*
|
||||
* private IEnumerator LoadSceneAsync()
|
||||
* {
|
||||
* // 씬을 비동기로 로드합니다.
|
||||
* AsyncOperation asyncLoad = UnityEngine.SceneManagement.SceneManager.LoadSceneAsync("NextSceneName");
|
||||
*
|
||||
* // 씬 로딩이 완료될 때까지 대기합니다.
|
||||
* while (!asyncLoad.isDone)
|
||||
* {
|
||||
* yield return null;
|
||||
* }
|
||||
*
|
||||
* // 씬 로딩이 완료되면 로딩 화면을 숨깁니다.
|
||||
* UVC.UI.Loading.UILoading.Hide();
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
namespace UVC.UI.Loading
|
||||
{
|
||||
/// <summary>
|
||||
/// 게임 전체에서 사용되는 로딩 UI를 제어하는 클래스입니다.
|
||||
/// 싱글톤(Singleton)과 유사한 방식으로 구현되어 어디서든 쉽게 접근하고 사용할 수 있습니다.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(CanvasGroup))]
|
||||
public class UILoading : UnityEngine.MonoBehaviour
|
||||
public class UILoading : MonoBehaviour
|
||||
{
|
||||
// 로딩 UI 프리팹이 Resources 폴더 내에 위치하는 경로입니다.
|
||||
// Resources.Load를 통해 동적으로 프리팹을 불러올 때 사용됩니다.
|
||||
public static string PrefabPath = "Prefabs/UI/Loading/UILoading";
|
||||
|
||||
// UILoading 클래스의 유일한 인스턴스를 저장하는 정적(static) 변수입니다.
|
||||
// 이 변수를 통해 다른 모든 스크립트에서 동일한 로딩 화면 인스턴스에 접근할 수 있습니다.
|
||||
private static UILoading instance;
|
||||
|
||||
/// <summary>
|
||||
/// 로딩 화면을 표시합니다.
|
||||
/// 만약 로딩 화면이 아직 생성되지 않았다면, 프리팹을 이용해 새로 생성합니다.
|
||||
/// </summary>
|
||||
public static void Show()
|
||||
{
|
||||
if (instance == null) {
|
||||
// instance가 null일 경우, 아직 로딩 화면이 만들어지지 않았다는 의미입니다.
|
||||
if (instance == null)
|
||||
{
|
||||
// Resources 폴더에서 프리팹을 불러옵니다.
|
||||
GameObject prefab = Resources.Load<GameObject>(PrefabPath);
|
||||
// 불러온 프리팹을 씬에 인스턴스(복제)하여 생성합니다.
|
||||
GameObject go = Instantiate(prefab);
|
||||
// 생성된 GameObject의 이름을 "UILoading"으로 설정하여 씬에서 쉽게 식별할 수 있도록 합니다.
|
||||
go.name = "UILoading";
|
||||
// 부모를 null로 설정하여 씬의 최상위 계층에 위치시킵니다.
|
||||
// 이렇게 하면 다른 씬으로 전환될 때 함께 파괴되지 않도록 관리하기 용이합니다. (DontDestroyOnLoad와 함께 사용 가능)
|
||||
go.transform.SetParent(null, false);
|
||||
// 생성된 GameObject에서 UILoading 컴포넌트를 찾아 instance 변수에 할당합니다.
|
||||
instance = go.GetComponent<UILoading>();
|
||||
}
|
||||
// 인스턴스의 ShowLoading 메서드를 호출하여 페이드인 애니메이션을 시작합니다.
|
||||
instance.ShowLoading();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 표시되고 있는 로딩 화면을 숨깁니다.
|
||||
/// </summary>
|
||||
public static void Hide()
|
||||
{
|
||||
// instance가 null이 아닐 경우에만 (즉, 로딩 화면이 존재할 때만) 실행합니다.
|
||||
if (instance != null)
|
||||
{
|
||||
// 인스턴스의 HideLoading 메서드를 호출하여 페이드아웃 애니메이션을 시작합니다.
|
||||
instance.HideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// --- 인스펙터(Inspector)에서 설정할 변수들 ---
|
||||
[Header("Component References")]
|
||||
[Tooltip("페이드 효과를 제어할 CanvasGroup 컴포넌트")]
|
||||
[SerializeField]
|
||||
private CanvasGroup canvasGroup;
|
||||
|
||||
[Tooltip("채우기(Fill) 및 회전 효과를 적용할 Image 컴포넌트")]
|
||||
[SerializeField]
|
||||
private Image loadinImage;
|
||||
|
||||
private float duration = 0.25f;
|
||||
private float target = 0;
|
||||
private float alpha = 1;
|
||||
private bool animatting = false;
|
||||
private Transform loadingImageTransform;
|
||||
|
||||
// --- 내부 동작을 위한 변수들 ---
|
||||
[Header("Animation Settings")]
|
||||
[Tooltip("페이드인/아웃 애니메이션의 지속 시간 (초)")]
|
||||
[SerializeField]
|
||||
private float duration = 0.25f;
|
||||
|
||||
[Tooltip("로딩 아이콘의 회전 속도")]
|
||||
[SerializeField]
|
||||
private float loadingSpeed = -1.5f;
|
||||
|
||||
[Tooltip("이미지 채우기(Fill) 효과의 속도")]
|
||||
[SerializeField]
|
||||
private float rotationSpeed = -1.0f;
|
||||
|
||||
private float target = 0; // 애니메이션의 목표 알파 값 (0: 투명, 1: 불투명)
|
||||
private bool animatting = false; // 현재 애니메이션이 진행 중인지 여부
|
||||
private Transform loadingImageTransform; // 회전 애니메이션을 적용할 이미지의 Transform
|
||||
|
||||
/// <summary>
|
||||
/// 스크립트 인스턴스가 처음 로드될 때 호출됩니다.
|
||||
/// 변수 초기화에 사용됩니다.
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
{
|
||||
// Image 컴포넌트의 Transform을 미리 찾아 변수에 저장해두어,
|
||||
// LateUpdate에서 매번 찾는 비용을 절약합니다.
|
||||
loadingImageTransform = loadinImage.transform;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 로딩 화면을 나타나게 하는 애니메이션을 시작합니다.
|
||||
/// </summary>
|
||||
public void ShowLoading()
|
||||
{
|
||||
// 이미 애니메이션이 진행 중이고, 목표가 '나타나기(target=1)'라면 중복 실행을 방지합니다.
|
||||
if (animatting && target == 1) return;
|
||||
|
||||
target = 1;
|
||||
animatting = true;
|
||||
StopCoroutine("Animate");
|
||||
StartCoroutine(Animate());
|
||||
target = 1; // 목표 알파 값을 1(불투명)로 설정
|
||||
animatting = true; // 애니메이션 시작 상태로 변경
|
||||
StopCoroutine("Animate"); // 이전에 실행 중이던 Animate 코루틴이 있다면 중지
|
||||
StartCoroutine(Animate()); // Animate 코루틴을 새로 시작
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 로딩 화면을 사라지게 하는 애니메이션을 시작합니다.
|
||||
/// </summary>
|
||||
public void HideLoading()
|
||||
{
|
||||
// 이미 애니메이션이 진행 중이고, 목표가 '사라지기(target=0)'라면 중복 실행을 방지합니다.
|
||||
if (animatting && target == 0) return;
|
||||
|
||||
target = 0;
|
||||
animatting = true;
|
||||
StopCoroutine("Animate");
|
||||
StartCoroutine(Animate());
|
||||
target = 0; // 목표 알파 값을 0(투명)으로 설정
|
||||
animatting = true; // 애니메이션 시작 상태로 변경
|
||||
StopCoroutine("Animate"); // 이전에 실행 중이던 Animate 코루틴이 있다면 중지
|
||||
StartCoroutine(Animate()); // Animate 코루틴을 새로 시작
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CanvasGroup의 알파 값을 부드럽게 변경하여 페이드인/아웃 효과를 주는 코루틴입니다.
|
||||
/// </summary>
|
||||
private IEnumerator Animate()
|
||||
{
|
||||
float start = canvasGroup.alpha;
|
||||
float start = canvasGroup.alpha; // 현재 알파 값을 시작 값으로 설정
|
||||
float time = 0;
|
||||
|
||||
// 경과 시간이 duration에 도달할 때까지 반복합니다.
|
||||
while (time < duration)
|
||||
{
|
||||
time += Time.deltaTime;
|
||||
// Time.unscaledDeltaTime: Time.timeScale 값에 영향을 받지 않는 시간 간격입니다.
|
||||
// 로딩 중 Time.timeScale이 0이 되어도 UI 애니메이션은 계속 실행되어야 하므로 사용합니다.
|
||||
time += Time.unscaledDeltaTime;
|
||||
// Mathf.Lerp(시작값, 목표값, 진행률): 두 값 사이를 선형 보간합니다.
|
||||
// 진행률(time / duration)에 따라 부드럽게 알파 값이 변합니다.
|
||||
canvasGroup.alpha = Mathf.Lerp(start, target, time / duration);
|
||||
// 다음 프레임까지 대기합니다.
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// 애니메이션이 끝난 후 목표 알파 값으로 정확히 맞춰줍니다.
|
||||
canvasGroup.alpha = target;
|
||||
animatting = false;
|
||||
if(target == 0)
|
||||
animatting = false; // 애니메이션 종료 상태로 변경
|
||||
|
||||
// 만약 목표가 '사라지기(target=0)'였다면, 애니메이션 종료 후 GameObject를 파괴합니다.
|
||||
if (target == 0)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
/// <summary>
|
||||
/// 모든 Update 함수가 호출된 후에 프레임마다 호출됩니다.
|
||||
/// 주로 카메라 이동이나 다른 애니메이션이 끝난 후 최종적으로 UI를 업데이트할 때 사용되어,
|
||||
/// 떨림(Jitter) 현상을 방지하는 데 도움이 됩니다.
|
||||
/// </summary>
|
||||
private void LateUpdate()
|
||||
{
|
||||
// 로딩 화면이 완전히 불투명할 때(애니메이션이 진행 중일 때)만 실행합니다.
|
||||
if (canvasGroup.alpha == 1)
|
||||
{
|
||||
loadingImageTransform.Rotate(Vector3.forward, loadingSpeed * Time.deltaTime * 360);
|
||||
loadinImage.fillAmount = Mathf.PingPong(Time.time * rotationSpeed, 1);
|
||||
// 1. 이미지 회전 애니메이션
|
||||
// Time.unscaledTime: Time.timeScale에 영향을 받지 않는 전체 게임 시간입니다.
|
||||
// 시간에 비례하는 절대적인 회전 각도를 계산하여 매 프레임 설정합니다.
|
||||
// 이렇게 하면 프레임 속도 변화에 관계없이 항상 일관되고 부드러운 회전을 보장할 수 있습니다.
|
||||
float zRotation = Time.unscaledTime * loadingSpeed * 360;
|
||||
loadingImageTransform.rotation = Quaternion.Euler(0, 0, zRotation);
|
||||
|
||||
// 2. 이미지 채우기(Fill) 애니메이션 (필요 시 주석 해제하여 사용)
|
||||
// Mathf.PingPong(시간, 길이): 0과 '길이' 사이를 왕복하는 값을 반환합니다.
|
||||
// 여기서는 fillAmount가 0과 1 사이를 계속 오가도록 하여 채워졌다 비워지는 효과를 만듭니다.
|
||||
loadinImage.fillAmount = Mathf.PingPong(Time.unscaledTime * rotationSpeed, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,36 @@ using UnityEngine.UI;
|
||||
|
||||
namespace UVC.UI.Loading
|
||||
{
|
||||
/// <summary>
|
||||
/// 로딩 UI의 표시와 숨김, 진행 상태를 관리하는 클래스입니다.
|
||||
/// CanvasGroup 컴포넌트를 사용하여 페이드 인/아웃 효과를 구현합니다.
|
||||
/// 이 클래스가 게임 오브젝트에 추가되면 CanvasGroup 컴포넌트도 자동으로 추가됩니다.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(CanvasGroup))]
|
||||
public class UILoadingBar : UnityEngine.MonoBehaviour
|
||||
public class UILoadingBar : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// 로딩 바 UI 프리팹이 있는 Resources 폴더 내의 경로입니다.
|
||||
/// Show() 메서드가 처음 호출될 때 이 경로를 사용하여 프리팹을 동적으로 생성합니다.
|
||||
/// </summary>
|
||||
public static string PrefabPath = "Prefabs/UI/Loading/UILoadingBar";
|
||||
|
||||
/// <summary>
|
||||
/// UILoadingBar의 유일한 인스턴스(Singleton)입니다.
|
||||
/// static으로 선언되어 어디서든 UILoadingBar.instance 형태로 접근할 수 있습니다.
|
||||
/// </summary>
|
||||
private static UILoadingBar instance;
|
||||
|
||||
/// <summary>
|
||||
/// 0.0~1.0
|
||||
/// 로딩 진행률을 0.0에서 1.0 사이의 값으로 설정하거나 가져옵니다.
|
||||
/// 이 값은 로딩 이미지의 채워지는 양(fillAmount)에 직접 반영됩니다.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // 로딩 진행률을 50%로 설정
|
||||
/// UILoadingBar.Percent = 0.5f;
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static float Percent
|
||||
{
|
||||
get
|
||||
@@ -29,6 +49,15 @@ namespace UVC.UI.Loading
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 로딩 화면에 표시될 메시지를 설정하거나 가져옵니다.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // 로딩 메시지 설정
|
||||
/// UILoadingBar.Message = "데이터를 불러오는 중입니다...";
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static string Message
|
||||
{
|
||||
get
|
||||
@@ -43,6 +72,20 @@ namespace UVC.UI.Loading
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 로딩 바를 화면에 표시합니다.
|
||||
/// 만약 로딩 바 인스턴스가 없다면 PrefabPath에서 프리팹을 불러와 생성합니다.
|
||||
/// </summary>
|
||||
/// <param name="message">로딩 화면에 표시할 메시지입니다.</param>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // 기본 메시지로 로딩 바 표시
|
||||
/// UILoadingBar.Show();
|
||||
///
|
||||
/// // 특정 메시지와 함께 로딩 바 표시
|
||||
/// UILoadingBar.Show("플레이어 정보를 로딩 중입니다...");
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static void Show(string message = "")
|
||||
{
|
||||
if (instance == null) {
|
||||
@@ -56,6 +99,16 @@ namespace UVC.UI.Loading
|
||||
instance.ShowLoading();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 로딩 바를 화면에서 숨깁니다.
|
||||
/// 숨겨진 후에는 자동으로 파괴됩니다.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // 로딩 완료 후 로딩 바 숨기기
|
||||
/// UILoadingBar.Hide();
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static void Hide()
|
||||
{
|
||||
if (instance != null)
|
||||
@@ -64,64 +117,74 @@ namespace UVC.UI.Loading
|
||||
}
|
||||
}
|
||||
|
||||
[Tooltip("페이드 인/아웃 효과를 제어하는 CanvasGroup 컴포넌트입니다. 알파 값을 조정하여 UI의 투명도를 변경합니다.")]
|
||||
[SerializeField]
|
||||
private CanvasGroup canvasGroup; // 페이드 효과를 제어하기 위한 CanvasGroup 컴포넌트
|
||||
[Tooltip("로딩 진행 상태를 표시하는 이미지 컴포넌트입니다. 로딩 중 진행률을 시각적으로 나타냅니다.")]
|
||||
[SerializeField]
|
||||
private Image loadinImage; // 로딩 진행 상태를 표시하는 이미지
|
||||
[Tooltip("로딩 메시지를 표시하는 텍스트 컴포넌트입니다. 로딩 중 사용자에게 정보를 제공합니다.")]
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI text; // 로딩 메시지를 표시하는 텍스트
|
||||
private float target = 0; // 애니메이션의 목표 알파 값 (0: 투명, 1: 불투명)
|
||||
private float duration = 0.25f; // 페이드 인/아웃 애니메이션 지속 시간
|
||||
private bool animatting = false; // 현재 애니메이션이 진행 중인지 여부
|
||||
|
||||
|
||||
private CanvasGroup canvasGroup;
|
||||
private float target = 0;
|
||||
private float duration = 0.25f;
|
||||
private float alpha = 1;
|
||||
private bool animatting = false;
|
||||
private Image loadinImage;
|
||||
private Transform loadingImageTransform;
|
||||
private TextMeshProUGUI text;
|
||||
|
||||
private float loadingSpeed = 1.5f;
|
||||
private float rotationSpeed = 1.0f;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
canvasGroup = GetComponent<CanvasGroup>();
|
||||
loadinImage = transform.Find("loadingImage").GetComponent<Image>();
|
||||
text = transform.Find("message").GetComponent<TextMeshProUGUI>();
|
||||
loadingImageTransform = loadinImage.transform;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 로딩 바를 부드럽게 나타나게 하는 애니메이션을 시작합니다.
|
||||
/// 이미 나타나는 중이면 다시 호출되지 않습니다.
|
||||
/// </summary>
|
||||
public void ShowLoading()
|
||||
{
|
||||
// 이미 애니메이션 중이고 목표 알파 값이 1(불투명)이면 중복 실행 방지
|
||||
if (animatting && target == 1) return;
|
||||
|
||||
target = 1;
|
||||
animatting = true;
|
||||
StopCoroutine("Animate");
|
||||
StartCoroutine(Animate());
|
||||
StopCoroutine("Animate"); // 이전에 실행 중이던 Animate 코루틴이 있다면 중지
|
||||
StartCoroutine(Animate()); // Animate 코루틴 시작
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 로딩 바를 부드럽게 사라지게 하는 애니메이션을 시작합니다.
|
||||
/// 이미 사라지는 중이면 다시 호출되지 않습니다.
|
||||
/// </summary>
|
||||
public void HideLoading()
|
||||
{
|
||||
// 이미 애니메이션 중이고 목표 알파 값이 0(투명)이면 중복 실행 방지
|
||||
if (animatting && target == 0) return;
|
||||
|
||||
target = 0;
|
||||
animatting = true;
|
||||
StopCoroutine("Animate");
|
||||
StartCoroutine(Animate());
|
||||
StopCoroutine("Animate"); // 이전에 실행 중이던 Animate 코루틴이 있다면 중지
|
||||
StartCoroutine(Animate()); // Animate 코루틴 시작
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CanvasGroup의 알파 값을 조절하여 페이드 인/아웃 효과를 주는 코루틴입니다.
|
||||
/// 코루틴은 특정 시간 동안 또는 특정 조건이 만족될 때까지 함수의 실행을 잠시 멈출 수 있는 특별한 함수입니다.
|
||||
/// </summary>
|
||||
private IEnumerator Animate()
|
||||
{
|
||||
float start = canvasGroup.alpha;
|
||||
float time = 0;
|
||||
float start = canvasGroup.alpha; // 애니메이션 시작 시점의 알파 값
|
||||
float time = 0; // 애니메이션 경과 시간
|
||||
|
||||
// 경과 시간이 duration에 도달할 때까지 반복
|
||||
while (time < duration)
|
||||
{
|
||||
time += Time.deltaTime;
|
||||
time += Time.deltaTime; // 한 프레임 동안의 시간을 더해줌
|
||||
// Mathf.Lerp를 사용하여 시작 알파 값과 목표 알파 값 사이를 부드럽게 보간
|
||||
canvasGroup.alpha = Mathf.Lerp(start, target, time / duration);
|
||||
// 다음 프레임까지 대기
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// 애니메이션이 끝난 후 목표 알파 값으로 정확히 설정
|
||||
canvasGroup.alpha = target;
|
||||
animatting = false;
|
||||
if(target == 0)
|
||||
|
||||
// 만약 목표 알파 값이 0(사라지는 애니메이션)이었다면, 게임 오브젝트를 파괴
|
||||
if (target == 0)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
8
Assets/Scripts/UVC/UI/Tab.meta
Normal file
8
Assets/Scripts/UVC/UI/Tab.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 87d7308ca55b2a34e818090493d0b897
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
16
Assets/Scripts/UVC/UI/Tab/ITabContent.cs
Normal file
16
Assets/Scripts/UVC/UI/Tab/ITabContent.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
#nullable enable
|
||||
namespace UVC.UI.Tab
|
||||
{
|
||||
/// <summary>
|
||||
/// 모든 탭 콘텐츠 클래스가 구현해야 하는 인터페이스입니다.
|
||||
/// 데이터를 받을 수 있는 공통 메서드를 정의합니다.
|
||||
/// </summary>
|
||||
public interface ITabContent
|
||||
{
|
||||
/// <summary>
|
||||
/// 탭 콘텐츠에 데이터를 전달합니다.
|
||||
/// </summary>
|
||||
/// <param name="data">전달할 데이터 객체</param>
|
||||
void SetContentData(object? data);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UI/Tab/ITabContent.cs.meta
Normal file
2
Assets/Scripts/UVC/UI/Tab/ITabContent.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2226fb1b6c4b11a4a9f899ab5e456a21
|
||||
113
Assets/Scripts/UVC/UI/Tab/TabButtonView.cs
Normal file
113
Assets/Scripts/UVC/UI/Tab/TabButtonView.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace UVC.UI.Tab
|
||||
{
|
||||
/// <summary>
|
||||
/// 단일 탭 버튼의 UI를 관리하는 컴포넌트입니다.
|
||||
/// </summary>
|
||||
public class TabButtonView : MonoBehaviour
|
||||
{
|
||||
[Tooltip("탭 버튼 컴포넌트")]
|
||||
[SerializeField] private Button? button;
|
||||
|
||||
[Tooltip("탭 이름을 표시할 Text 컴포넌트")]
|
||||
[SerializeField] private TextMeshProUGUI? tabText;
|
||||
|
||||
[Tooltip("탭 아이콘을 표시할 Image 컴포넌트")]
|
||||
[SerializeField] private Image? tabIcon;
|
||||
|
||||
[Tooltip("탭 배경 이미지 컴포넌트")]
|
||||
[SerializeField] private Image? background;
|
||||
|
||||
[Tooltip("탭이 활성화되었을 때의 색상")]
|
||||
[SerializeField] private Color? activeColor;// = Color.white;
|
||||
|
||||
[Tooltip("탭이 비활성화되었을 때의 색상")]
|
||||
[SerializeField] private Color? inactiveColor;// = new Color(0.8f, 0.8f, 0.8f);
|
||||
|
||||
private int _tabIndex;
|
||||
private Action<int>? _onTabSelected;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Button 컴포넌트가 할당되지 않은 경우 자동으로 찾기
|
||||
if (button == null)
|
||||
button = GetComponent<Button>();
|
||||
|
||||
// 버튼 클릭 이벤트 연결
|
||||
if (button != null)
|
||||
button.onClick.AddListener(OnButtonClick);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탭 버튼을 초기화합니다.
|
||||
/// </summary>
|
||||
/// <param name="index">탭 인덱스</param>
|
||||
/// <param name="tabName">탭 이름</param>
|
||||
/// <param name="icon">탭 아이콘</param>
|
||||
/// <param name="onSelectedCallback">탭 선택 시 호출될 콜백</param>
|
||||
public void Setup(int index, string tabName, Sprite? icon, Action<int> onSelectedCallback)
|
||||
{
|
||||
_tabIndex = index;
|
||||
_onTabSelected = onSelectedCallback;
|
||||
|
||||
// 탭 이름 설정
|
||||
if (tabText != null)
|
||||
tabText.text = tabName;
|
||||
|
||||
// 탭 아이콘 설정
|
||||
if (tabIcon != null)
|
||||
{
|
||||
if (icon != null)
|
||||
{
|
||||
tabIcon.sprite = icon;
|
||||
tabIcon.gameObject.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
tabIcon.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탭 버튼의 활성화 상태를 변경합니다.
|
||||
/// </summary>
|
||||
/// <param name="isActive">활성화 여부</param>
|
||||
public void SetActive(bool isActive)
|
||||
{
|
||||
// 배경 색상 변경
|
||||
if (background != null && activeColor.HasValue && inactiveColor.HasValue)
|
||||
{
|
||||
background.color = isActive ? activeColor.Value : inactiveColor.Value;
|
||||
}
|
||||
else if(button != null)
|
||||
{
|
||||
button.image.color = isActive ? button.colors.pressedColor : button.colors.normalColor;
|
||||
}
|
||||
|
||||
// 텍스트 스타일 변경
|
||||
if (tabText != null) {
|
||||
tabText.fontStyle = isActive ? FontStyles.Bold : FontStyles.Normal;
|
||||
}
|
||||
}
|
||||
|
||||
// 버튼 클릭 이벤트 처리
|
||||
private void OnButtonClick()
|
||||
{
|
||||
_onTabSelected?.Invoke(_tabIndex);
|
||||
}
|
||||
|
||||
// 게임 오브젝트가 제거될 때 이벤트 리스너 제거
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (button != null)
|
||||
button.onClick.RemoveListener(OnButtonClick);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UI/Tab/TabButtonView.cs.meta
Normal file
2
Assets/Scripts/UVC/UI/Tab/TabButtonView.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 58af61b23f40877429f8889337d2006a
|
||||
29
Assets/Scripts/UVC/UI/Tab/TabContentConfig.cs
Normal file
29
Assets/Scripts/UVC/UI/Tab/TabContentConfig.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
#nullable enable
|
||||
using UnityEngine;
|
||||
|
||||
namespace UVC.UI.Tab
|
||||
{
|
||||
/// <summary>
|
||||
/// 탭 컨텐츠 설정 정보를 정의하는 클래스
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class TabContentConfig
|
||||
{
|
||||
public string tabID = "";
|
||||
public string tabName = "";
|
||||
public string contentPath = "";
|
||||
public Sprite? tabIcon = null;
|
||||
public bool useLazyLoading = false;
|
||||
public object? initialData = null;
|
||||
|
||||
public TabContentConfig(string id, string name, string path, Sprite? icon = null, bool lazy = false, object? data = null)
|
||||
{
|
||||
tabID = id;
|
||||
tabName = name;
|
||||
contentPath = path;
|
||||
tabIcon = icon;
|
||||
useLazyLoading = lazy;
|
||||
initialData = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UI/Tab/TabContentConfig.cs.meta
Normal file
2
Assets/Scripts/UVC/UI/Tab/TabContentConfig.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7ed77ea0eae0094b92dd0507d68f76d
|
||||
285
Assets/Scripts/UVC/UI/Tab/TabContentLoader.cs
Normal file
285
Assets/Scripts/UVC/UI/Tab/TabContentLoader.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace UVC.UI.Tab
|
||||
{
|
||||
/// <summary>
|
||||
/// 탭 컨텐츠의 동적 로딩을 관리하는 클래스입니다.
|
||||
/// TabView에 소속되어 지연 로딩 기능을 제공합니다.
|
||||
/// </summary>
|
||||
public class TabContentLoader : MonoBehaviour
|
||||
{
|
||||
[Tooltip("지연 로딩 사용 여부")]
|
||||
[SerializeField] private bool lazyLoadTabs = true;
|
||||
|
||||
[Tooltip("최대 동시 로드 탭 수")]
|
||||
[SerializeField] private int maxLoadedTabs = 3;
|
||||
|
||||
[Tooltip("탭 전환 시 자동 로드/언로드 여부")]
|
||||
[SerializeField] private bool autoManageTabs = true;
|
||||
|
||||
/// <summary>
|
||||
/// 지연 로딩할 탭 컨텐츠 정보를 정의하는 클래스입니다.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class LazyTabContent
|
||||
{
|
||||
public string tabID = "";
|
||||
public string contentPrefabPath = "";
|
||||
public bool isLoaded = false;
|
||||
|
||||
[HideInInspector] public GameObject? instance = null;
|
||||
[HideInInspector] public object? contentData = null;
|
||||
|
||||
public LazyTabContent(string id, string path, object? data = null)
|
||||
{
|
||||
tabID = id;
|
||||
contentPrefabPath = path;
|
||||
contentData = data;
|
||||
isLoaded = false;
|
||||
instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 지연 로드 컨텐츠 목록
|
||||
private List<LazyTabContent> _lazyContents = new List<LazyTabContent>();
|
||||
|
||||
// 초기화 여부
|
||||
private bool _isInitialized = false;
|
||||
|
||||
// 현재 활성화된 탭 ID
|
||||
private string? _currentTabID = null;
|
||||
|
||||
// 부모 TabView 참조
|
||||
private TabView? _parentTabView;
|
||||
|
||||
// 이벤트
|
||||
public UnityEvent<string> OnTabContentLoaded = new UnityEvent<string>();
|
||||
public UnityEvent<string> OnTabContentUnloaded = new UnityEvent<string>();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_parentTabView = GetComponentInParent<TabView>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탭 로더를 초기화합니다.
|
||||
/// </summary>
|
||||
public void InitializeTabLoader()
|
||||
{
|
||||
if (_isInitialized) return;
|
||||
|
||||
if (!lazyLoadTabs)
|
||||
{
|
||||
PreloadAllTabs();
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지연 로드 탭 설정을 추가합니다.
|
||||
/// </summary>
|
||||
public bool AddLazyTabContent(string tabID, string prefabPath, object? initialData = null)
|
||||
{
|
||||
LazyTabContent content = new LazyTabContent(tabID, prefabPath, initialData);
|
||||
_lazyContents.Add(content);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 지연 로드 설정을 제거합니다.
|
||||
/// </summary>
|
||||
public void ClearLazyContents()
|
||||
{
|
||||
foreach (var content in _lazyContents)
|
||||
{
|
||||
if (content.isLoaded && content.instance != null)
|
||||
{
|
||||
Destroy(content.instance);
|
||||
}
|
||||
}
|
||||
|
||||
_lazyContents.Clear();
|
||||
_isInitialized = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 탭의 컨텐츠를 로드합니다.
|
||||
/// </summary>
|
||||
public GameObject? LoadTabContent(string tabID, object? data = null)
|
||||
{
|
||||
LazyTabContent? contentToLoad = _lazyContents.Find(c => c.tabID == tabID);
|
||||
if (contentToLoad == null)
|
||||
{
|
||||
Debug.LogWarning($"지연 로드할 탭 컨텐츠를 찾을 수 없음: {tabID}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 이미 로드된 경우
|
||||
if (contentToLoad.isLoaded && contentToLoad.instance != null)
|
||||
{
|
||||
if (data != null)
|
||||
{
|
||||
contentToLoad.contentData = data;
|
||||
UpdateContentData(contentToLoad);
|
||||
}
|
||||
return contentToLoad.instance;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 프리팹 로드
|
||||
GameObject prefab = Resources.Load<GameObject>(contentToLoad.contentPrefabPath);
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogError($"탭 컨텐츠 프리팹을 찾을 수 없음: {contentToLoad.contentPrefabPath}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// TabView의 ContentContainer를 부모로 사용
|
||||
Transform parentTransform = _parentTabView?.ContentContainer ?? transform;
|
||||
|
||||
// 인스턴스 생성
|
||||
contentToLoad.instance = Instantiate(prefab, parentTransform);
|
||||
contentToLoad.instance.name = $"LazyTabContent_{tabID}";
|
||||
contentToLoad.instance.SetActive(false);
|
||||
contentToLoad.isLoaded = true;
|
||||
|
||||
// 데이터 설정
|
||||
if (data != null)
|
||||
{
|
||||
contentToLoad.contentData = data;
|
||||
}
|
||||
UpdateContentData(contentToLoad);
|
||||
|
||||
// 이벤트 발생
|
||||
OnTabContentLoaded.Invoke(tabID);
|
||||
|
||||
Debug.Log($"지연 로드 탭 컨텐츠 로드됨: {tabID}");
|
||||
return contentToLoad.instance;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"탭 컨텐츠 로드 중 오류 발생: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 탭 컨텐츠의 데이터를 업데이트합니다.
|
||||
/// </summary>
|
||||
private void UpdateContentData(LazyTabContent content)
|
||||
{
|
||||
if (content.instance != null && content.contentData != null)
|
||||
{
|
||||
ITabContent? tabContent = content.instance.GetComponent<ITabContent>();
|
||||
tabContent?.SetContentData(content.contentData);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 탭의 컨텐츠를 언로드합니다.
|
||||
/// </summary>
|
||||
public bool UnloadTabContent(string tabID)
|
||||
{
|
||||
LazyTabContent? contentToUnload = _lazyContents.Find(c => c.tabID == tabID);
|
||||
if (contentToUnload == null || !contentToUnload.isLoaded || contentToUnload.instance == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Destroy(contentToUnload.instance);
|
||||
contentToUnload.instance = null;
|
||||
contentToUnload.isLoaded = false;
|
||||
|
||||
OnTabContentUnloaded.Invoke(tabID);
|
||||
Debug.Log($"지연 로드 탭 컨텐츠 언로드됨: {tabID}");
|
||||
return true;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"탭 컨텐츠 언로드 중 오류 발생: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 활성화된 탭을 제외한 모든 탭을 언로드합니다.
|
||||
/// </summary>
|
||||
public void UnloadAllExceptCurrent(string currentTabID)
|
||||
{
|
||||
foreach (var content in _lazyContents)
|
||||
{
|
||||
if (content.tabID != currentTabID && content.isLoaded)
|
||||
{
|
||||
UnloadTabContent(content.tabID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 탭 컨텐츠를 미리 로드합니다.
|
||||
/// </summary>
|
||||
public void PreloadAllTabs()
|
||||
{
|
||||
foreach (var content in _lazyContents)
|
||||
{
|
||||
if (!content.isLoaded)
|
||||
{
|
||||
LoadTabContent(content.tabID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 탭의 데이터를 설정합니다.
|
||||
/// </summary>
|
||||
public void SetTabContentData(string tabID, object? data)
|
||||
{
|
||||
LazyTabContent? content = _lazyContents.Find(c => c.tabID == tabID);
|
||||
if (content != null)
|
||||
{
|
||||
content.contentData = data;
|
||||
|
||||
if (content.isLoaded && content.instance != null)
|
||||
{
|
||||
UpdateContentData(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 탭이 로드되었는지 확인합니다.
|
||||
/// </summary>
|
||||
public bool IsTabLoaded(string tabID)
|
||||
{
|
||||
LazyTabContent? content = _lazyContents.Find(c => c.tabID == tabID);
|
||||
return content?.isLoaded == true && content.instance != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 탭의 컨텐츠 인스턴스를 가져옵니다.
|
||||
/// </summary>
|
||||
public GameObject? GetTabInstance(string tabID)
|
||||
{
|
||||
LazyTabContent? content = _lazyContents.Find(c => c.tabID == tabID);
|
||||
return content?.isLoaded == true ? content.instance : null;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
foreach (var content in _lazyContents)
|
||||
{
|
||||
if (content.isLoaded && content.instance != null)
|
||||
{
|
||||
Destroy(content.instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UI/Tab/TabContentLoader.cs.meta
Normal file
2
Assets/Scripts/UVC/UI/Tab/TabContentLoader.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 19466bc7c0ad9c940820d52eb7e676a6
|
||||
352
Assets/Scripts/UVC/UI/Tab/TabController.cs
Normal file
352
Assets/Scripts/UVC/UI/Tab/TabController.cs
Normal file
@@ -0,0 +1,352 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UVC.UI.Tab
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 탭 시스템을 제어하는 컨트롤러 클래스입니다.
|
||||
/// 설정 관리와 조정 역할만 담당하고, 실제 UI와 로딩은 TabView에서 처리합니다.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <b>사용 예제:</b>
|
||||
/// 1. TabContentConfig 설정
|
||||
/// 2. 컨트롤러 초기화
|
||||
/// <code>
|
||||
/// public class TabSetup : MonoBehaviour
|
||||
/// {
|
||||
/// [SerializeField]
|
||||
/// protected TabController? tabController;
|
||||
///
|
||||
/// protected virtual void Awake()
|
||||
/// {
|
||||
/// if (tabController == null)
|
||||
/// {
|
||||
/// Debug.LogError("TabController가 설정되지 않았습니다.");
|
||||
/// return;
|
||||
/// }
|
||||
///
|
||||
/// // 코드로 탭 설정하기
|
||||
/// SetupTabs();
|
||||
/// }
|
||||
///
|
||||
/// private void SetupTabs()
|
||||
/// {
|
||||
/// // 1. TabConfig 설정
|
||||
/// tabController?.AddTabConfig("AGV", "AGV", "Prefabs/Factory/UI/Tab/DraggableListContent", null, CreateAGVData(), true);
|
||||
/// tabController?.AddTabConfig("ALARM", "ALARM", "Prefabs/Factory/UI/Tab/DraggableListContent", null, CreateAlarmData(), true);
|
||||
///
|
||||
/// // 2. 컨트롤러 초기화
|
||||
/// tabController?.Initialize();
|
||||
/// }
|
||||
///
|
||||
/// // 샘플 데이터 생성 메서드들
|
||||
///
|
||||
/// private object CreateAGVData()
|
||||
/// {
|
||||
/// Dictionary<string, string> data = new Dictionary<string, string>();
|
||||
/// return data;
|
||||
/// }
|
||||
///
|
||||
/// private object CreateAlarmData()
|
||||
/// {
|
||||
/// Dictionary<string, string> data = new Dictionary<string, string>();
|
||||
/// return data;
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class TabController : MonoBehaviour
|
||||
{
|
||||
[Header("탭 설정")]
|
||||
[Tooltip("탭 뷰 컴포넌트")]
|
||||
[SerializeField] private TabView? tabView;
|
||||
|
||||
[Header("탭 컨텐츠")]
|
||||
[Tooltip("시작 시 자동 초기화 여부")]
|
||||
[SerializeField] private bool initializeOnStart = false;
|
||||
|
||||
[Tooltip("탭 설정 목록")]
|
||||
[SerializeField] private TabContentConfig[] tabConfigs = new TabContentConfig[0];
|
||||
|
||||
// 탭 모델
|
||||
private TabModel? _tabModel;
|
||||
|
||||
// 초기화 여부
|
||||
private bool _isInitialized = false;
|
||||
|
||||
// 코드로 추가된 탭 설정 저장
|
||||
private List<TabContentConfig> _additionalTabConfigs = new List<TabContentConfig>();
|
||||
|
||||
// 탭 변경 이벤트
|
||||
public event Action<int>? OnTabChanged;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_tabModel = new TabModel();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (initializeOnStart)
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
#region 코드 기반 탭 설정 관리
|
||||
|
||||
/// <summary>
|
||||
/// 코드에서 탭 설정을 추가합니다.
|
||||
/// </summary>
|
||||
public bool AddTabConfig(string id, string name, string path, Sprite? icon = null, object? data = null, bool useLazyLoading = false)
|
||||
{
|
||||
if (_isInitialized)
|
||||
{
|
||||
Debug.LogWarning("탭 시스템이 이미 초기화되었습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var config = new TabContentConfig(id, name, path, icon, useLazyLoading, data);
|
||||
_additionalTabConfigs.Add(config);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 탭 설정을 가져옵니다.
|
||||
/// </summary>
|
||||
public List<TabContentConfig> GetAllTabConfigs()
|
||||
{
|
||||
List<TabContentConfig> allConfigs = new List<TabContentConfig>();
|
||||
|
||||
// Inspector 설정 추가
|
||||
foreach (var config in tabConfigs)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(config.tabID) && !string.IsNullOrEmpty(config.contentPath))
|
||||
{
|
||||
allConfigs.Add(config);
|
||||
}
|
||||
}
|
||||
|
||||
// 코드로 추가된 설정 추가
|
||||
allConfigs.AddRange(_additionalTabConfigs);
|
||||
|
||||
return allConfigs;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 탭 시스템 초기화 및 관리
|
||||
|
||||
/// <summary>
|
||||
/// 탭 시스템을 초기화합니다.
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
if (_isInitialized || _tabModel == null || tabView == null)
|
||||
return;
|
||||
|
||||
// 탭 모델 초기화
|
||||
InitializeModel();
|
||||
|
||||
// TabView에 설정 전달하여 초기화
|
||||
List<TabContentConfig> allConfigs = GetAllTabConfigs();
|
||||
tabView.InitializeTabs(_tabModel.Tabs, allConfigs, OnTabButtonSelected);
|
||||
|
||||
// 모델 이벤트 구독
|
||||
_tabModel.OnTabChanged += HandleTabChanged;
|
||||
|
||||
// 초기 탭 활성화
|
||||
if (_tabModel.Tabs.Count > 0)
|
||||
{
|
||||
HandleTabChanged(_tabModel.ActiveTabIndex);
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탭 모델을 초기화합니다.
|
||||
/// </summary>
|
||||
private void InitializeModel()
|
||||
{
|
||||
if (_tabModel == null) return;
|
||||
|
||||
List<TabContentConfig> allConfigs = GetAllTabConfigs();
|
||||
|
||||
foreach (var config in allConfigs)
|
||||
{
|
||||
_tabModel.AddTab(new TabData(
|
||||
config.tabID,
|
||||
config.tabName,
|
||||
config.contentPath,
|
||||
config.tabIcon,
|
||||
config.initialData
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탭 버튼 선택 이벤트 처리
|
||||
/// </summary>
|
||||
private void OnTabButtonSelected(int tabIndex)
|
||||
{
|
||||
_tabModel?.SwitchToTab(tabIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탭 변경 이벤트 처리
|
||||
/// </summary>
|
||||
private void HandleTabChanged(int newTabIndex)
|
||||
{
|
||||
if (_tabModel == null || tabView == null) return;
|
||||
|
||||
TabData? activeTabData = _tabModel.GetActiveTab();
|
||||
if (activeTabData == null) return;
|
||||
|
||||
// TabView에 탭 변경 전달
|
||||
tabView.UpdateActiveTab(newTabIndex, activeTabData);
|
||||
|
||||
// 외부 이벤트 발생
|
||||
OnTabChanged?.Invoke(newTabIndex);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 탭 활성화 및 데이터 관리
|
||||
|
||||
/// <summary>
|
||||
/// ID로 특정 탭을 활성화합니다.
|
||||
/// </summary>
|
||||
public void ActivateTab(string tabID)
|
||||
{
|
||||
_tabModel?.SwitchToTab(tabID);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 인덱스로 특정 탭을 활성화합니다.
|
||||
/// </summary>
|
||||
public void ActivateTab(int tabIndex)
|
||||
{
|
||||
_tabModel?.SwitchToTab(tabIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 활성화된 탭 인덱스를 반환합니다.
|
||||
/// </summary>
|
||||
public int GetActiveTabIndex()
|
||||
{
|
||||
return _tabModel?.ActiveTabIndex ?? -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 활성화된 탭의 ID를 반환합니다.
|
||||
/// </summary>
|
||||
public string? GetActiveTabID()
|
||||
{
|
||||
TabData? activeTab = _tabModel?.GetActiveTab();
|
||||
return activeTab?.tabID;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 탭의 데이터를 설정하고 갱신합니다.
|
||||
/// </summary>
|
||||
public void SetTabContentData(string tabID, object? data)
|
||||
{
|
||||
if (_tabModel == null || tabView == null) return;
|
||||
|
||||
// 모델 업데이트
|
||||
_tabModel.UpdateTabContentData(tabID, data);
|
||||
|
||||
// TabView에 업데이트 전달
|
||||
int tabIndex = GetTabIndexByID(tabID);
|
||||
if (tabIndex >= 0)
|
||||
{
|
||||
tabView.UpdateTabContentData(tabIndex, data);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탭 ID로 인덱스를 찾습니다.
|
||||
/// </summary>
|
||||
private int GetTabIndexByID(string tabID)
|
||||
{
|
||||
if (_tabModel != null)
|
||||
{
|
||||
for (int i = 0; i < _tabModel.Tabs.Count; i++)
|
||||
{
|
||||
if (_tabModel.Tabs[i].tabID == tabID)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TabView 위임 메서드
|
||||
|
||||
/// <summary>
|
||||
/// 특정 탭의 컨텐츠 인스턴스를 가져옵니다.
|
||||
/// </summary>
|
||||
public GameObject? GetTabInstance(string tabID, bool autoLoad = false)
|
||||
{
|
||||
return tabView?.GetTabInstance(tabID, autoLoad);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 탭의 컴포넌트를 가져옵니다.
|
||||
/// </summary>
|
||||
public T? GetTabComponent<T>(string tabID, bool autoLoad = false) where T : Component
|
||||
{
|
||||
return tabView?.GetTabComponent<T>(tabID, autoLoad);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 탭이 로드되었는지 확인합니다.
|
||||
/// </summary>
|
||||
public bool IsTabLoaded(string tabID)
|
||||
{
|
||||
return tabView?.IsTabLoaded(tabID) ?? false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 활성화된 탭을 제외한 모든 지연 로드 탭을 언로드합니다.
|
||||
/// </summary>
|
||||
public void UnloadAllExceptCurrent()
|
||||
{
|
||||
int currentIndex = GetActiveTabIndex();
|
||||
tabView?.UnloadAllExceptCurrent(currentIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 탭의 컨텐츠를 언로드합니다.
|
||||
/// </summary>
|
||||
public bool UnloadTabContent(string tabID)
|
||||
{
|
||||
return tabView?.UnloadTabContent(tabID) ?? false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 탭 컨텐츠를 미리 로드합니다.
|
||||
/// </summary>
|
||||
public void PreloadAllTabs()
|
||||
{
|
||||
tabView?.PreloadAllTabs();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_tabModel != null)
|
||||
{
|
||||
_tabModel.OnTabChanged -= HandleTabChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UI/Tab/TabController.cs.meta
Normal file
2
Assets/Scripts/UVC/UI/Tab/TabController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d811cbf1956561469a1474f3949ea60
|
||||
37
Assets/Scripts/UVC/UI/Tab/TabData.cs
Normal file
37
Assets/Scripts/UVC/UI/Tab/TabData.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UVC.UI.Tab
|
||||
{
|
||||
/// <summary>
|
||||
/// 단일 탭에 대한 데이터를 보관하는 클래스입니다.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class TabData
|
||||
{
|
||||
public string tabID; // 탭 고유 식별자
|
||||
public string tabName; // 탭 표시 이름
|
||||
public string contentPath; // 탭 내용을 담고 있는 Prefab 경로 (Resources 폴더 기준)
|
||||
public Sprite? tabIcon; // 탭 아이콘 (선택사항, null 가능)
|
||||
public object? contentData; // 탭 콘텐츠에 전달할 데이터 객체 (null 가능)
|
||||
|
||||
// 프리팹 경로로 초기화하는 생성자
|
||||
/// <summary>
|
||||
/// 탭 데이터를 초기화합니다.
|
||||
/// </summary>
|
||||
/// <param name="id">탭의 고유 식별자 (예: "inventory", "settings")</param>
|
||||
/// <param name="name">탭의 표시 이름 (예: "인벤토리", "설정")</param>
|
||||
/// <param name="path">탭 컨텐츠 프리팹의 리소스 경로 (예: "Prefabs/UI/InventoryTab")</param>
|
||||
/// <param name="icon">탭 아이콘 이미지 (선택사항)</param>
|
||||
/// <param name="data">탭 컨텐츠에 전달할 초기 데이터 (선택사항)</param>
|
||||
public TabData(string id, string name, string path, Sprite? icon = null, object? data = null)
|
||||
{
|
||||
tabID = id;
|
||||
tabName = name;
|
||||
contentPath = path;
|
||||
tabIcon = icon;
|
||||
contentData = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UI/Tab/TabData.cs.meta
Normal file
2
Assets/Scripts/UVC/UI/Tab/TabData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 11ef99aba8ccd6245ac008e719d0f81a
|
||||
183
Assets/Scripts/UVC/UI/Tab/TabModel.cs
Normal file
183
Assets/Scripts/UVC/UI/Tab/TabModel.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UVC.UI.Tab
|
||||
{
|
||||
/// <summary>
|
||||
/// 전체 탭 시스템의 데이터를 관리하는 모델 클래스입니다.
|
||||
/// </summary>
|
||||
public class TabModel
|
||||
{
|
||||
// 모든 탭 목록
|
||||
private List<TabData> _tabs = new List<TabData>();
|
||||
// 현재 활성화된 탭의 인덱스 (-1은 활성화된 탭이 없음을 의미)
|
||||
private int _activeTabIndex = -1;
|
||||
|
||||
// 탭 컨텐츠 인스턴스를 저장하는 Dictionary (키: tabID)
|
||||
private Dictionary<string, GameObject> _contentInstances = new Dictionary<string, GameObject>();
|
||||
|
||||
/// <summary>
|
||||
/// 현재 활성화된 탭 인덱스를 가져옵니다.
|
||||
/// </summary>
|
||||
public int ActiveTabIndex => _activeTabIndex;
|
||||
|
||||
/// <summary>
|
||||
/// 등록된 모든 탭의 목록을 가져옵니다.
|
||||
/// </summary>
|
||||
public IReadOnlyList<TabData> Tabs => _tabs;
|
||||
|
||||
/// <summary>
|
||||
/// 탭이 변경될 때 발생하는 이벤트입니다.
|
||||
/// </summary>
|
||||
public event Action<int>? OnTabChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 새로운 탭을 모델에 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="tab">추가할 탭 데이터</param>
|
||||
public void AddTab(TabData tab)
|
||||
{
|
||||
_tabs.Add(tab);
|
||||
|
||||
// 첫 번째 추가된 탭을 기본 활성화 탭으로 설정
|
||||
if (_activeTabIndex == -1 && _tabs.Count == 1)
|
||||
{
|
||||
_activeTabIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탭 인덱스로 탭을 전환합니다.
|
||||
/// </summary>
|
||||
/// <param name="tabIndex">활성화할 탭의 인덱스</param>
|
||||
public void SwitchToTab(int tabIndex)
|
||||
{
|
||||
// 인덱스 범위 확인
|
||||
if (tabIndex < 0 || tabIndex >= _tabs.Count)
|
||||
{
|
||||
Debug.LogWarning($"잘못된 탭 인덱스: {tabIndex}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 같은 탭을 다시 선택한 경우 무시
|
||||
if (_activeTabIndex == tabIndex)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 활성화 탭 인덱스 업데이트
|
||||
_activeTabIndex = tabIndex;
|
||||
|
||||
// 이벤트 발생
|
||||
OnTabChanged?.Invoke(_activeTabIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탭 ID로 탭을 전환합니다.
|
||||
/// </summary>
|
||||
/// <param name="tabID">활성화할 탭의 ID</param>
|
||||
public void SwitchToTab(string tabID)
|
||||
{
|
||||
for (int i = 0; i < _tabs.Count; i++)
|
||||
{
|
||||
if (_tabs[i].tabID == tabID)
|
||||
{
|
||||
SwitchToTab(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.LogWarning($"해당 ID의 탭을 찾을 수 없음: {tabID}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 활성화된 탭 데이터를 반환합니다.
|
||||
/// </summary>
|
||||
/// <returns>활성화된 탭 데이터 또는 활성화된 탭이 없는 경우 null</returns>
|
||||
public TabData? GetActiveTab()
|
||||
{
|
||||
if (_activeTabIndex >= 0 && _activeTabIndex < _tabs.Count)
|
||||
{
|
||||
return _tabs[_activeTabIndex];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 탭의 contentData를 업데이트합니다.
|
||||
/// </summary>
|
||||
/// <param name="tabID">업데이트할 탭의 ID</param>
|
||||
/// <param name="newData">새로운 데이터</param>
|
||||
public void UpdateTabContentData(string tabID, object? newData)
|
||||
{
|
||||
for (int i = 0; i < _tabs.Count; i++)
|
||||
{
|
||||
if (_tabs[i].tabID == tabID)
|
||||
{
|
||||
_tabs[i].contentData = newData;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 탭의 contentData를 업데이트합니다.
|
||||
/// </summary>
|
||||
/// <param name="tabIndex">업데이트할 탭의 인덱스</param>
|
||||
/// <param name="newData">새로운 데이터</param>
|
||||
public void UpdateTabContentData(int tabIndex, object? newData)
|
||||
{
|
||||
if (tabIndex >= 0 && tabIndex < _tabs.Count)
|
||||
{
|
||||
_tabs[tabIndex].contentData = newData;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 탭의 컨텐츠 인스턴스를 저장합니다.
|
||||
/// </summary>
|
||||
/// <param name="tabID">탭 ID</param>
|
||||
/// <param name="instance">인스턴스화된 컨텐츠 GameObject</param>
|
||||
public void SetContentInstance(string tabID, GameObject instance)
|
||||
{
|
||||
if (_contentInstances.ContainsKey(tabID))
|
||||
{
|
||||
_contentInstances[tabID] = instance;
|
||||
}
|
||||
else
|
||||
{
|
||||
_contentInstances.Add(tabID, instance);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 탭의 컨텐츠 인스턴스를 가져옵니다.
|
||||
/// </summary>
|
||||
/// <param name="tabID">탭 ID</param>
|
||||
/// <returns>컨텐츠 인스턴스 또는 인스턴스가 없는 경우 null</returns>
|
||||
public GameObject? GetContentInstance(string tabID)
|
||||
{
|
||||
if (_contentInstances.TryGetValue(tabID, out GameObject instance))
|
||||
{
|
||||
return instance;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 인덱스로 특정 탭의 컨텐츠 인스턴스를 가져옵니다.
|
||||
/// </summary>
|
||||
/// <param name="tabIndex">탭 인덱스</param>
|
||||
/// <returns>컨텐츠 인스턴스 또는 인스턴스가 없는 경우 null</returns>
|
||||
public GameObject? GetContentInstance(int tabIndex)
|
||||
{
|
||||
if (tabIndex >= 0 && tabIndex < _tabs.Count)
|
||||
{
|
||||
return GetContentInstance(_tabs[tabIndex].tabID);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UI/Tab/TabModel.cs.meta
Normal file
2
Assets/Scripts/UVC/UI/Tab/TabModel.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35d6d440b8fa06549b7df3c17961456d
|
||||
481
Assets/Scripts/UVC/UI/Tab/TabView.cs
Normal file
481
Assets/Scripts/UVC/UI/Tab/TabView.cs
Normal file
@@ -0,0 +1,481 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UVC.UI.Tab
|
||||
{
|
||||
/// <summary>
|
||||
/// 전체 탭 시스템의 UI를 관리하는 View 클래스입니다.
|
||||
/// TabContentLoader와 협력하여 컨텐츠 로딩도 담당합니다.
|
||||
/// </summary>
|
||||
public class TabView : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// 전체 탭 시스템의 UI를 관리하는 View 클래스입니다.
|
||||
/// TabContentLoader와 협력하여 컨텐츠 로딩도 담당합니다.
|
||||
/// </summary>
|
||||
[Header("UI 설정")]
|
||||
[Tooltip("탭 버튼들이 생성될 부모 Transform")]
|
||||
[SerializeField] private Transform? tabButtonContainer;
|
||||
|
||||
[Tooltip("탭 컨텐츠들이 표시될 부모 Transform")]
|
||||
[SerializeField] private Transform? contentContainer;
|
||||
|
||||
[Tooltip("탭 버튼 프리팹")]
|
||||
[SerializeField] private GameObject? tabButtonPrefab;
|
||||
|
||||
[Header("컨텐츠 로딩")]
|
||||
[Tooltip("컨텐츠 로더 (지연 로딩 기능, 선택사항)")]
|
||||
[SerializeField] private TabContentLoader? contentLoader;
|
||||
|
||||
// 생성된 탭 버튼 컴포넌트들
|
||||
private List<TabButtonView> _tabButtons = new List<TabButtonView>();
|
||||
|
||||
// 모든 컨텐츠 인스턴스를 저장
|
||||
private List<GameObject?> _allContentInstances = new List<GameObject?>();
|
||||
|
||||
// 탭 설정 정보 (TabController에서 전달받음)
|
||||
private List<TabContentConfig> _tabConfigs = new List<TabContentConfig>();
|
||||
|
||||
// 탭 버튼 클릭 시 호출될 콜백
|
||||
private Action<int>? _onTabButtonClicked;
|
||||
|
||||
/// <summary>
|
||||
/// 컨텐츠 컨테이너를 반환합니다.
|
||||
/// </summary>
|
||||
public Transform? ContentContainer => contentContainer;
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 탭 시스템을 초기화합니다.
|
||||
/// </summary>
|
||||
/// <param name="tabs">탭 데이터 목록</param>
|
||||
/// <param name="configs">탭 설정 목록 (TabController에서 전달)</param>
|
||||
/// <param name="onTabSelected">탭 선택 시 호출될 콜백</param>
|
||||
public void InitializeTabs(IReadOnlyList<TabData> tabs, List<TabContentConfig> configs, Action<int> onTabSelected)
|
||||
{
|
||||
// 기존 탭 정리
|
||||
ClearTabs();
|
||||
|
||||
_onTabButtonClicked = onTabSelected;
|
||||
_tabConfigs = new List<TabContentConfig>(configs);
|
||||
|
||||
// 컨텐츠 인스턴스 리스트 초기화
|
||||
_allContentInstances.Clear();
|
||||
for (int i = 0; i < tabs.Count; i++)
|
||||
{
|
||||
_allContentInstances.Add(null);
|
||||
}
|
||||
|
||||
// TabContentLoader 초기화 (있는 경우)
|
||||
if (contentLoader != null)
|
||||
{
|
||||
InitializeContentLoader();
|
||||
}
|
||||
|
||||
// 탭 버튼 생성
|
||||
for (int i = 0; i < tabs.Count; i++)
|
||||
{
|
||||
CreateTabButton(i, tabs[i]);
|
||||
}
|
||||
|
||||
// 지연 로딩을 사용하지 않는 탭들 미리 로드
|
||||
PreloadNonLazyTabs();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TabContentLoader를 초기화합니다.
|
||||
/// </summary>
|
||||
private void InitializeContentLoader()
|
||||
{
|
||||
if (contentLoader == null) return;
|
||||
|
||||
// 기존 설정 클리어
|
||||
contentLoader.ClearLazyContents();
|
||||
|
||||
// 지연 로딩 탭들을 ContentLoader에 등록
|
||||
foreach (var config in _tabConfigs)
|
||||
{
|
||||
if (config.useLazyLoading)
|
||||
{
|
||||
contentLoader.AddLazyTabContent(config.tabID, config.contentPath, config.initialData);
|
||||
}
|
||||
}
|
||||
|
||||
// ContentLoader 초기화
|
||||
contentLoader.InitializeTabLoader();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지연 로딩을 사용하지 않는 탭들을 미리 로드합니다.
|
||||
/// </summary>
|
||||
private void PreloadNonLazyTabs()
|
||||
{
|
||||
for (int i = 0; i < _tabConfigs.Count; i++)
|
||||
{
|
||||
var config = _tabConfigs[i];
|
||||
if (!config.useLazyLoading)
|
||||
{
|
||||
GameObject? instance = LoadTabContentDirectly(config);
|
||||
if (instance != null)
|
||||
{
|
||||
_allContentInstances[i] = instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log("지연 로딩을 사용하지 않는 탭들이 미리 로드되었습니다.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탭 컨텐츠를 직접 로드합니다.
|
||||
/// </summary>
|
||||
private GameObject? LoadTabContentDirectly(TabContentConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 프리팹 로드
|
||||
GameObject prefab = Resources.Load<GameObject>(config.contentPath);
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogError($"탭 컨텐츠 프리팹을 찾을 수 없음: {config.contentPath}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 인스턴스 생성
|
||||
GameObject instance = Instantiate(prefab, contentContainer);
|
||||
instance.name = $"TabContent_{config.tabID}";
|
||||
instance.SetActive(false);
|
||||
|
||||
// 초기 데이터 전달
|
||||
if (config.initialData != null)
|
||||
{
|
||||
ITabContent? tabContent = instance.GetComponent<ITabContent>();
|
||||
tabContent?.SetContentData(config.initialData);
|
||||
}
|
||||
|
||||
Debug.Log($"탭 컨텐츠 직접 로드됨: {config.tabID}");
|
||||
return instance;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"탭 컨텐츠 직접 로드 중 오류 발생: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탭 버튼을 생성합니다.
|
||||
/// </summary>
|
||||
private void CreateTabButton(int index, TabData tabData)
|
||||
{
|
||||
if (tabButtonPrefab == null)
|
||||
{
|
||||
Debug.LogError("탭 버튼 프리팹이 설정되지 않았습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (tabButtonContainer == null)
|
||||
{
|
||||
Debug.LogError("탭 버튼 컨테이너가 설정되지 않았습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 탭 버튼 인스턴스 생성
|
||||
GameObject tabButtonGO = Instantiate(tabButtonPrefab, tabButtonContainer);
|
||||
TabButtonView? tabButton = tabButtonGO.GetComponent<TabButtonView>();
|
||||
|
||||
if (tabButton == null)
|
||||
{
|
||||
Debug.LogError("탭 버튼 프리팹에 TabButtonView 컴포넌트가 없습니다.");
|
||||
Destroy(tabButtonGO);
|
||||
return;
|
||||
}
|
||||
|
||||
// 탭 버튼 설정
|
||||
tabButton.Setup(index, tabData.tabName, tabData.tabIcon, OnTabButtonClicked);
|
||||
_tabButtons.Add(tabButton);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탭 버튼 클릭 이벤트 처리
|
||||
/// </summary>
|
||||
private void OnTabButtonClicked(int tabIndex)
|
||||
{
|
||||
_onTabButtonClicked?.Invoke(tabIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 활성화된 탭을 업데이트합니다.
|
||||
/// 필요한 경우 컨텐츠를 로드합니다.
|
||||
/// </summary>
|
||||
/// <param name="tabIndex">활성화할 탭 인덱스</param>
|
||||
/// <param name="tabData">탭 데이터</param>
|
||||
public void UpdateActiveTab(int tabIndex, TabData tabData)
|
||||
{
|
||||
if (tabIndex < 0 || tabIndex >= _tabConfigs.Count) return;
|
||||
|
||||
var config = _tabConfigs[tabIndex];
|
||||
|
||||
// 컨텐츠가 아직 로드되지 않은 경우 로드
|
||||
if (_allContentInstances[tabIndex] == null)
|
||||
{
|
||||
if (config.useLazyLoading && contentLoader != null)
|
||||
{
|
||||
// 지연 로딩을 통해 로드
|
||||
GameObject? lazyInstance = contentLoader.LoadTabContent(config.tabID, tabData.contentData);
|
||||
if (lazyInstance != null)
|
||||
{
|
||||
// ContentContainer로 이동 (일관성 확보)
|
||||
if (lazyInstance.transform.parent != contentContainer)
|
||||
{
|
||||
lazyInstance.transform.SetParent(contentContainer, false);
|
||||
}
|
||||
_allContentInstances[tabIndex] = lazyInstance;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 직접 로드
|
||||
_allContentInstances[tabIndex] = LoadTabContentDirectly(config);
|
||||
}
|
||||
}
|
||||
|
||||
// 모든 컨텐츠 비활성화
|
||||
foreach (var content in _allContentInstances)
|
||||
{
|
||||
if (content != null)
|
||||
content.SetActive(false);
|
||||
}
|
||||
|
||||
// 활성화된 컨텐츠만 활성화
|
||||
GameObject? activeContent = _allContentInstances[tabIndex];
|
||||
if (activeContent != null)
|
||||
{
|
||||
activeContent.SetActive(true);
|
||||
|
||||
// 데이터 업데이트
|
||||
if (tabData.contentData != null)
|
||||
{
|
||||
ITabContent? tabContent = activeContent.GetComponent<ITabContent>();
|
||||
tabContent?.SetContentData(tabData.contentData);
|
||||
}
|
||||
}
|
||||
|
||||
// 탭 버튼 상태 업데이트
|
||||
for (int i = 0; i < _tabButtons.Count; i++)
|
||||
{
|
||||
if (_tabButtons[i] != null)
|
||||
_tabButtons[i].SetActive(i == tabIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 탭의 컨텐츠 데이터를 업데이트합니다.
|
||||
/// </summary>
|
||||
/// <param name="tabIndex">업데이트할 탭 인덱스</param>
|
||||
/// <param name="data">새로운 데이터</param>
|
||||
public void UpdateTabContentData(int tabIndex, object? data)
|
||||
{
|
||||
if (tabIndex < 0 || tabIndex >= _allContentInstances.Count) return;
|
||||
|
||||
GameObject? contentInstance = _allContentInstances[tabIndex];
|
||||
if (contentInstance != null && data != null)
|
||||
{
|
||||
ITabContent? tabContent = contentInstance.GetComponent<ITabContent>();
|
||||
tabContent?.SetContentData(data);
|
||||
}
|
||||
|
||||
// 지연 로딩 탭인 경우 ContentLoader에도 업데이트
|
||||
if (tabIndex < _tabConfigs.Count)
|
||||
{
|
||||
var config = _tabConfigs[tabIndex];
|
||||
if (config.useLazyLoading && contentLoader != null)
|
||||
{
|
||||
contentLoader.SetTabContentData(config.tabID, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 탭의 컨텐츠 인스턴스를 가져옵니다.
|
||||
/// </summary>
|
||||
/// <param name="tabID">탭 ID</param>
|
||||
/// <param name="autoLoad">자동 로드 여부</param>
|
||||
/// <returns>컨텐츠 인스턴스</returns>
|
||||
public GameObject? GetTabInstance(string tabID, bool autoLoad = false)
|
||||
{
|
||||
int tabIndex = GetTabIndexByID(tabID);
|
||||
if (tabIndex < 0) return null;
|
||||
|
||||
// 이미 로드된 인스턴스가 있으면 반환
|
||||
if (_allContentInstances[tabIndex] != null)
|
||||
{
|
||||
return _allContentInstances[tabIndex];
|
||||
}
|
||||
|
||||
// 자동 로드가 활성화된 경우
|
||||
if (autoLoad && tabIndex < _tabConfigs.Count)
|
||||
{
|
||||
var config = _tabConfigs[tabIndex];
|
||||
|
||||
if (config.useLazyLoading && contentLoader != null)
|
||||
{
|
||||
// 지연 로딩을 통해 로드
|
||||
GameObject? instance = contentLoader.LoadTabContent(tabID);
|
||||
if (instance != null)
|
||||
{
|
||||
// ContentContainer로 이동
|
||||
if (instance.transform.parent != contentContainer)
|
||||
{
|
||||
instance.transform.SetParent(contentContainer, false);
|
||||
}
|
||||
_allContentInstances[tabIndex] = instance;
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 직접 로드
|
||||
GameObject? instance = LoadTabContentDirectly(config);
|
||||
if (instance != null)
|
||||
{
|
||||
_allContentInstances[tabIndex] = instance;
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 탭의 컴포넌트를 가져옵니다.
|
||||
/// </summary>
|
||||
public T? GetTabComponent<T>(string tabID, bool autoLoad = false) where T : Component
|
||||
{
|
||||
GameObject? instance = GetTabInstance(tabID, autoLoad);
|
||||
return instance?.GetComponent<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탭 ID로 인덱스를 찾습니다.
|
||||
/// </summary>
|
||||
private int GetTabIndexByID(string tabID)
|
||||
{
|
||||
for (int i = 0; i < _tabConfigs.Count; i++)
|
||||
{
|
||||
if (_tabConfigs[i].tabID == tabID)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 탭이 로드되었는지 확인합니다.
|
||||
/// </summary>
|
||||
public bool IsTabLoaded(string tabID)
|
||||
{
|
||||
int tabIndex = GetTabIndexByID(tabID);
|
||||
if (tabIndex < 0) return false;
|
||||
|
||||
return _allContentInstances[tabIndex] != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ContentLoader 관련 메서드들 (있는 경우에만 동작)
|
||||
/// </summary>
|
||||
public void UnloadAllExceptCurrent(int currentTabIndex)
|
||||
{
|
||||
if (contentLoader != null && currentTabIndex < _tabConfigs.Count)
|
||||
{
|
||||
string currentTabID = _tabConfigs[currentTabIndex].tabID;
|
||||
contentLoader.UnloadAllExceptCurrent(currentTabID);
|
||||
|
||||
// 언로드된 탭들의 인스턴스 참조도 제거
|
||||
for (int i = 0; i < _allContentInstances.Count; i++)
|
||||
{
|
||||
if (i != currentTabIndex && _tabConfigs[i].useLazyLoading)
|
||||
{
|
||||
_allContentInstances[i] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool UnloadTabContent(string tabID)
|
||||
{
|
||||
if (contentLoader != null)
|
||||
{
|
||||
bool result = contentLoader.UnloadTabContent(tabID);
|
||||
if (result)
|
||||
{
|
||||
// 인스턴스 참조도 제거
|
||||
int tabIndex = GetTabIndexByID(tabID);
|
||||
if (tabIndex >= 0)
|
||||
{
|
||||
_allContentInstances[tabIndex] = null;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void PreloadAllTabs()
|
||||
{
|
||||
if (contentLoader != null)
|
||||
{
|
||||
contentLoader.PreloadAllTabs();
|
||||
|
||||
// 지연 로딩 탭들의 인스턴스를 _allContentInstances에 등록
|
||||
for (int i = 0; i < _tabConfigs.Count; i++)
|
||||
{
|
||||
var config = _tabConfigs[i];
|
||||
if (config.useLazyLoading)
|
||||
{
|
||||
GameObject? instance = contentLoader.GetTabInstance(config.tabID);
|
||||
if (instance != null)
|
||||
{
|
||||
// ContentContainer로 이동
|
||||
if (instance.transform.parent != contentContainer)
|
||||
{
|
||||
instance.transform.SetParent(contentContainer, false);
|
||||
}
|
||||
_allContentInstances[i] = instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 탭과 관련 게임오브젝트를 정리합니다.
|
||||
/// </summary>
|
||||
private void ClearTabs()
|
||||
{
|
||||
// 탭 버튼 제거
|
||||
foreach (var button in _tabButtons)
|
||||
{
|
||||
if (button != null)
|
||||
Destroy(button.gameObject);
|
||||
}
|
||||
_tabButtons.Clear();
|
||||
|
||||
// 컨텐츠 인스턴스 제거
|
||||
foreach (var content in _allContentInstances)
|
||||
{
|
||||
if (content != null)
|
||||
Destroy(content);
|
||||
}
|
||||
_allContentInstances.Clear();
|
||||
|
||||
// 설정 클리어
|
||||
_tabConfigs.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UI/Tab/TabView.cs.meta
Normal file
2
Assets/Scripts/UVC/UI/Tab/TabView.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5346e2ecd480134fa0aac7853ba41bb
|
||||
@@ -19,88 +19,151 @@ namespace UVC.UI
|
||||
/// using UnityEngine;
|
||||
/// using UVC.UI;
|
||||
///
|
||||
/// public class DraggableWindowController : MonoBehaviour
|
||||
/// {
|
||||
/// // UIDragger 컴포넌트가 부착된 드래그 핸들 오브젝트
|
||||
/// public UIDragger draggerHandle;
|
||||
///
|
||||
/// void Start()
|
||||
/// {
|
||||
/// // 드래그 시작 이벤트 구독
|
||||
/// draggerHandle.onBeginDragHandler += () =>
|
||||
/// {
|
||||
/// Debug.Log("창 드래그가 시작되었습니다!");
|
||||
/// };
|
||||
///
|
||||
/// // 드래그 종료 이벤트 구독
|
||||
/// draggerHandle.OnEndDragHandler += pos =>
|
||||
/// {
|
||||
/// Debug.Log($"창 드래그가 종료되었습니다! 최종 위치: {pos}");
|
||||
/// };
|
||||
/// }
|
||||
/// }
|
||||
///public class DraggableWindowController : MonoBehaviour
|
||||
///{
|
||||
/// [SerializeField] private UIDragger draggerHandle;
|
||||
/// [SerializeField] private RectTransform customDragArea;
|
||||
///
|
||||
/// private void Start()
|
||||
/// {
|
||||
/// // 커스텀 드래그 영역 설정
|
||||
/// if (customDragArea != null)
|
||||
/// {
|
||||
/// draggerHandle.SetDragArea(customDragArea);
|
||||
/// }
|
||||
///
|
||||
/// // 이벤트 구독
|
||||
/// draggerHandle.OnBeginDragHandler += OnDragStart;
|
||||
/// draggerHandle.OnDragHandler += OnDragging;
|
||||
/// draggerHandle.OnEndDragHandler += OnDragEnd;
|
||||
///
|
||||
/// // 창을 중앙으로 이동
|
||||
/// draggerHandle.CenterInDragArea();
|
||||
/// }
|
||||
///
|
||||
/// private void OnDragStart()
|
||||
/// {
|
||||
/// Debug.Log("창 드래그 시작!");
|
||||
/// // 드래그 시작 시 추가 로직
|
||||
/// }
|
||||
///
|
||||
/// private void OnDragging(Vector2 position)
|
||||
/// {
|
||||
/// // 드래그 중 실시간 처리
|
||||
/// Debug.Log($"드래그 중: {position}");
|
||||
/// }
|
||||
///
|
||||
/// private void OnDragEnd(Vector2 finalPosition)
|
||||
/// {
|
||||
/// Debug.Log($"드래그 완료! 최종 위치: {finalPosition}");
|
||||
/// // 위치 저장 등의 후처리
|
||||
/// }
|
||||
///
|
||||
/// // 런타임에서 드래그 기능 제어
|
||||
/// public void ToggleDragging()
|
||||
/// {
|
||||
/// draggerHandle.SetDraggingEnabled(!draggerHandle.enabled);
|
||||
/// }
|
||||
///}
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class UIDragger : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
|
||||
{
|
||||
[Header("드래그 설정")]
|
||||
[SerializeField]
|
||||
[Tooltip("드래그 가능한 영역을 지정합니다. 이 영역 내에서 드래그가 가능합니다.")]
|
||||
[Tooltip("드래그 가능한 영역을 지정합니다. null인 경우 Canvas를 자동으로 찾습니다.")]
|
||||
private RectTransform dragArea; // 드래그가 가능한 영역
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("드래그할 UI 요소를 지정합니다. 이 요소가 실제로 드래그됩니다.")]
|
||||
[Tooltip("드래그할 UI 요소를 지정합니다. null인 경우 부모를 자동으로 설정합니다.")]
|
||||
private RectTransform dragObject; // 실제로 드래그될 UI 요소 (예: 창 전체)
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("드래그 시작 시 해당 UI 요소를 맨 앞으로 가져올지 여부를 설정합니다.")]
|
||||
private bool topOnDrag = true; // 드래그 시작 시 맨 앞으로 가져올지 여부
|
||||
private bool bringToFrontOnDrag = true; // 드래그 시작 시 맨 앞으로 가져올지 여부
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("드래그 영역 제한 시 사용할 최소 높이값")]
|
||||
private float yMinHeight = 30;
|
||||
private float yMinHeight = 0;
|
||||
|
||||
private Vector2 originalLocalPointerPosition; // 드래그 시작 시 마우스 포인터의 로컬 위치
|
||||
private Vector3 originalPanelLocalPosition; // 드래그 시작 시 패널의 로컬 위치
|
||||
[SerializeField]
|
||||
[Tooltip("드래그 중 실시간으로 영역 제한을 적용할지 여부")]
|
||||
private bool constrainDuringDrag = true;
|
||||
|
||||
/// <summary>
|
||||
/// 드래그가 끝났을 때 발생하는 이벤트입니다. 최종 위치 정보를 전달합니다.
|
||||
/// </summary>
|
||||
public Action<Vector3> OnEndDragHandler { get; set; }
|
||||
// 이벤트
|
||||
public Action OnBeginDragHandler { get; set; }
|
||||
public Action<Vector2> OnDragHandler { get; set; }
|
||||
public Action<Vector2> OnEndDragHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 드래그가 시작될 때 발생하는 액션입니다.
|
||||
/// </summary>
|
||||
public Action onBeginDragHandler;
|
||||
// 캐시된 변수들
|
||||
private Vector2 originalLocalPointerPosition;
|
||||
private Vector2 originalAnchoredPosition;
|
||||
private int originalSiblingIndex;
|
||||
private Canvas parentCanvas;
|
||||
private Camera canvasCamera;
|
||||
|
||||
|
||||
// 원래의 형제 순서(UI 렌더링 순서)
|
||||
private int baseSibling;
|
||||
// 프로퍼티
|
||||
public RectTransform DragObject => dragObject;
|
||||
public RectTransform DragArea => dragArea;
|
||||
public bool IsDragging { private set; get; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
InitializeComponents();
|
||||
ValidateSetup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 컴포넌트들을 초기화합니다.
|
||||
/// </summary>
|
||||
private void InitializeComponents()
|
||||
{
|
||||
|
||||
// dragObject가 설정되지 않았다면, 부모를 드래그 대상으로 설정
|
||||
if (dragObject == null)
|
||||
{
|
||||
dragObject = transform.parent as RectTransform;
|
||||
if (dragObject == null)
|
||||
{
|
||||
Debug.LogError("<b>[UIDragger]</b> 드래그할 객체(dragObject)를 찾을 수 없습니다. 부모가 RectTransform이 아닙니다.", this);
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// dragArea가 설정되지 않았다면, 최상위 Canvas를 드래그 영역으로 설정
|
||||
if (dragArea == null)
|
||||
// Canvas와 Camera 캐싱
|
||||
parentCanvas = GetComponentInParent<Canvas>();
|
||||
if (parentCanvas != null)
|
||||
{
|
||||
dragArea = GetComponentInParent<Canvas>()?.transform as RectTransform;
|
||||
canvasCamera = parentCanvas.worldCamera;
|
||||
|
||||
// dragArea가 설정되지 않았다면, Canvas를 드래그 영역으로 설정
|
||||
if (dragArea == null)
|
||||
{
|
||||
Debug.Log("<b>[UIDragger]</b> 드래그 영역(dragArea)으로 사용할 Canvas를 찾을 수 없습니다.", this);
|
||||
enabled = false;
|
||||
return;
|
||||
dragArea = parentCanvas.transform as RectTransform;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 설정이 올바른지 검증합니다.
|
||||
/// </summary>
|
||||
private void ValidateSetup()
|
||||
{
|
||||
if (dragObject == null)
|
||||
{
|
||||
Debug.LogError("[UIDragger] dragObject를 찾을 수 없습니다. 부모가 RectTransform이 아닙니다.", this);
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (dragArea == null)
|
||||
{
|
||||
Debug.LogError("[UIDragger] dragArea를 찾을 수 없습니다. Canvas를 찾을 수 없습니다.", this);
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (parentCanvas == null)
|
||||
{
|
||||
Debug.LogWarning("[UIDragger] Canvas를 찾을 수 없습니다. 드래그 기능이 제한될 수 있습니다.", this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그가 허용되는 영역을 설정합니다.
|
||||
/// </summary>
|
||||
@@ -109,43 +172,84 @@ namespace UVC.UI
|
||||
/// <param name="area">드래그 영역의 경계를 정의하는 <see cref="RectTransform"/>입니다. null일 수 없습니다.</param>
|
||||
public void SetDragArea(RectTransform area)
|
||||
{
|
||||
if (area == null)
|
||||
{
|
||||
Debug.LogWarning("[UIDragger] null인 dragArea를 설정하려고 했습니다.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
dragArea = area;
|
||||
enabled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 대상을 동적으로 설정합니다.
|
||||
/// </summary>
|
||||
public void SetDragObject(RectTransform target)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
Debug.LogWarning("[UIDragger] null인 dragObject를 설정하려고 했습니다.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
dragObject = target;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그가 시작될 때 호출됩니다. (IBeginDragHandler)
|
||||
/// </summary>
|
||||
public void OnBeginDrag(PointerEventData data)
|
||||
public void OnBeginDrag(PointerEventData eventData)
|
||||
{
|
||||
if (data.button != PointerEventData.InputButton.Left) return;
|
||||
if (eventData.button != PointerEventData.InputButton.Left) return;
|
||||
if (!IsValidForDrag()) return;
|
||||
|
||||
originalPanelLocalPosition = dragObject.localPosition;
|
||||
baseSibling = dragObject.GetSiblingIndex();
|
||||
IsDragging = true;
|
||||
originalAnchoredPosition = dragObject.anchoredPosition;
|
||||
originalSiblingIndex = dragObject.GetSiblingIndex();
|
||||
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(dragArea, data.position, data.pressEventCamera, out originalLocalPointerPosition);
|
||||
// 마우스 포인터의 로컬 위치 계산
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
dragArea,
|
||||
eventData.position,
|
||||
canvasCamera,
|
||||
out originalLocalPointerPosition);
|
||||
|
||||
if (topOnDrag)
|
||||
// 맨 앞으로 가져오기
|
||||
if (bringToFrontOnDrag)
|
||||
{
|
||||
dragObject.SetAsLastSibling();
|
||||
}
|
||||
onBeginDragHandler?.Invoke();
|
||||
|
||||
OnBeginDragHandler?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 중일 때 매 프레임 호출됩니다. (IDragHandler)
|
||||
/// </summary>
|
||||
public void OnDrag(PointerEventData data)
|
||||
public void OnDrag(PointerEventData eventData)
|
||||
{
|
||||
if (data.button != PointerEventData.InputButton.Left) return;
|
||||
if (eventData.button != PointerEventData.InputButton.Left) return;
|
||||
if (!IsDragging || !IsValidForDrag()) return;
|
||||
|
||||
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(dragArea, data.position, data.pressEventCamera, out Vector2 localPointerPosition))
|
||||
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
dragArea,
|
||||
eventData.position,
|
||||
canvasCamera,
|
||||
out Vector2 localPointerPosition))
|
||||
{
|
||||
Vector3 offsetToOriginal = localPointerPosition - originalLocalPointerPosition;
|
||||
dragObject.localPosition = originalPanelLocalPosition + offsetToOriginal;
|
||||
}
|
||||
Vector2 offsetToOriginal = localPointerPosition - originalLocalPointerPosition;
|
||||
Vector2 newPosition = originalAnchoredPosition + offsetToOriginal;
|
||||
|
||||
ClampToArea();
|
||||
// 실시간 제약 적용
|
||||
if (constrainDuringDrag)
|
||||
{
|
||||
newPosition = ClampToArea(newPosition);
|
||||
}
|
||||
|
||||
dragObject.anchoredPosition = newPosition;
|
||||
OnDragHandler?.Invoke(newPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -154,34 +258,100 @@ namespace UVC.UI
|
||||
public void OnEndDrag(PointerEventData eventData)
|
||||
{
|
||||
if (eventData.button != PointerEventData.InputButton.Left) return;
|
||||
if (!IsDragging) return;
|
||||
|
||||
if (topOnDrag)
|
||||
IsDragging = false;
|
||||
|
||||
// 원래 형제 순서로 복원
|
||||
if (bringToFrontOnDrag)
|
||||
{
|
||||
dragObject.SetSiblingIndex(baseSibling);
|
||||
dragObject.SetSiblingIndex(originalSiblingIndex);
|
||||
}
|
||||
OnEndDragHandler?.Invoke(dragObject.anchoredPosition);
|
||||
|
||||
// 최종 위치 제약 적용
|
||||
Vector2 finalPosition = ClampToArea(dragObject.anchoredPosition);
|
||||
dragObject.anchoredPosition = finalPosition;
|
||||
|
||||
OnEndDragHandler?.Invoke(finalPosition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그가 가능한 상태인지 확인합니다.
|
||||
/// </summary>
|
||||
private bool IsValidForDrag()
|
||||
{
|
||||
return dragObject != null && dragArea != null && enabled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UI 요소가 드래그 영역 내에 있도록 위치를 제한합니다.
|
||||
/// </summary>
|
||||
private void ClampToArea()
|
||||
private Vector2 ClampToArea(Vector2 position)
|
||||
{
|
||||
Vector3 pos = dragObject.localPosition;
|
||||
if (dragArea == null || dragObject == null)
|
||||
return position;
|
||||
|
||||
Rect dragObjectRect = dragObject.rect;
|
||||
Rect dragAreaRect = dragArea.rect;
|
||||
|
||||
// Pivot을 기준으로 최소/최대 위치를 계산합니다.
|
||||
float minX = dragAreaRect.xMin - dragObjectRect.xMin;
|
||||
float maxX = dragAreaRect.xMax - dragObjectRect.xMax;
|
||||
float minY = dragAreaRect.yMin - dragObjectRect.yMin;
|
||||
float maxY = dragAreaRect.yMax - dragObjectRect.yMax;
|
||||
// Pivot과 앵커를 고려한 경계 계산
|
||||
Vector2 pivot = dragObject.pivot;
|
||||
Vector2 size = dragObjectRect.size;
|
||||
|
||||
pos.x = Mathf.Clamp(pos.x, minX, maxX);
|
||||
pos.y = Mathf.Clamp(pos.y, minY, maxY - yMinHeight);
|
||||
float leftBoundary = dragAreaRect.xMin + (size.x * pivot.x);
|
||||
float rightBoundary = dragAreaRect.xMax - (size.x * (1f - pivot.x));
|
||||
float bottomBoundary = dragAreaRect.yMin + (size.y * pivot.y) + yMinHeight;
|
||||
float topBoundary = dragAreaRect.yMax - (size.y * (1f - pivot.y));
|
||||
|
||||
dragObject.localPosition = pos;
|
||||
position.x = Mathf.Clamp(position.x, leftBoundary, rightBoundary);
|
||||
position.y = Mathf.Clamp(position.y, bottomBoundary, topBoundary);
|
||||
|
||||
return position;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 객체를 특정 위치로 이동시킵니다.
|
||||
/// </summary>
|
||||
public void SetPosition(Vector2 position, bool clampToArea = true)
|
||||
{
|
||||
if (dragObject == null) return;
|
||||
|
||||
if (clampToArea)
|
||||
{
|
||||
position = ClampToArea(position);
|
||||
}
|
||||
|
||||
dragObject.anchoredPosition = position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 객체를 중앙으로 이동시킵니다.
|
||||
/// </summary>
|
||||
public void CenterInDragArea()
|
||||
{
|
||||
if (dragArea == null || dragObject == null) return;
|
||||
|
||||
Vector2 centerPosition = dragArea.rect.center;
|
||||
SetPosition(centerPosition, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 기능을 활성화/비활성화합니다.
|
||||
/// </summary>
|
||||
public void SetDraggingEnabled(bool enabled)
|
||||
{
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate()
|
||||
{
|
||||
// 에디터에서 값 변경 시 유효성 검사
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
ValidateSetup();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user