UGUI拖拽效果实现2
前言
之前我个人写过一个版本的拖拽效果实现,但是后面在一些场景中发现了一些问题,比如:应用窗口任意变换的时候,拖拽效果的设定显得麻烦,且会照成跟不上鼠标的问题;如果拖拽的物体本身的情况复杂,比如其锚点和其UI对齐格式奇葩或者其父节点也存在问题,那么判断是否在屏幕的方法就会错误,等等。
所以,为了解决这些问题,我又写了一个新的版本,这次我将会详细介绍一下这个版本的实现。
环境
Windows10操作系统 Unity2022.3.34f1c1 Unity UI 1.0.0
正文
这个版本的实现,仍然是基于UGUI本身提供的事件接口IBeginDragHandler、IDragHandler和IEndDragHandler。这次我还额外多加了IPointerDownHandler,这个接口。至于为什么我要多加这个接口,这个我在后面来说明。与前面的版本不同的是,这次的版本我并不考虑是否在屏幕外,我仅考虑UI是否在画布内。如果你要实现是否在屏幕内,其实就是将这个限制移动的画布设定为主画布就好了。特别说明,这次的版本我在UI为RenderMode.ScreenSpaceCamera模式下测试过。完整的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
| using System; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI;
namespace Test { [RequireComponent(typeof(Image))] public class DragArea : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IPointerDownHandler { [SerializeField] private Canvas refCanvas;
[SerializeField] private Camera uiCamera;
private Vector2 offset; private RectTransform rectTransform; private RectTransform canvasRectTransform;
private bool ready = false;
private bool interoperable = true;
public bool Interoperable { get{ return interoperable; } set{ interoperable = value; } } public bool LockedInCanvas { get; set; }
public Action<PointerEventData> StartDrag; public Action<PointerEventData> Draging; public Action<PointerEventData> DragEnd;
private void Awake() { rectTransform = GetComponent<RectTransform>();
if (!ready) { refCanvas = GetComponentInParent<Canvas>(); if (refCanvas != null) { canvasRectTransform = refCanvas.GetComponent<RectTransform>(); uiCamera = refCanvas.worldCamera; } } }
public void SetCanvasInfo(Canvas canvas,Camera uiCamera) { refCanvas = canvas; canvasRectTransform = refCanvas.GetComponent<RectTransform>(); this.uiCamera = uiCamera; ready = true; }
public void OnBeginDrag(PointerEventData eventData) { }
public void OnDrag(PointerEventData eventData) { if (Interoperable && ready) { Draging?.Invoke(eventData);
Vector2 pos; RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, eventData.position, uiCamera, out pos);
pos -= offset; pos += rectTransform.anchoredPosition; rectTransform.anchoredPosition = pos; if (LockedInCanvas) { var screenPos = eventData.position - offset; Vector2 uiPos; RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRectTransform, screenPos, uiCamera, out uiPos); var rect = this.rectTransform.rect; var lockedArea = canvasRectTransform.sizeDelta; uiPos.x += lockedArea.x * this.canvasRectTransform.pivot.x; uiPos.y += lockedArea.y * this.canvasRectTransform.pivot.y; float maxXVal = uiPos.x + rect.width * (1 - this.rectTransform.pivot.x); float maxYVal = uiPos.y + rect.height * (1 - this.rectTransform.pivot.y); float minXVal = uiPos.x - rect.width * this.rectTransform.pivot.x; float minYVal = uiPos.y - rect.height * this.rectTransform.pivot.y; if (maxXVal > lockedArea.x) { pos.x -= maxXVal - lockedArea.x; }
if (maxYVal > lockedArea.y) { pos.y -= maxYVal - lockedArea.y; }
if (minXVal < 0) pos.x -= minXVal; if (minYVal < 0) pos.y -= minYVal; rectTransform.anchoredPosition = pos; } } }
public void OnEndDrag(PointerEventData eventData) { DragEnd?.Invoke(eventData); }
public void OnPointerDown(PointerEventData eventData) { if (refCanvas == null) { Debug.LogError("DragArea: Canvas is null!"); ready = false; return; }
if (uiCamera == null) { Debug.LogError("DragArea: uiCamera is null!"); ready = false; return; } ready = true; if (Interoperable) { StartDrag?.Invoke(eventData); RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, eventData.position, uiCamera, out offset); } } } }
|
这里你可以发现,我将初始判断是否拖拽放到了IPointerDownHandler
所要实现的函数OnPointerDown
下。这是因为当开始调用OnBeginDrag
时,鼠标其实要进行一定的偏移才会触发。如果这时候游戏卡顿就会照成鼠标与UI之间位置有着较大的偏移。当然如果你只要UI的点位和鼠标的点位重叠在一起,那么就在代码中将这个偏移值对应的逻辑删除。其实计算这个偏移值也是有很大的坑点,这个我放在后文讲述,因此如果你并没有这样的要求最好是不要加上偏移的。这里我额外说明一下UI的点位,代码中我们使用RectTransformUtility.ScreenPointToLocalPointInRectangle
计算出来的偏差其实和UI轴心点(pivot)之间的偏差。
我们将UI限制在画布中时,首先我们要先知道UI轴心点在这个画布中的位置。然后我们再用窗口中的位置得到UI轴心点在画布中的位置。因为我以画布的左下为原点,所以我要把现在得到的点转换为相对于画布左下点的位置。然后我们再通过加减UI的整体大小来得到UI在画布中的范围,最后我们将UI超出的部分移除,其位置限制便在画布内。我这里使用了鼠标位置(eventData.position
)减去偏移量(offset
)得到的位置便得到UI轴心点在窗口的位置。或许你有点奇怪,我们都有相机的情况下,我们完全可以使用相机提供的WorldToScreenPoint
函数来实现我们想要的效果。这里我也有点疑惑,我个人认为是位置更新并没有实时更新,所以导致了WorldToScreenPoint
函数的结果有问题。但是这个只是我的猜测,具体的原因我还没能找出来。
而正是因为我使用了偏移量导致了另外一个问题。当UI缩放和主画布缩放不一致的情况下,eventData.position - offset
不会是UI轴心点在窗口的位置。这是因为无论你如何缩放UI,你使用RectTransformUtility.ScreenPointToLocalPointInRectangle
所得到的偏移值只会限制在UI设定的大小内即RectTransform.sizeDelta
范围内,这个大家可以自行试验一下。而我们用rect
获取到的值也不会随着UI缩放而改变。因此我们要用另外的办法计算这个偏移值,但是原本的偏移值仍然有用。因为我们拖拽赋值仍然使用的是RectTransformUtility.ScreenPointToLocalPointInRectangle
函数得到的值,所以原先使用RectTransformUtility.ScreenPointToLocalPointInRectangle
得到的偏移值仍然有用。下面是我重新修改后的计算偏移值的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public void OnPointerDown(PointerEventData eventData) { if (refCanvas == null) { Debug.LogError("DragArea: Canvas is null!"); ready = false; return; }
if (uiCamera == null) { Debug.LogError("DragArea: uiCamera is null!"); ready = false; return; } ready = true; if (Interoperable) { StartDrag?.Invoke(eventData); var screenPos = uiCamera.WorldToScreenPoint(rectTransform.position); offset.x = eventData.position.x - screenPos.x; offset.y = eventData.position.y - screenPos.y; RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, eventData.position, uiCamera, out rectOffset); } }
|
这里我建之前的offset
变量变为了rectOffset
,而offset
用来计算在窗口下的偏移值。因为缩放不仅仅在计算偏移值时会导致问题,在后续计算是否超出画布边界时也会导致问题。这里我直接将UI和画布下的所有信息都除于其对应的缩放,让他们在统一尺度下进行计算,最后再次计算缩放得到最终的UI位置。最后完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
| using System; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI;
namespace Test { [RequireComponent(typeof(Image))] public class DragArea : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IPointerDownHandler { [SerializeField] private Canvas refCanvas;
[SerializeField] private Camera uiCamera;
private Vector2 offset; private Vector2 rectOffset; private RectTransform rectTransform; private RectTransform canvasRectTransform;
private bool ready = false;
private bool interoperable = true;
public bool Interoperable { get { return interoperable; } set { interoperable = value; } } public bool LockedInCanvas { get; set; }
public Action<PointerEventData> StartDrag; public Action<PointerEventData> Draging; public Action<PointerEventData> DragEnd;
private void Awake() { rectTransform = GetComponent<RectTransform>();
if (!ready) { refCanvas = GetComponentInParent<Canvas>(); if (refCanvas != null) { canvasRectTransform = refCanvas.GetComponent<RectTransform>(); uiCamera = refCanvas.worldCamera; } } }
public void SetCanvasInfo(Canvas canvas, Camera uiCamera) { refCanvas = canvas; canvasRectTransform = refCanvas.GetComponent<RectTransform>(); this.uiCamera = uiCamera; ready = true; }
public void OnBeginDrag(PointerEventData eventData) {
}
public void OnDrag(PointerEventData eventData) { if (Interoperable && ready) { Draging?.Invoke(eventData);
Vector2 pos; RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, eventData.position, uiCamera, out pos);
pos -= rectOffset; pos += rectTransform.anchoredPosition; rectTransform.anchoredPosition = pos; if (LockedInCanvas) { var screenPos = eventData.position - offset; Vector2 uiPos; RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRectTransform, screenPos, uiCamera, out uiPos); var rectWorldSize = rectTransform.rect.size * rectTransform.lossyScale; var lockedArea = canvasRectTransform.sizeDelta; uiPos.x += lockedArea.x * this.canvasRectTransform.pivot.x; uiPos.y += lockedArea.y * this.canvasRectTransform.pivot.y; lockedArea *= canvasRectTransform.lossyScale; uiPos *= canvasRectTransform.lossyScale; float maxXVal = uiPos.x + rectWorldSize.x * (1 - this.rectTransform.pivot.x); float maxYVal = uiPos.y + rectWorldSize.y * (1 - this.rectTransform.pivot.y); float minXVal = uiPos.x - rectWorldSize.x * this.rectTransform.pivot.x; float minYVal = uiPos.y - rectWorldSize.y * this.rectTransform.pivot.y;
if (maxXVal > lockedArea.x) { pos.x -= (maxXVal - lockedArea.x) / rectTransform.lossyScale.x; }
if (maxYVal > lockedArea.y) { pos.y -= (maxYVal - lockedArea.y) / rectTransform.lossyScale.y; }
if (minXVal < 0) pos.x -= minXVal / rectTransform.lossyScale.x; if (minYVal < 0) pos.y -= minYVal / rectTransform.lossyScale.y; rectTransform.anchoredPosition = pos; } } }
public void OnEndDrag(PointerEventData eventData) { DragEnd?.Invoke(eventData); }
public void OnPointerDown(PointerEventData eventData) { if (refCanvas == null) { Debug.LogError("DragArea: Canvas is null!"); ready = false; return; }
if (uiCamera == null) { Debug.LogError("DragArea: uiCamera is null!"); ready = false; return; } ready = true; if (Interoperable) { StartDrag?.Invoke(eventData); var screenPos = uiCamera.WorldToScreenPoint(rectTransform.position); offset.x = eventData.position.x - screenPos.x; offset.y = eventData.position.y - screenPos.y; RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, eventData.position, uiCamera, out rectOffset); } } } }
|
总结
这里我做两个实现,我认为这些都很有用。第一个实现需要画布,UI和主画布全部缩放一致下使用,虽然限制很多,但是它性能比第二个来得好。所以这里我将这两个实现都放出来,大家按照各自项目的需求来选择使用。现在我暂时没遇到新的问题了,但是我觉得可能还有问题。如果还有问题欢迎直接私信告知我。毕竟我并没有在很多复杂的场景下进行使用,所以我也不知道还有没有新的问题。