Files
EnglewoodLAB/Assets/Scripts/UVC/UI/UIDragger.cs

355 lines
13 KiB
C#

using System;
using UnityEngine;
using UnityEngine.EventSystems;
namespace UVC.UI
{
/// <summary>
/// UI 요소를 마우스로 드래그하여 이동할 수 있게 만드는 컴포넌트입니다.
/// IBeginDragHandler, IDragHandler, IEndDragHandler 인터페이스를 구현하여 드래그 이벤트를 처리합니다.
/// 이 컴포넌트는 드래그될 UI 요소의 자식 오브젝트(예: 창의 헤더 영역)에 부착되어야 합니다.
/// </summary>
/// <example>
/// <b>사용 예제:</b>
/// 1. 드래그하고 싶은 창(Panel) UI를 만듭니다.
/// 2. 해당 창의 자식으로 드래그 핸들 역할을 할 UI(예: Image)를 만들고, 이 오브젝트에 `UIDragger` 컴포넌트를 추가합니다.
/// 3. 아래와 같은 컨트롤러 스크립트를 만들어 창에 추가하고, Inspector 창에서 `draggerHandle`과 `windowPanel`을 연결해줍니다.
/// <code>
/// using UnityEngine;
/// using UVC.UI;
///
///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("드래그 가능한 영역을 지정합니다. null인 경우 Canvas를 자동으로 찾습니다.")]
private RectTransform dragArea; // 드래그가 가능한 영역
[SerializeField]
[Tooltip("드래그할 UI 요소를 지정합니다. null인 경우 부모를 자동으로 설정합니다.")]
private RectTransform dragObject; // 실제로 드래그될 UI 요소 (예: 창 전체)
[SerializeField]
[Tooltip("드래그 시작 시 해당 UI 요소를 맨 앞으로 가져올지 여부를 설정합니다.")]
private bool bringToFrontOnDrag = true; // 드래그 시작 시 맨 앞으로 가져올지 여부
[Header("드래그 영역 여백")]
[SerializeField]
[Tooltip("드래그 영역 하단에 추가할 여백입니다.")]
private float bottomPadding = 0;
[SerializeField]
[Tooltip("드래그 영역 상단에 추가할 여백입니다.")]
private float topPadding = 0;
[SerializeField]
[Tooltip("드래그 영역 좌측에 추가할 여백입니다.")]
private float leftPadding = 0;
[SerializeField]
[Tooltip("드래그 영역 우측에 추가할 여백입니다.")]
private float rightPadding = 0;
[SerializeField]
[Tooltip("드래그 중 실시간으로 영역 제한을 적용할지 여부")]
private bool constrainDuringDrag = true;
// 이벤트
public Action OnBeginDragHandler { get; set; }
public Action<Vector2> OnDragHandler { get; set; }
public Action<Vector2> OnEndDragHandler { get; set; }
// 캐시된 변수들
private Vector2 originalLocalPointerPosition;
private Vector2 originalAnchoredPosition;
private int originalSiblingIndex;
private Canvas parentCanvas;
private Camera canvasCamera;
// 프로퍼티
public RectTransform DragObject => dragObject;
public RectTransform DragArea => dragArea;
public bool IsDragging { private set; get; }
private void Start()
{
InitializeComponents();
ValidateSetup();
}
/// <summary>
/// 컴포넌트들을 초기화합니다.
/// </summary>
private void InitializeComponents()
{
// dragObject가 설정되지 않았다면, 부모를 드래그 대상으로 설정
if (dragObject == null)
{
dragObject = transform.parent as RectTransform;
}
// Canvas와 Camera 캐싱
parentCanvas = GetComponentInParent<Canvas>();
if (parentCanvas != null)
{
canvasCamera = parentCanvas.worldCamera;
}
// dragArea가 설정되지 않았다면, Canvas를 드래그 영역으로 설정
if (dragArea == null && parentCanvas != null)
{
dragArea = parentCanvas.transform as RectTransform;
}
}
/// <summary>
/// 설정이 올바른지 검증합니다.
/// </summary>
private void ValidateSetup()
{
if (dragObject == null)
{
Debug.LogWarning("[UIDragger] dragObject를 찾을 수 없습니다. 부모가 RectTransform이 아닙니다.", this);
enabled = false;
return;
}
if (parentCanvas == null)
{
Debug.LogWarning("[UIDragger] Canvas를 찾을 수 없습니다. 드래그 기능이 제한될 수 있습니다.", this);
}
if (dragArea == null)
{
Debug.LogWarning("[UIDragger] dragArea를 찾을 수 없습니다. Canvas를 찾을 수 없습니다.", this);
enabled = false;
return;
}
}
/// <summary>
/// 드래그가 허용되는 영역을 설정합니다.
/// </summary>
/// <remarks>이 메서드를 활성화하면 지정된 영역 내에서 드래그 기능이 활성화됩니다.
///</remarks>
/// <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 eventData)
{
if (eventData.button != PointerEventData.InputButton.Left) return;
if (!IsValidForDrag()) return;
IsDragging = true;
originalAnchoredPosition = dragObject.anchoredPosition;
originalSiblingIndex = dragObject.GetSiblingIndex();
// 마우스 포인터의 로컬 위치 계산
RectTransformUtility.ScreenPointToLocalPointInRectangle(
dragArea,
eventData.position,
canvasCamera,
out originalLocalPointerPosition);
// 맨 앞으로 가져오기
if (bringToFrontOnDrag)
{
dragObject.SetAsLastSibling();
}
OnBeginDragHandler?.Invoke();
}
/// <summary>
/// 드래그 중일 때 매 프레임 호출됩니다. (IDragHandler)
/// </summary>
public void OnDrag(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left) return;
if (!IsDragging || !IsValidForDrag()) return;
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(
dragArea,
eventData.position,
canvasCamera,
out Vector2 localPointerPosition))
{
Vector2 offsetToOriginal = localPointerPosition - originalLocalPointerPosition;
Vector2 newPosition = originalAnchoredPosition + offsetToOriginal;
//Debug.Log($"OnDrag originalAnchoredPosition:{originalAnchoredPosition}, newPosition:{newPosition}");
// 실시간 제약 적용
if (constrainDuringDrag) newPosition = ClampToArea(newPosition);
//Debug.Log($"OnDrag2 newPosition:{newPosition}");
dragObject.anchoredPosition = newPosition;
OnDragHandler?.Invoke(newPosition);
}
}
/// <summary>
/// 드래그가 끝났을 때 호출됩니다. (IEndDragHandler)
/// </summary>
public void OnEndDrag(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left) return;
if (!IsDragging) return;
IsDragging = false;
// 원래 형제 순서로 복원
if (bringToFrontOnDrag)
{
dragObject.SetSiblingIndex(originalSiblingIndex);
}
// 최종 위치 제약 적용
//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 Vector2 ClampToArea(Vector2 position)
{
if (dragArea == null || dragObject == null)
return position;
Rect dragObjectRect = dragObject.rect;
Rect dragAreaRect = dragArea.rect;
dragAreaRect.x = 0;
dragAreaRect.y = 0;
// Pivot과 앵커를 고려한 경계 계산
Vector2 pivot = dragObject.pivot;
Vector2 size = dragObjectRect.size;
//아래로 내려갈수록 좌상(0,0), 우하(1,-1)
float leftBoundary = dragAreaRect.xMin + (size.x * pivot.x) + leftPadding;
float rightBoundary = dragAreaRect.xMax - (size.x * (1f - pivot.x)) - rightPadding;
float bottomBoundary = -dragAreaRect.height + (size.y * pivot.y) + bottomPadding;
float topBoundary = -(size.y * (1f - pivot.y)) - topPadding;
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;
}
}
}