226 lines
7.9 KiB
C#
226 lines
7.9 KiB
C#
using UnityEngine;
|
||
using UnityEngine.EventSystems;
|
||
using System.Collections.Generic;
|
||
|
||
[RequireComponent(typeof(RectTransform))]
|
||
public class DragUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
|
||
{
|
||
// 拖拽参考点
|
||
private Vector2 m_dragOffset = Vector2.zero;
|
||
private RectTransform m_rectTransform;
|
||
private Canvas m_canvas;
|
||
|
||
[Header("边界限制")]
|
||
[Tooltip("是否将UI限制在屏幕边界内")]
|
||
public bool clampToScreen = true;
|
||
[Tooltip("限制的边距(像素)")]
|
||
public Vector4 screenMargin = Vector4.zero; // 左, 下, 右, 上
|
||
|
||
[Header("避让其他UI (可选)")]
|
||
[Tooltip("需要避开的UI矩形列表。如果列表为空,则不检查遮挡。")]
|
||
public List<RectTransform> avoidTheseRects = new List<RectTransform>();
|
||
[Tooltip("避让的缓冲间距(像素)")]
|
||
public float avoidancePadding = 10f;
|
||
|
||
private void Start()
|
||
{
|
||
m_rectTransform = GetComponent<RectTransform>();
|
||
// 向上查找Canvas
|
||
m_canvas = GetComponentInParent<Canvas>();
|
||
if (m_canvas == null)
|
||
{
|
||
Debug.LogError("此脚本需要在一个Canvas的子物体上!", this);
|
||
}
|
||
}
|
||
|
||
public void OnBeginDrag(PointerEventData eventData)
|
||
{
|
||
// 计算鼠标点击点与UI中心点的偏移量
|
||
Vector2 localPointerPosition;
|
||
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(m_rectTransform, eventData.position, eventData.pressEventCamera, out localPointerPosition))
|
||
{
|
||
m_dragOffset = m_rectTransform.anchoredPosition - localPointerPosition;
|
||
}
|
||
}
|
||
|
||
public void OnDrag(PointerEventData eventData)
|
||
{
|
||
if (m_rectTransform == null || m_canvas == null) return;
|
||
|
||
// 1. 先将UI移动到鼠标位置(应用偏移)
|
||
Vector2 localPointerPosition;
|
||
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(m_rectTransform.parent as RectTransform, eventData.position, eventData.pressEventCamera, out localPointerPosition))
|
||
{
|
||
m_rectTransform.anchoredPosition = localPointerPosition + m_dragOffset;
|
||
}
|
||
|
||
// 2. 应用屏幕边界约束
|
||
if (clampToScreen)
|
||
{
|
||
ClampToScreen();
|
||
}
|
||
|
||
// 3. 应用避让规则
|
||
if (avoidTheseRects != null && avoidTheseRects.Count > 0)
|
||
{
|
||
AvoidOverlap();
|
||
}
|
||
}
|
||
|
||
public void OnEndDrag(PointerEventData eventData)
|
||
{
|
||
// 拖拽结束时再次确保约束,防止在结束瞬间甩出去
|
||
if (clampToScreen)
|
||
{
|
||
ClampToScreen();
|
||
}
|
||
if (avoidTheseRects != null && avoidTheseRects.Count > 0)
|
||
{
|
||
AvoidOverlap();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将UI限制在屏幕范围内
|
||
/// </summary>
|
||
private void ClampToScreen()
|
||
{
|
||
Vector3[] corners = new Vector3[4];
|
||
m_rectTransform.GetWorldCorners(corners);
|
||
|
||
// 根据Canvas的渲染模式处理屏幕坐标
|
||
Rect canvasRect = GetCanvasRect();
|
||
Rect uiRect = GetWorldRectFromCorners(corners);
|
||
|
||
Vector2 clampedPosition = m_rectTransform.anchoredPosition;
|
||
|
||
// 左边界
|
||
if (uiRect.xMin < canvasRect.xMin + screenMargin.x)
|
||
{
|
||
float overflow = canvasRect.xMin + screenMargin.x - uiRect.xMin;
|
||
clampedPosition.x += overflow;
|
||
}
|
||
// 右边界
|
||
if (uiRect.xMax > canvasRect.xMax - screenMargin.z)
|
||
{
|
||
float overflow = uiRect.xMax - (canvasRect.xMax - screenMargin.z);
|
||
clampedPosition.x -= overflow;
|
||
}
|
||
// 下边界
|
||
if (uiRect.yMin < canvasRect.yMin + screenMargin.y)
|
||
{
|
||
float overflow = canvasRect.yMin + screenMargin.y - uiRect.yMin;
|
||
clampedPosition.y += overflow;
|
||
}
|
||
// 上边界
|
||
if (uiRect.yMax > canvasRect.yMax - screenMargin.w)
|
||
{
|
||
float overflow = uiRect.yMax - (canvasRect.yMax - screenMargin.w);
|
||
clampedPosition.y -= overflow;
|
||
}
|
||
|
||
m_rectTransform.anchoredPosition = clampedPosition;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 尝试调整位置以避免与指定的UI重叠
|
||
/// </summary>
|
||
private void AvoidOverlap()
|
||
{
|
||
Rect myRect = GetWorldRect(m_rectTransform);
|
||
|
||
foreach (var otherRectTransform in avoidTheseRects)
|
||
{
|
||
if (otherRectTransform == null || otherRectTransform == m_rectTransform) continue;
|
||
|
||
Rect otherRect = GetWorldRect(otherRectTransform);
|
||
otherRect.xMin -= avoidancePadding;
|
||
otherRect.xMax += avoidancePadding;
|
||
otherRect.yMin -= avoidancePadding;
|
||
otherRect.yMax += avoidancePadding;
|
||
|
||
if (myRect.Overlaps(otherRect))
|
||
{
|
||
// 计算重叠区域
|
||
float overlapX = Mathf.Min(myRect.xMax, otherRect.xMax) - Mathf.Max(myRect.xMin, otherRect.xMin);
|
||
float overlapY = Mathf.Min(myRect.yMax, otherRect.yMax) - Mathf.Max(myRect.yMin, otherRect.yMin);
|
||
|
||
// 向重叠较少的方向移动
|
||
Vector2 shift = Vector2.zero;
|
||
if (overlapX < overlapY)
|
||
{
|
||
// 水平方向重叠少,水平移动
|
||
float shiftDirection = (myRect.center.x < otherRect.center.x) ? -1f : 1f;
|
||
shift.x = overlapX * shiftDirection;
|
||
}
|
||
else
|
||
{
|
||
// 垂直方向重叠少,垂直移动
|
||
float shiftDirection = (myRect.center.y < otherRect.center.y) ? -1f : 1f;
|
||
shift.y = overlapY * shiftDirection;
|
||
}
|
||
|
||
// 应用偏移
|
||
Vector2 newScreenPos = RectTransformUtility.WorldToScreenPoint(null, m_rectTransform.position) + shift;
|
||
Vector2 localPos;
|
||
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(m_rectTransform.parent as RectTransform, newScreenPos, null, out localPos))
|
||
{
|
||
m_rectTransform.anchoredPosition = localPos;
|
||
}
|
||
|
||
// 移动后重新计算自己的矩形,并重新进行屏幕边界约束
|
||
if (clampToScreen)
|
||
{
|
||
ClampToScreen();
|
||
}
|
||
// 跳出循环,一次只处理一个重叠(移动后可能又和另一个重叠,这是简化处理)
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取UI的世界空间矩形
|
||
/// </summary>
|
||
private Rect GetWorldRect(RectTransform rt)
|
||
{
|
||
Vector3[] corners = new Vector3[4];
|
||
rt.GetWorldCorners(corners);
|
||
return GetWorldRectFromCorners(corners);
|
||
}
|
||
|
||
private Rect GetWorldRectFromCorners(Vector3[] corners)
|
||
{
|
||
Vector2 min = corners[0];
|
||
Vector2 max = corners[2];
|
||
for (int i = 1; i < 4; i++)
|
||
{
|
||
min.x = Mathf.Min(min.x, corners[i].x);
|
||
min.y = Mathf.Min(min.y, corners[i].y);
|
||
max.x = Mathf.Max(max.x, corners[i].x);
|
||
max.y = Mathf.Max(max.y, corners[i].y);
|
||
}
|
||
return new Rect(min.x, min.y, max.x - min.x, max.y - min.y);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取Canvas对应的屏幕矩形(世界坐标)
|
||
/// 注意:此方法适用于Screen Space - Overlay模式。其他模式需调整。
|
||
/// </summary>
|
||
private Rect GetCanvasRect()
|
||
{
|
||
if (m_canvas.renderMode == RenderMode.ScreenSpaceOverlay)
|
||
{
|
||
return new Rect(0, 0, Screen.width, Screen.height);
|
||
}
|
||
else
|
||
{
|
||
// 对于World Space或Screen Space - Camera,这里需要更复杂的转换
|
||
// 为简化,假设Canvas充满屏幕
|
||
RectTransform canvasRect = m_canvas.GetComponent<RectTransform>();
|
||
Vector3[] canvasCorners = new Vector3[4];
|
||
canvasRect.GetWorldCorners(canvasCorners);
|
||
return GetWorldRectFromCorners(canvasCorners);
|
||
}
|
||
}
|
||
} |