355 lines
13 KiB
C#
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;
|
|
}
|
|
}
|
|
} |