MeowmentDebugTool/Packages/com.bywaystudios.meowmentdebugtool/Runtime/DraggableFloatingButton.cs
2026-02-03 10:32:06 +08:00

311 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace MeowmentDebugTool
{
/// <summary>
/// 可拖动的悬浮按钮
/// 点击打开调试工具,可以在屏幕上自由拖动
/// </summary>
public class DraggableFloatingButton : MonoBehaviour, IPointerDownHandler, IBeginDragHandler, IDragHandler, IEndDragHandler, IPointerClickHandler
{
[Header("设置")]
[Tooltip("是否启用拖动功能")]
public bool enableDragging = false;
[Tooltip("是否限制在屏幕范围内")]
public bool clampToScreen = true;
[Tooltip("与屏幕边缘的最小距离")]
public float edgePadding = 20f;
[Header("自动吸附")]
[Tooltip("是否自动吸附到屏幕边缘")]
public bool snapToEdge = true;
[Tooltip("吸附动画时间")]
public float snapDuration = 0.1f;
private RectTransform rectTransform;
private Canvas canvas;
private Vector2 dragOffset;
private bool isDragging = false;
private Vector2 targetPosition;
private bool isSnapping = false;
// 用于区分点击和拖动
private bool hasDragged = false; // 标记本次操作是否进行了拖动
private Vector2 dragStartPosition; // 拖动起始位置
private const float dragThreshold = 5f; // 拖动阈值,移动超过这个距离才算拖动
private void Awake()
{
rectTransform = GetComponent<RectTransform>();
canvas = GetComponentInParent<Canvas>();
if (canvas == null)
{
Debug.LogError("DraggableFloatingButton 必须在Canvas下");
}
targetPosition = rectTransform.anchoredPosition;
}
private void Update()
{
// 平滑移动到目标位置(吸附动画)
if (isSnapping && !isDragging)
{
rectTransform.anchoredPosition = Vector2.Lerp(
rectTransform.anchoredPosition,
targetPosition,
Time.deltaTime / snapDuration
);
if (Vector2.Distance(rectTransform.anchoredPosition, targetPosition) < 0.5f)
{
rectTransform.anchoredPosition = targetPosition;
isSnapping = false;
}
}
}
/// <summary>
/// 每次按下时重置拖动标记在OnBeginDrag之前触发
/// </summary>
public void OnPointerDown(PointerEventData eventData)
{
hasDragged = false; // 重置拖动标记
Debug.Log("[DraggableFloatingButton] OnPointerDown - 重置拖动标记");
}
public void OnBeginDrag(PointerEventData eventData)
{
if (!enableDragging)
{
// 如果禁用拖动将事件传递给父级让其他UI元素能够接收拖动事件
PassEventToParent(eventData, ExecuteEvents.beginDragHandler);
return;
}
isDragging = true;
isSnapping = false;
dragStartPosition = eventData.position; // 记录起始位置
// 计算拖动偏移
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.transform as RectTransform,
eventData.position,
eventData.pressEventCamera,
out Vector2 localPoint
);
dragOffset = rectTransform.anchoredPosition - localPoint;
}
public void OnDrag(PointerEventData eventData)
{
if (!enableDragging)
{
// 如果禁用拖动,将事件传递给父级
PassEventToParent(eventData, ExecuteEvents.dragHandler);
return;
}
if (!isDragging) return;
// 检查是否超过拖动阈值
float dragDistance = Vector2.Distance(dragStartPosition, eventData.position);
if (dragDistance > dragThreshold)
{
hasDragged = true; // 标记已经拖动
}
// 转换屏幕坐标到Canvas局部坐标
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.transform as RectTransform,
eventData.position,
eventData.pressEventCamera,
out Vector2 localPoint
);
// 应用拖动偏移
Vector2 newPosition = localPoint + dragOffset;
// 限制在屏幕范围内
if (clampToScreen)
{
newPosition = ClampToScreen(newPosition);
}
rectTransform.anchoredPosition = newPosition;
}
public void OnEndDrag(PointerEventData eventData)
{
if (!enableDragging)
{
// 如果禁用拖动,将事件传递给父级
PassEventToParent(eventData, ExecuteEvents.endDragHandler);
return;
}
isDragging = false;
// 自动吸附到最近的边缘
if (snapToEdge)
{
SnapToNearestEdge();
}
}
/// <summary>
/// 处理点击事件 - 只有在没有拖动时才打开窗口
/// </summary>
public void OnPointerClick(PointerEventData eventData)
{
Debug.Log($"[DraggableFloatingButton] OnPointerClick 被触发hasDragged={hasDragged}");
// 如果进行了拖动,不执行点击操作
if (hasDragged)
{
Debug.Log("[!] 检测到拖动,不打开窗口");
return;
}
// 纯点击操作,打开调试窗口
Debug.Log("[OK] 点击浮窗,打开调试窗口");
UniversalDebugTool debugTool = FindObjectOfType<UniversalDebugTool>();
if (debugTool != null)
{
debugTool.OpenDebugWindow();
}
else
{
Debug.LogWarning("[X] 未找到 UniversalDebugTool 实例!");
}
}
private Vector2 ClampToScreen(Vector2 position)
{
RectTransform canvasRect = canvas.transform as RectTransform;
Vector2 canvasSize = canvasRect.sizeDelta;
Vector2 buttonSize = rectTransform.sizeDelta;
// 计算可移动范围
float minX = -canvasSize.x / 2 + buttonSize.x / 2 + edgePadding;
float maxX = canvasSize.x / 2 - buttonSize.x / 2 - edgePadding;
float minY = -canvasSize.y / 2 + buttonSize.y / 2 + edgePadding;
float maxY = canvasSize.y / 2 - buttonSize.y / 2 - edgePadding;
position.x = Mathf.Clamp(position.x, minX, maxX);
position.y = Mathf.Clamp(position.y, minY, maxY);
return position;
}
private void SnapToNearestEdge()
{
RectTransform canvasRect = canvas.transform as RectTransform;
Vector2 canvasSize = canvasRect.sizeDelta;
Vector2 currentPos = rectTransform.anchoredPosition;
// 计算到各个边缘的距离
float distToLeft = Mathf.Abs(currentPos.x + canvasSize.x / 2);
float distToRight = Mathf.Abs(currentPos.x - canvasSize.x / 2);
float distToTop = Mathf.Abs(currentPos.y - canvasSize.y / 2);
float distToBottom = Mathf.Abs(currentPos.y + canvasSize.y / 2);
// 找到最近的边
float minDist = Mathf.Min(distToLeft, distToRight, distToTop, distToBottom);
Vector2 snapPosition = currentPos;
Vector2 buttonSize = rectTransform.sizeDelta;
if (minDist == distToLeft)
{
// 吸附到左边
snapPosition.x = -canvasSize.x / 2 + buttonSize.x / 2 + edgePadding;
}
else if (minDist == distToRight)
{
// 吸附到右边
snapPosition.x = canvasSize.x / 2 - buttonSize.x / 2 - edgePadding;
}
else if (minDist == distToTop)
{
// 吸附到顶部
snapPosition.y = canvasSize.y / 2 - buttonSize.y / 2 - edgePadding;
}
else if (minDist == distToBottom)
{
// 吸附到底部
snapPosition.y = -canvasSize.y / 2 + buttonSize.y / 2 + edgePadding;
}
// 确保在屏幕范围内
snapPosition = ClampToScreen(snapPosition);
targetPosition = snapPosition;
isSnapping = true;
}
/// <summary>
/// 设置悬浮按钮位置
/// </summary>
public void SetPosition(Vector2 position)
{
if (clampToScreen)
{
position = ClampToScreen(position);
}
rectTransform.anchoredPosition = position;
targetPosition = position;
}
/// <summary>
/// 获取当前位置
/// </summary>
public Vector2 GetPosition()
{
return rectTransform.anchoredPosition;
}
/// <summary>
/// 将事件传递给父级或下层的UI元素
/// 当拖动功能被禁用时确保其他UI元素仍能接收拖动事件
/// </summary>
private void PassEventToParent<T>(PointerEventData eventData, ExecuteEvents.EventFunction<T> eventFunction) where T : IEventSystemHandler
{
Transform parent = transform.parent;
while (parent != null)
{
// 尝试执行父级对象上的事件
if (ExecuteEvents.Execute(parent.gameObject, eventData, eventFunction))
{
// 如果父级处理了事件,停止传递
return;
}
parent = parent.parent;
}
// 如果父级都没有处理,尝试传递给射线检测到的下一个对象
var raycastResults = new System.Collections.Generic.List<RaycastResult>();
EventSystem.current.RaycastAll(eventData, raycastResults);
// 跳过当前对象,找到下一个可以处理事件的对象
foreach (var result in raycastResults)
{
if (result.gameObject == gameObject)
continue;
if (ExecuteEvents.Execute(result.gameObject, eventData, eventFunction))
{
return;
}
}
}
}
}