Files
XRLib/Assets/Scripts/UVC/UI/List/DraggableScrollList.cs

1076 lines
38 KiB
C#
Raw Normal View History

2025-07-30 20:16:21 +09:00
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Unity.VisualScripting;
using UnityEditorInternal.VersionControl;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace UVC.UI.List
{
/// <summary>
/// 드래그 가능한 ScrollRect 목록을 관리하는 메인 컨트롤러
/// Model-View 패턴을 적용하여 데이터와 UI를 분리
/// </summary>
/// <example>
/// <b>사용 예제:</b>
/// 1. 이벤트 구독
/// 2. DraggableItemData 설정
/// <code>
/// public class DraggableScrollListSetup : MonoBehaviour
/// {
/// [SerializeField]
/// private DraggableScrollList? draggableList;
///
/// protected virtual void Awake()
/// {
/// if (draggableList == null)
/// {
/// Debug.LogError("draggableList 참조가 설정되지 않았습니다.");
/// return;
/// }
///
/// // 이벤트 구독
/// draggableList.OnItemReordered += OnItemReordered;
/// draggableList.OnItemSelected += OnItemSelected;
/// }
///
/// void Start()
/// {
/// // 1. DraggableItemData 설정
/// draggableList?.AddItem(new DraggableItemData("AGV", 0));
/// draggableList?.AddItem(new DraggableItemData("ALARM", 1));
/// }
///
/// /// <summary>
/// /// 아이템 순서 변경 이벤트 처리
/// /// </summary>
/// /// <param name="sender">이벤트 발생자</param>
/// /// <param name="e">이벤트 인자</param>
/// private void OnItemReordered(object? sender, DraggableItemReorderEventArgs e)
/// {
/// Debug.Log($"아이템 순서 변경됨: ID={e.ItemId}, {e.OldIndex} -> {e.NewIndex}");
///
/// // 여기에 순서 변경에 대한 비즈니스 로직 구현
/// // 예: 서버에 변경사항 전송, 설정 저장 등
/// }
///
/// /// <summary>
/// /// 아이템 선택 이벤트 처리
/// /// </summary>
/// /// <param name="sender">이벤트 발생자</param>
/// /// <param name="item">선택된 아이템</param>
/// private void OnItemSelected(object? sender, DraggableListItem item)
/// {
/// if (item?.Data != null)
/// {
/// Debug.Log($"아이템 선택됨: {item.Data.Id}");
///
/// // 선택된 아이템에 대한 처리
/// // 예: 상세 정보 표시, 편집 모드 진입 등
/// }
/// }
///
/// /// <summary>
/// /// 컴포넌트 정리
/// /// </summary>
/// private void OnDestroy()
/// {
/// if (draggableList != null)
/// {
/// draggableList.OnItemReordered -= OnItemReordered;
/// draggableList.OnItemSelected -= OnItemSelected;
/// }
/// }
/// }
/// </code>
/// </example>
public class DraggableScrollList : MonoBehaviour
{
[Header("UI 참조")]
[SerializeField] private ScrollRect? scrollRect;
[SerializeField] private RectTransform? contentParent;
[SerializeField] private VerticalLayoutGroup? layoutGroup;
[Header("드롭 인디케이터")]
[SerializeField] private Sprite? dropLineSprite;
[SerializeField] private Color dropLineColor = Color.cyan;
[SerializeField] private float dropLineHeight = 3f;
[SerializeField] private float dropLineMargin = 10f;
[SerializeField] private Material? dropLineMaterial; // 선택적: 특별한 Material 사용 시
[Header("프리팹 설정")]
[SerializeField] private string itemPrefabPath = "Prefabs/UI/List/DraggableListItem";
[Header("드래그 설정")]
[SerializeField] private float dropZoneThreshold = 50f;
[SerializeField] private float scrollSensitivity = 100f;
[SerializeField] private bool enableAutoScroll = true;
// 이벤트
public event EventHandler<DraggableItemReorderEventArgs>? OnItemReordered;
public event EventHandler<DraggableListItem>? OnItemSelected;
// 데이터 및 UI 관리
private List<DraggableItemData> itemDataList = new List<DraggableItemData>();
private List<DraggableListItem> itemUIList = new List<DraggableListItem>();
private GameObject? itemPrefab;
// 드래그 상태 관리
private DraggableListItem? currentDraggingItem;
private int dragStartIndex = -1;
private int currentDropIndex = -1;
private Camera? uiCamera;
// 드롭 라인 관리 (동적 생성)
private GameObject? dropLineObject;
private Image? dropLineImage;
private RectTransform? dropLineRectTransform;
private bool isDropLineVisible = false;
/// <summary>
/// 현재 아이템 데이터 목록 (읽기 전용)
/// </summary>
public IReadOnlyList<DraggableItemData> ItemDataList => itemDataList.AsReadOnly();
/// <summary>
/// 컴포넌트 초기화
/// </summary>
private void Awake()
{
InitializeComponents();
LoadItemPrefab();
}
/// <summary>
/// UI 카메라 참조 설정
/// </summary>
private void Start()
{
// UI 카메라 찾기 (Canvas의 카메라 또는 메인 카메라)
Canvas? canvas = GetComponentInParent<Canvas>();
uiCamera = canvas?.worldCamera ?? Camera.main;
// 드롭 라인 생성 (Start에서 호출하여 모든 컴포넌트가 초기화된 후 실행)
CreateDropLine();
}
/// <summary>
/// 필수 컴포넌트들 초기화 및 검증
/// </summary>
private void InitializeComponents()
{
// ScrollRect 자동 할당
if (scrollRect == null)
scrollRect = GetComponent<ScrollRect>();
// Content 부모 자동 할당
if (contentParent == null && scrollRect?.content != null)
contentParent = scrollRect.content;
// LayoutGroup 자동 할당
if (layoutGroup == null && contentParent != null)
layoutGroup = contentParent.GetComponent<VerticalLayoutGroup>();
// 필수 컴포넌트 검증
if (scrollRect == null)
Debug.LogError($"[{nameof(DraggableScrollList)}] ScrollRect 컴포넌트를 찾을 수 없습니다!");
if (contentParent == null)
Debug.LogError($"[{nameof(DraggableScrollList)}] Content 부모 Transform을 찾을 수 없습니다!");
}
/// <summary>
/// 드롭 라인을 동적으로 생성
/// </summary>
private void CreateDropLine()
{
if (contentParent == null)
{
Debug.LogError($"[{nameof(DraggableScrollList)}] Content 부모가 없어 드롭 라인을 생성할 수 없습니다!");
return;
}
try
{
// 드롭 라인 GameObject 생성
dropLineObject = new GameObject("DropLineIndicator");
// Content의 자식으로 설정
dropLineObject.transform.SetParent(contentParent, false);
// Image 컴포넌트 추가
dropLineImage = dropLineObject.AddComponent<Image>();
// RectTransform 참조 가져오기
dropLineRectTransform = dropLineObject.GetComponent<RectTransform>();
// 드롭 라인 설정 적용
ConfigureDropLine();
// 초기에는 비활성화
if (dropLineObject != null) dropLineObject.SetActive(false);
Debug.Log($"[{nameof(DraggableScrollList)}] 드롭 라인이 동적으로 생성되었습니다.");
}
catch (Exception ex)
{
Debug.LogError($"[{nameof(DraggableScrollList)}] 드롭 라인 생성 중 오류 발생: {ex.Message}");
}
}
/// <summary>
/// 드롭 라인의 시각적 속성 설정
/// </summary>
private void ConfigureDropLine()
{
if (dropLineImage == null || dropLineRectTransform == null) return;
// Sprite 설정
if (dropLineSprite != null)
{
dropLineImage.sprite = dropLineSprite;
dropLineImage.type = Image.Type.Sliced; // 스프라이트 늘어남 방지
}
else
{
// 기본 스프라이트 생성 (단색 사각형)
dropLineImage.sprite = CreateDefaultDropLineSprite();
dropLineImage.type = Image.Type.Simple;
}
// 색상 설정
dropLineImage.color = dropLineColor;
// Material 설정 (선택적)
if (dropLineMaterial != null)
{
dropLineImage.material = dropLineMaterial;
}
// RectTransform 설정
SetupDropLineRectTransform();
}
/// <summary>
/// 드롭 라인의 RectTransform 설정
/// </summary>
private void SetupDropLineRectTransform()
{
if (dropLineRectTransform == null) return;
// 앵커와 피벗 설정 (가로 전체를 차지하도록)
dropLineRectTransform.anchorMin = new Vector2(0f, 0.5f);
dropLineRectTransform.anchorMax = new Vector2(1f, 0.5f);
dropLineRectTransform.pivot = new Vector2(0.5f, 0.5f);
// 크기 설정 (가로는 부모에 맞춤, 세로는 설정값 사용)
dropLineRectTransform.sizeDelta = new Vector2(0f, dropLineHeight);
// 위치 초기화
dropLineRectTransform.anchoredPosition = Vector2.zero;
// 레이캐스트 차단 방지 (드래그 중 방해하지 않도록)
dropLineImage!.raycastTarget = false;
}
/// <summary>
/// 기본 드롭 라인 스프라이트 생성 (Sprite가 없을 경우)
/// </summary>
/// <returns>생성된 기본 스프라이트</returns>
private Sprite CreateDefaultDropLineSprite()
{
// 1x1 픽셀 텍스처 생성
Texture2D texture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
texture.SetPixel(0, 0, Color.white);
texture.Apply();
// 스프라이트 생성
Sprite defaultSprite = Sprite.Create(
texture,
new Rect(0, 0, 1, 1),
new Vector2(0.5f, 0.5f),
100f // pixelsPerUnit
);
defaultSprite.name = "DefaultDropLineSprite";
Debug.Log($"[{nameof(DraggableScrollList)}] 기본 드롭 라인 스프라이트가 생성되었습니다.");
return defaultSprite;
}
/// <summary>
/// 아이템 프리팹 로드
/// </summary>
private void LoadItemPrefab()
{
try
{
itemPrefab = Resources.Load<GameObject>(itemPrefabPath);
if (itemPrefab == null)
{
Debug.LogError($"[{nameof(DraggableScrollList)}] 프리팹을 로드할 수 없습니다: {itemPrefabPath}");
}
}
catch (Exception ex)
{
Debug.LogError($"[{nameof(DraggableScrollList)}] 프리팹 로드 중 오류 발생: {ex.Message}");
}
}
/// <summary>
/// 목록에 새 아이템 추가
/// </summary>
/// <param name="data">추가할 아이템 데이터</param>
public void AddItem(DraggableItemData data)
{
if (data == null)
{
Debug.LogWarning($"[{nameof(DraggableScrollList)}] null 데이터는 추가할 수 없습니다.");
return;
}
// 데이터 추가
data.SortOrder = itemDataList.Count;
itemDataList.Add(data);
// UI 생성
CreateItemUI(data);
}
/// <summary>
/// 여러 아이템을 한번에 추가 (성능 최적화)
/// </summary>
/// <param name="dataList">추가할 아이템 데이터 목록</param>
public void AddItems(IEnumerable<DraggableItemData> dataList)
{
if (dataList == null) return;
var dataArray = dataList.ToArray();
if (dataArray.Length == 0) return;
// 레이아웃 업데이트 일시 중지 (성능 최적화)
if (layoutGroup != null)
layoutGroup.enabled = false;
try
{
foreach (var data in dataArray)
{
if (data != null)
{
data.SortOrder = itemDataList.Count;
itemDataList.Add(data);
CreateItemUI(data);
}
}
}
finally
{
// 레이아웃 업데이트 재개
if (layoutGroup != null)
{
layoutGroup.enabled = true;
LayoutRebuilder.ForceRebuildLayoutImmediate(contentParent);
}
}
}
/// <summary>
/// 특정 아이템 제거
/// </summary>
/// <param name="itemId">제거할 아이템의 ID</param>
/// <returns>제거 성공 여부</returns>
public bool RemoveItem(string itemId)
{
if (string.IsNullOrEmpty(itemId)) return false;
// 데이터에서 제거
var dataIndex = itemDataList.FindIndex(d => d.Id == itemId);
if (dataIndex == -1) return false;
itemDataList.RemoveAt(dataIndex);
// UI에서 제거
var uiIndex = itemUIList.FindIndex(ui => ui.Data?.Id == itemId);
if (uiIndex >= 0)
{
var itemUI = itemUIList[uiIndex];
itemUIList.RemoveAt(uiIndex);
// 이벤트 해제
UnsubscribeFromItemEvents(itemUI);
// GameObject 파괴
if (itemUI != null)
Destroy(itemUI.gameObject);
}
// 순서 재정렬
UpdateSortOrders();
return true;
}
/// <summary>
/// 모든 아이템 제거
/// </summary>
public void ClearItems()
{
// UI 제거
foreach (var itemUI in itemUIList)
{
if (itemUI != null)
{
UnsubscribeFromItemEvents(itemUI);
Destroy(itemUI.gameObject);
}
}
// 목록 초기화
itemDataList.Clear();
itemUIList.Clear();
currentDraggingItem = null;
dragStartIndex = -1;
}
/// <summary>
/// 아이템 UI 생성
/// </summary>
/// <param name="data">아이템 데이터</param>
private void CreateItemUI(DraggableItemData data)
{
Debug.Log($"[{nameof(DraggableScrollList)}]1 itemPrefab == null:{itemPrefab == null}, contentParent == null:{contentParent == null}");
if (itemPrefab == null || contentParent == null) return;
Debug.Log($"[{nameof(DraggableScrollList)}]2 아이템 UI 생성: {data.Id}");
try
{
// 프리팹 인스턴스 생성
GameObject itemObject = Instantiate(itemPrefab, contentParent);
// DraggableListItem 컴포넌트 가져오기
DraggableListItem? itemUI = itemObject.GetComponentInChildren<DraggableListItem>();
if (itemUI == null)
{
Debug.LogError($"[{nameof(DraggableScrollList)}] 프리팹에 DraggableListItem 컴포넌트가 없습니다!");
Destroy(itemObject);
return;
}
// 데이터 설정
itemUI.SetData(data);
// 이벤트 연결
SubscribeToItemEvents(itemUI);
// UI 목록에 추가
itemUIList.Add(itemUI);
}
catch (Exception ex)
{
Debug.LogError($"[{nameof(DraggableScrollList)}] 아이템 UI 생성 중 오류: {ex.Message}");
}
}
/// <summary>
/// 아이템 이벤트 구독
/// </summary>
/// <param name="item">구독할 아이템</param>
private void SubscribeToItemEvents(DraggableListItem item)
{
if (item == null) return;
item.OnBeginDragEvent += OnItemBeginDrag;
item.OnDragEvent += OnItemDrag;
item.OnEndDragEvent += OnItemEndDrag;
}
/// <summary>
/// 아이템 이벤트 구독 해제
/// </summary>
/// <param name="item">구독 해제할 아이템</param>
private void UnsubscribeFromItemEvents(DraggableListItem item)
{
if (item == null) return;
item.OnBeginDragEvent -= OnItemBeginDrag;
item.OnDragEvent -= OnItemDrag;
item.OnEndDragEvent -= OnItemEndDrag;
}
/// <summary>
/// 아이템 드래그 시작 이벤트 처리
/// </summary>
/// <param name="item">드래그 시작된 아이템</param>
private void OnItemBeginDrag(DraggableListItem item)
{
if (item?.Data == null) return;
currentDraggingItem = item;
dragStartIndex = itemUIList.IndexOf(item);
// ScrollRect 드래그 비활성화 (아이템 드래그와 충돌 방지)
if (scrollRect != null)
scrollRect.enabled = false;
if (layoutGroup != null) layoutGroup.enabled = false;
// 드래그 중인 아이템을 최상위로 이동 (다른 아이템 위에 표시)
item.transform.SetAsLastSibling();
// 드롭 라인을 최상위로 이동 (드래그 아이템 다음)
if (dropLineObject != null)
dropLineObject.transform.SetAsLastSibling();
Debug.Log($"드래그 시작: {item.Data.Id} (인덱스: {dragStartIndex})");
}
/// <summary>
/// 아이템 드래그 중 이벤트 처리
/// </summary>
/// <param name="item">드래그 중인 아이템</param>
/// <param name="itemAnchoredPosition">item의 anchoredPosition</param>
private void OnItemDrag(DraggableListItem item, Vector2 itemAnchoredPosition)
{
if (item != currentDraggingItem || uiCamera == null) return;
// 자동 스크롤 처리
if (enableAutoScroll)
HandleAutoScroll(itemAnchoredPosition);
// 드롭 위치 계산 및 시각적 피드백
UpdateDropIndicator(itemAnchoredPosition);
}
/// <summary>
/// 아이템 드래그 종료 이벤트 처리
/// </summary>
/// <param name="item">드래그 종료된 아이템</param>
private void OnItemEndDrag(DraggableListItem item)
{
if (item != currentDraggingItem) return;
try
{
// 드롭 위치 계산
int dropIndex = CalculateDropIndex();
// 순서 변경 처리
if (dropIndex != dragStartIndex && dropIndex >= 0)
{
ReorderItem(dragStartIndex, dropIndex);
}
else
{
// 순서 변경이 없으면 원래 위치로 복원
item.ResetToOriginalPosition();
}
}
finally
{
// 드롭 라인 숨기기
HideDropLine();
// 상태 초기화
currentDraggingItem = null;
dragStartIndex = -1;
// ScrollRect 다시 활성화
if (scrollRect != null)
scrollRect.enabled = true;
if (layoutGroup != null) layoutGroup.enabled = true;
// 레이아웃 강제 업데이트
if (layoutGroup != null)
LayoutRebuilder.ForceRebuildLayoutImmediate(contentParent);
}
}
/// <summary>
/// 아이템 순서 변경
/// </summary>
/// <param name="fromIndex">원래 위치</param>
/// <param name="toIndex">새로운 위치</param>
private void ReorderItem(int fromIndex, int toIndex)
{
if (fromIndex == toIndex || fromIndex < 0 || toIndex < 0 ||
fromIndex >= itemUIList.Count || toIndex >= itemUIList.Count)
return;
try
{
// UI 목록에서 순서 변경
var itemUI = itemUIList[fromIndex];
itemUIList.RemoveAt(fromIndex);
itemUIList.Insert(toIndex, itemUI);
// 데이터 목록에서 순서 변경
var itemData = itemDataList[fromIndex];
itemDataList.RemoveAt(fromIndex);
itemDataList.Insert(toIndex, itemData);
// Transform 계층 구조에서 순서 변경
itemUI.transform.SetSiblingIndex(toIndex);
// 정렬 순서 업데이트
UpdateSortOrders();
// 이벤트 발생
var eventArgs = new DraggableItemReorderEventArgs(itemData.Id, fromIndex, toIndex);
OnItemReordered?.Invoke(this, eventArgs);
Debug.Log($"아이템 순서 변경: {itemData.Id} ({fromIndex} -> {toIndex})");
}
catch (Exception ex)
{
Debug.LogError($"[{nameof(DraggableScrollList)}] 순서 변경 중 오류: {ex.Message}");
}
}
/// <summary>
/// 모든 아이템의 정렬 순서 업데이트
/// </summary>
private void UpdateSortOrders()
{
for (int i = 0; i < itemDataList.Count; i++)
{
itemDataList[i].SortOrder = i;
}
}
/// <summary>
/// 자동 스크롤 처리
/// </summary>
/// <param name="itemAnchoredPosition">item의 anchoredPosition</param>
private void HandleAutoScroll(Vector2 itemAnchoredPosition)
{
if (scrollRect?.viewport == null) return;
// 뷰포트 영역을 스크린 좌표로 변환
Vector3[] viewportCorners = new Vector3[4];
scrollRect.viewport.GetWorldCorners(viewportCorners);
// contentParent의 월드 좌표를 로컬로 변환
Vector2 viewportTopLeft = contentParent.InverseTransformPoint(viewportCorners[1]);
Vector2 viewportBottomLeft = contentParent.InverseTransformPoint(viewportCorners[0]);
float topY = viewportTopLeft.y;
float bottomY = viewportBottomLeft.y;
float scrollZone = 100f; // 스크롤 감지 영역 높이
// 상단 스크롤
if (itemAnchoredPosition.y > topY - scrollZone && itemAnchoredPosition.y < topY)
{
float scrollSpeed = (itemAnchoredPosition.y - (topY - scrollZone)) / scrollZone * scrollSensitivity;
scrollRect.verticalNormalizedPosition = Mathf.Clamp01(
scrollRect.verticalNormalizedPosition + scrollSpeed * Time.deltaTime);
}
// 하단 스크롤
else if (itemAnchoredPosition.y < bottomY + scrollZone && itemAnchoredPosition.y > bottomY)
{
float scrollSpeed = ((bottomY + scrollZone) - itemAnchoredPosition.y) / scrollZone * scrollSensitivity;
scrollRect.verticalNormalizedPosition = Mathf.Clamp01(
scrollRect.verticalNormalizedPosition - scrollSpeed * Time.deltaTime);
}
//float topY = viewportCorners[1].y;
//float bottomY = viewportCorners[0].y;
//float scrollZone = 100f; // 스크롤 감지 영역 높이
//// 상단 스크롤
//if (screenPosition.y > topY - scrollZone && screenPosition.y < topY)
//{
// float scrollSpeed = (screenPosition.y - (topY - scrollZone)) / scrollZone * scrollSensitivity;
// scrollRect.verticalNormalizedPosition = Mathf.Clamp01(
// scrollRect.verticalNormalizedPosition + scrollSpeed * Time.deltaTime);
//}
//// 하단 스크롤
//else if (screenPosition.y < bottomY + scrollZone && screenPosition.y > bottomY)
//{
// float scrollSpeed = ((bottomY + scrollZone) - screenPosition.y) / scrollZone * scrollSensitivity;
// scrollRect.verticalNormalizedPosition = Mathf.Clamp01(
// scrollRect.verticalNormalizedPosition - scrollSpeed * Time.deltaTime);
//}
}
/// <summary>
/// 드롭 위치 시각적 표시 업데이트
/// </summary>
/// <param name="itemAnchoredPosition">드래그 되고 있는 item의 anchoredPosition</param>
protected virtual void UpdateDropIndicator(Vector2 itemAnchoredPosition)
{
if (currentDraggingItem?.RectTransform == null ||
contentParent == null ||
dropLineRectTransform == null ||
uiCamera == null)
{
HideDropLine();
return;
}
// 스크린 좌표를 로컬 좌표로 변환
//Vector2 localPoint;
//if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(
// contentParent,
// eventData.position,
// uiCamera,
// out localPoint))
//{
// HideDropLine();
// return;
//}
// 드롭 인덱스 계산
int newDropIndex = CalculateDropIndexFromPosition(itemAnchoredPosition);
Debug.Log($"드롭 인덱스 계산: {newDropIndex}, {itemAnchoredPosition})");
if (newDropIndex != currentDropIndex)
{
currentDropIndex = newDropIndex;
}
ShowDropLineAtIndex(newDropIndex);
}
/// <summary>
/// 로컬 좌표를 기준으로 드롭 인덱스 계산
/// </summary>
/// <param name="localPoint">Content 내의 로컬 좌표</param>
/// <returns>드롭될 인덱스</returns>
private int CalculateDropIndexFromPosition(Vector2 localPoint)
{
if (itemUIList.Count == 0) return 0;
// 드래그 중인 아이템을 제외한 아이템들과 비교
float targetY = localPoint.y;
for (int i = 0; i < itemUIList.Count; i++)
{
var itemUI = itemUIList[i];
if (itemUI == currentDraggingItem || itemUI?.RectTransform == null)
continue;
//float itemY = itemUI.RectTransform.anchoredPosition.y;
//float itemHeight = itemUI.RectTransform.rect.height;
// 아이템의 월드 좌표를 contentParent의 로컬 좌표로 변환
Vector3 itemWorldPos = itemUI.RectTransform.position;
Vector2 itemLocalPos = contentParent.InverseTransformPoint(itemWorldPos);
float itemY = itemLocalPos.y;
float itemHeight = itemUI.RectTransform.rect.height;
Debug.Log($"targetY:{localPoint}, 아이템 {i} 위치 Y 좌표: {itemY}, 높이: {itemHeight}, {itemY - itemHeight / 2}");
// 아이템의 중앙을 기준으로 위/아래 판단
if (targetY > itemY - itemHeight / 2)
{
return i; // 현재 아이템 위에 드롭
}
}
// 모든 아이템보다 아래에 있으면 마지막 위치
return itemUIList.Count;
}
/// <summary>
/// 기존 드롭 위치 계산 (fallback용)
/// </summary>
/// <returns>드롭될 인덱스</returns>
private int CalculateDropIndex()
{
if (currentDraggingItem?.RectTransform == null || uiCamera == null)
return -1;
Vector2 localPoint;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(
contentParent,
Input.mousePosition,
uiCamera,
out localPoint))
{
return -1;
}
return CalculateDropIndexFromPosition(localPoint);
}
/// <summary>
/// 지정된 인덱스 위치에 드롭 라인 표시
/// </summary>
/// <param name="index">드롭 라인을 표시할 인덱스</param>
private void ShowDropLineAtIndex(int index)
{
if (dropLineRectTransform == null || contentParent == null) return;
Vector2 linePosition;
if (index >= itemUIList.Count)
{
// 마지막 위치에 드롭하는 경우
if (itemUIList.Count > 0)
{
var lastItem = itemUIList[itemUIList.Count - 1];
if (lastItem?.RectTransform != null)
{
//드래그 하는 아이템이 동일한 경우
if (dragStartIndex == itemUIList.Count - 1 && itemUIList.Count > 1)
{
lastItem = itemUIList[itemUIList.Count - 2];
}
float lastItemY = lastItem.RectTransform.anchoredPosition.y;
float lastItemHeight = lastItem.RectTransform.rect.height;
linePosition = new Vector2(0, lastItemY - lastItemHeight);// - dropLineMargin);
}
else
{
linePosition = Vector2.zero;
}
}
else
{
linePosition = Vector2.zero;
}
}
else if (index <= 0)
{
// 첫 번째 위치에 드롭하는 경우
if (itemUIList.Count > 0)
{
var firstItem = itemUIList[0];
if (firstItem?.RectTransform != null)
{
//드래그 하는 아이템이 동일한 경우
if (dragStartIndex == 0 && itemUIList.Count > 1)
{
firstItem = itemUIList[1];
}
float firstItemY = firstItem.RectTransform.anchoredPosition.y;
linePosition = new Vector2(0, firstItemY);// + dropLineMargin);
}
else
{
linePosition = Vector2.zero;
}
}
else
{
linePosition = Vector2.zero;
}
}
else
{
// 중간 위치에 드롭하는 경우
var upperItem = itemUIList[index - 1];
var lowerItem = itemUIList[index];
if (upperItem?.RectTransform != null && lowerItem?.RectTransform != null)
{
float upperItemY = upperItem.RectTransform.anchoredPosition.y;
float lowerItemY = lowerItem.RectTransform.anchoredPosition.y;
float upperItemHeight = upperItem.RectTransform.rect.height;
//드래그 하는 아이템이 동일한 경우
if (dragStartIndex == index - 1)
{
upperItemY = lowerItemY - upperItemHeight - layoutGroup.spacing;
}else if (dragStartIndex == index)
{
lowerItemY = upperItemY - upperItemHeight - layoutGroup.spacing;
}
// 두 아이템 사이의 중간 지점에 라인 표시
float middleY = (upperItemY - upperItemHeight + lowerItemY) / 2f;
linePosition = new Vector2(0, middleY);
}
else
{
linePosition = Vector2.zero;
}
}
// 드롭 라인 위치 설정
dropLineRectTransform.anchoredPosition = linePosition;
// 드롭 라인 표시
ShowDropLine();
Debug.Log($"드롭 라인 표시: 인덱스 {index}, 위치 {linePosition}");
}
/// <summary>
/// 드롭 라인 표시
/// </summary>
private void ShowDropLine()
{
if (dropLineObject == null) return;
if (!isDropLineVisible)
{
dropLineObject.SetActive(true);
isDropLineVisible = true;
}
}
/// <summary>
/// 드롭 라인 숨기기
/// </summary>
private void HideDropLine()
{
if (dropLineObject == null) return;
if (isDropLineVisible)
{
dropLineObject.SetActive(false);
isDropLineVisible = false;
}
}
/// <summary>
/// 드롭 라인 색상 설정
/// </summary>
/// <param name="color">설정할 색상</param>
public void SetDropLineColor(Color color)
{
dropLineColor = color;
if (dropLineImage != null)
{
dropLineImage.color = color;
}
}
/// <summary>
/// 드롭 라인 높이 설정
/// </summary>
/// <param name="height">설정할 높이</param>
public void SetDropLineHeight(float height)
{
dropLineHeight = height;
if (dropLineRectTransform != null)
{
dropLineRectTransform.sizeDelta = new Vector2(dropLineRectTransform.sizeDelta.x, height);
}
}
/// <summary>
/// 드롭 라인 스프라이트 변경
/// </summary>
/// <param name="sprite">새로운 스프라이트 (null일 경우 기본 스프라이트 사용)</param>
public void SetDropLineSprite(Sprite? sprite)
{
dropLineSprite = sprite;
if (dropLineImage != null)
{
if (sprite != null)
{
dropLineImage.sprite = sprite;
dropLineImage.type = Image.Type.Sliced;
}
else
{
dropLineImage.sprite = CreateDefaultDropLineSprite();
dropLineImage.type = Image.Type.Simple;
}
}
}
/// <summary>
/// 드롭 라인 Material 설정
/// </summary>
/// <param name="material">적용할 Material</param>
public void SetDropLineMaterial(Material? material)
{
dropLineMaterial = material;
if (dropLineImage != null)
{
dropLineImage.material = material;
}
}
/// <summary>
/// 드롭 라인 재생성 (설정 변경 후 적용 시 사용)
/// </summary>
public void RecreateDropLine()
{
// 기존 드롭 라인 제거
if (dropLineObject != null)
{
DestroyImmediate(dropLineObject);
dropLineObject = null;
dropLineImage = null;
dropLineRectTransform = null;
isDropLineVisible = false;
}
// 새로운 드롭 라인 생성
CreateDropLine();
}
/// <summary>
/// 특정 위치로 스크롤
/// </summary>
/// <param name="itemId">스크롤할 아이템 ID</param>
public void ScrollToItem(string itemId)
{
if (string.IsNullOrEmpty(itemId) || scrollRect == null) return;
var itemUI = itemUIList.FirstOrDefault(ui => ui.Data?.Id == itemId);
if (itemUI?.RectTransform == null) return;
// 아이템 위치 계산하여 스크롤
Canvas.ForceUpdateCanvases();
var contentRect = scrollRect.content;
var itemRect = itemUI.RectTransform;
if (contentRect != null && itemRect != null)
{
var itemPosition = (Vector2)scrollRect.transform.InverseTransformPoint(itemRect.position);
var contentPosition = (Vector2)scrollRect.transform.InverseTransformPoint(contentRect.position);
var newY = itemPosition.y - contentPosition.y;
contentRect.anchoredPosition = new Vector2(contentRect.anchoredPosition.x, -newY);
}
}
/// <summary>
/// 현재 목록 상태를 JSON으로 직렬화
/// </summary>
/// <returns>직렬화된 JSON 문자열</returns>
public virtual string SerializeToJson()
{
try
{
var serializableData = itemDataList.Select(data => new
{
id = data.Id,
sortOrder = data.SortOrder
// 주의: Sprite는 직렬화되지 않음 (필요시 별도 처리)
}).ToArray();
return JsonUtility.ToJson(new { items = serializableData }, true);
}
catch (Exception ex)
{
Debug.LogError($"[{nameof(DraggableScrollList)}] JSON 직렬화 오류: {ex.Message}");
return string.Empty;
}
}
/// <summary>
/// 컴포넌트 정리
/// </summary>
private void OnDestroy()
{
// 동적으로 생성한 텍스처 정리 (메모리 누수 방지)
if (dropLineImage?.sprite?.texture != null &&
dropLineImage.sprite.name == "DefaultDropLineSprite")
{
DestroyImmediate(dropLineImage.sprite.texture);
}
}
}
}