배경 색상, Volume 조절

This commit is contained in:
logonkhi
2025-12-23 18:23:44 +09:00
parent 192d6963c0
commit 54d8157203
5 changed files with 309 additions and 106 deletions

View File

@@ -6,6 +6,7 @@ using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UVC.Pool;
using UVC.UI.Modal.DatePicker;
namespace UVC.UI.List.Tree
@@ -114,6 +115,26 @@ namespace UVC.UI.List.Tree
/// </summary>
public TreeListDragDropManager DragDropManager => dragDropManager;
#region (Object Pooling)
/// <summary>
/// TreeListItem 오브젝트 풀
/// </summary>
protected GameObjectPool<TreeListItem>? _itemPool;
/// <summary>
/// 재활용 컨테이너 (비활성 아이템 보관용)
/// </summary>
[SerializeField]
protected Transform? recycledContainer;
/// <summary>
/// 오브젝트 풀 읽기 전용 접근자
/// </summary>
public GameObjectPool<TreeListItem>? ItemPool => _itemPool;
#endregion
/// <summary>
/// 아이템 선택 상태 변경 이벤트 (data, isSelected)
/// TreeListItem에서 호출됩니다.
@@ -166,6 +187,73 @@ namespace UVC.UI.List.Tree
{
dragDropManager.OnDropped += HandleItemDropped;
}
// 오브젝트 풀 초기화
InitializePool();
}
/// <summary>
/// 오브젝트 풀 초기화
/// </summary>
protected virtual void InitializePool()
{
if (itemPrefab == null) return;
// recycledContainer가 없으면 동적 생성
if (recycledContainer == null)
{
var recycledGO = new GameObject("_RecycledItems");
recycledGO.transform.SetParent(transform, false);
recycledGO.SetActive(false); // 비활성 상태로 유지
recycledContainer = recycledGO.transform;
}
_itemPool = new GameObjectPool<TreeListItem>(
itemPrefab.gameObject,
root,
recycledContainer
);
}
/// <summary>
/// 풀에서 TreeListItem 획득
/// </summary>
/// <param name="key">아이템 고유 키 (TreeListItemData.Id.ToString())</param>
/// <param name="parent">부모 Transform (null이면 root 사용)</param>
/// <returns>풀에서 획득한 TreeListItem</returns>
public TreeListItem? GetPooledItem(string key, Transform? parent = null)
{
if (_itemPool == null)
{
InitializePool();
}
var item = _itemPool!.GetItem(key, true, parent ?? root);
return item;
}
/// <summary>
/// TreeListItem을 풀에 반환
/// </summary>
/// <param name="item">반환할 TreeListItem</param>
/// <param name="key">아이템 고유 키</param>
public void ReturnItemToPool(TreeListItem item, string key)
{
if (_itemPool == null || item == null) return;
_itemPool.ReturnItem(key, true);
}
/// <summary>
/// 모든 아이템을 풀에 반환
/// </summary>
public void ReturnAllToPool()
{
_itemPool?.ReturnAll();
allItemFlattened.Clear();
allItemDataFlattened.Clear();
selectedItems.Clear();
_viewMap.Clear();
}
protected void OnDestroy()
@@ -187,6 +275,9 @@ namespace UVC.UI.List.Tree
allItemDataFlattened.Clear();
selectedItems.Clear();
items.Clear();
// 풀 정리
_itemPool?.ClearRecycledItems();
}
#region (Data Modification Methods)
@@ -909,9 +1000,9 @@ namespace UVC.UI.List.Tree
/// <summary>
/// 아이템 삭제
///
///
/// 동작:
/// 1. 데이터 리스트에서 제거
/// 1. 데이터 리스트에서 제거 (루트 또는 부모의 Children)
/// 2. 선택 리스트에서 제거
/// 3. UI 컴포넌트 삭제
/// 4. 데이터 메모리 해제
@@ -919,7 +1010,15 @@ namespace UVC.UI.List.Tree
/// <param name="data">삭제할 아이템 데이터</param>
public void DeleteItem(TreeListItemData data)
{
items.Remove(data);
// 루트 레벨이면 items에서 제거, 자식이면 부모의 Children에서 제거
if (data.Parent != null)
{
data.Parent.RemoveChild(data);
}
else
{
items.Remove(data);
}
// selectedItems에서도 제거
if (selectedItems.Contains(data))
@@ -927,10 +1026,19 @@ namespace UVC.UI.List.Tree
selectedItems.Remove(data);
}
TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data);
if (item != null)
// UI 삭제 - _viewMap에서 먼저 찾기 (풀링 적용 후 더 정확함)
if (_viewMap.TryGetValue(data, out var viewItem) && viewItem != null)
{
item.Delete(true);
viewItem.Delete(true);
}
else
{
// fallback: allItemFlattened에서 찾기
TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data);
if (item != null)
{
item.Delete(true);
}
}
}

View File

@@ -149,16 +149,23 @@ namespace UVC.UI.List.Tree
childRootLayoutGroup = childRoot.GetComponent<VerticalLayoutGroup>();
}
if (data.Children.Count > 0)
// 성능 최적화: 펼쳐진 상태에서만 자식 UI 생성 (지연 생성)
if (data.IsExpanded && data.Children.Count > 0)
{
// 각 자식 데이터에 대해 UI 생성
foreach (var childData in data.Children)
{
CreateItem(childData); // 재귀적으로 트리 구조 생성
}
childRoot.gameObject.SetActive(true);
}
// 화살표 방향 설정 (초기에는 펼쳐짐)
else
{
// 접힌 상태면 자식 컨테이너 비활성화
childRoot.gameObject.SetActive(false);
}
// 화살표 방향 설정
SetExpand();
// 4. 버튼 클릭 이벤트 구독
@@ -170,7 +177,8 @@ namespace UVC.UI.List.Tree
if(visibleToggle != null)
{
visibleToggle.isOn = data.IsVisible;
// 풀에서 재사용 시 동일 값이어도 시각 업데이트를 위해 강제 갱신
visibleToggle.ForceSetIsOnWithoutNotify(data.IsVisible);
visibleToggle.OnValueChanged.AddListener(OnItemVisibilityChanged);
}
@@ -217,7 +225,18 @@ namespace UVC.UI.List.Tree
if (changedType == ChangedType.Expanded)
{
childRoot.gameObject.SetActive(data.IsExpanded);
if (data.IsExpanded)
{
// 펼침 시: 자식 UI가 없으면 생성 (지연 생성)
EnsureChildrenCreated();
childRoot.gameObject.SetActive(true);
}
else
{
// 접힘 시: 자식 UI를 풀에 반환 (메모리 절약)
ReturnChildrenToPool();
childRoot.gameObject.SetActive(false);
}
// 펼침/접힘 상태 변경 처리
SetExpand();
return;
@@ -504,6 +523,8 @@ namespace UVC.UI.List.Tree
protected void OnItemVisibilityChanged(bool isVisible)
{
if (data == null) return;
// 데이터에 가시성 상태 저장 (풀에서 재사용 시 복원을 위해)
data.IsVisible = isVisible;
treeList.OnItemVisibilityChanged?.Invoke(data, isVisible);
}
@@ -555,6 +576,26 @@ namespace UVC.UI.List.Tree
#region (Child Management)
/// <summary>
/// 자식 UI가 필요할 때만 생성 (Lazy Instantiation)
/// </summary>
protected void EnsureChildrenCreated()
{
if (data == null || treeList == null) return;
// 데이터에 있지만 UI가 없는 자식들 생성
foreach (var childData in data.Children)
{
// 이미 뷰가 있는지 확인
if (treeList.ItemPool?.ActiveItems.ContainsKey(childData.Id.ToString()) == true)
{
continue;
}
CreateItem(childData);
}
}
/// <summary>
/// 새로운 자식 아이템을 추가하고 UI를 갱신합니다.
/// </summary>
@@ -573,31 +614,35 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 자식 데이터를 받아 UI TreeListItem을 생성합니다.
/// 자식 데이터를 받아 UI TreeListItem을 생성합니다. (풀에서 획득)
/// </summary>
/// <param name="data">생성할 아이템 데이터</param>
/// <returns>생성된 TreeListItem</returns>
protected TreeListItem CreateItem(TreeListItemData data)
{
// 1. 프리팹을 복제해서 새로운 TreeListItem 생성
// Instantiate<T>(원본, 부모, 옵션)
// treeList.ItemPrefab: UI 아이템 템플릿
// childRoot: 새 아이템의 부모 Transform
TreeListItem item = GameObject.Instantiate<TreeListItem>(
treeList.ItemPrefab, // 복제할 프리팹
childRoot // 부모로 배치할 위치
);
//item.layout의 너비를 childRootLayoutGroup의 padding.left 만큼 줄이기
// 1. 풀에서 아이템 획득 (ID를 키로 사용)
string key = data.Id.ToString();
TreeListItem? item = treeList.GetPooledItem(key, childRoot);
if (item == null)
{
// 풀에서 가져올 수 없으면 기존 방식으로 생성 (fallback)
item = GameObject.Instantiate<TreeListItem>(
treeList.ItemPrefab,
childRoot
);
}
// 2. layout의 너비를 childRootLayoutGroup의 padding.left 만큼 줄이기
if (item != null && item.layout != null && childRootLayoutGroup != null)
{
item.layout.sizeDelta = new Vector2(layout.sizeDelta.x - childRootLayoutGroup.padding.left, item.layout.sizeDelta.y);
}
// 2. 생성된 아이템 초기화
item.Init(data, treeList, treeList.DragDropManager);
// 3. 생성된 아이템 초기화
item!.Init(data, treeList, treeList.DragDropManager);
// 3. 생성된 아이템 반환
// 4. 생성된 아이템 반환
return item;
}
@@ -606,10 +651,116 @@ namespace UVC.UI.List.Tree
#region (Deletion)
/// <summary>
/// 이벤트 구독 해제, 애니메이션 중지 후 GameObject를 삭제합니다.
/// 이벤트 구독 해제, 애니메이션 중지 후 GameObject를 풀에 반환합니다.
/// </summary>
/// <param name="deleteData">데이터도 삭제할지 여부</param>
public void Delete(bool deleteData = false)
{
if (data == null || treeList == null)
{
// 데이터나 treeList가 없으면 기존 방식으로 삭제
CleanupAndDestroy();
return;
}
string key = data.Id.ToString();
// 자식들 먼저 풀에 반환
ReturnChildrenToPool();
// 데이터 정리 (선택적)
if (deleteData)
{
data.Dispose();
}
// 풀에 등록된 아이템인지 확인
bool isPooledItem = treeList.ItemPool?.ActiveItems.ContainsKey(key) == true;
if (isPooledItem)
{
// 풀에 반환
ResetForPool();
treeList.ReturnItemToPool(this, key);
}
else
{
// 풀에 없는 아이템 (루트 레벨 등)은 직접 삭제
CleanupAndDestroy();
}
}
/// <summary>
/// 풀에서 재사용 시 초기화를 위한 메서드
/// </summary>
public virtual void ResetForPool()
{
// 이벤트 리스너 해제
if (itemButton != null)
{
itemButton.onClick.RemoveListener(OnItemClicked);
}
if (visibleToggle != null)
{
visibleToggle.OnValueChanged.RemoveAllListeners();
}
// 뷰 맵에서 해제
if (data != null && treeList != null)
{
treeList.UnregisterView(data, this);
}
// 데이터 참조만 해제 (Dispose 안 함 - 데이터는 외부에서 관리)
data = null;
// 애니메이션 정리
if (childExpand != null)
{
childExpand.transform.DOKill();
}
// 상태 초기화
isAnimating = false;
_lastClickTime = 0f;
// 선택 배경 숨기기
if (selectedBg != null)
{
selectedBg.gameObject.SetActive(false);
}
}
/// <summary>
/// 자식 TreeListItem들을 풀에 반환 (재귀)
/// </summary>
protected void ReturnChildrenToPool()
{
if (childRoot == null || treeList == null) return;
for (int i = childRoot.childCount - 1; i >= 0; i--)
{
var childTransform = childRoot.GetChild(i);
var childItem = childTransform.GetComponent<TreeListItem>();
if (childItem != null && childItem.data != null)
{
string childKey = childItem.data.Id.ToString();
// 재귀적으로 자식의 자식도 반환
childItem.ReturnChildrenToPool();
// 풀에 반환
childItem.ResetForPool();
treeList.ReturnItemToPool(childItem, childKey);
}
}
}
/// <summary>
/// 정리 후 GameObject 삭제 (풀 미사용 시 fallback)
/// </summary>
protected void CleanupAndDestroy()
{
// 맵에서 해제
if (data != null && treeList != null)
@@ -617,14 +768,14 @@ namespace UVC.UI.List.Tree
treeList.UnregisterView(data, this);
}
// 1. 데이터 정리
// 데이터 정리
if (data != null)
{
data.Dispose();
data = null;
}
// 2. 버튼 클릭 이벤트 구독 해제
// 버튼 클릭 이벤트 구독 해제
if (itemButton != null)
{
itemButton.onClick.RemoveListener(OnItemClicked);
@@ -637,13 +788,11 @@ namespace UVC.UI.List.Tree
if (childExpand != null)
{
childExpand.transform.DOKill(); // 진행 중인 회전 애니메이션 중단
childExpand.transform.DOKill();
}
// 3. 참조 정리
treeList = null!;
// 4. 이 GameObject 삭제
GameObject.Destroy(gameObject);
}