- 引言:
作为一个Unity初学者,遇到了需要实现以鼠标为中心缩放的功能且需要支持拖拽,秉着复用主义的原则,在网上查找了不少博客,要么免费但不能直接拿来使用,要么需要VIP充值获取项目代码,此外,原理且讲解甚少。为此,笔者花了半天时间研究清楚底层原理后,实现了这个功能。核心代码量其实就那么十几行,懂得原理才是核心。
在讲解下面基础原理前,默认读者已经会实现以图片轴心(缩放的默认中心)缩放的功能。(这个功能很简单,相信网上有很多可靠的资料。)
-
以鼠标为中心缩放的基础介绍:
在图片缩放中,一般做法是对rectTransform.localScale赋值缩放倍率,这个缩放是以图片轴心(默认是图片几何中心)缩放的,相对以鼠标位置缩放存在偏移。
直观上就是当我们鼠标滚轮滚动时,以图片轴心为中心的缩放,在缩放后,鼠标的位置相对图片发生偏移了,也就是说--鼠标对应的屏幕位置没有动,但是该屏幕上显示的内容不是缩放前的内容,
而很多功能需要具有局部放大的功能,即需要以鼠标为中心进行缩放。 -
以鼠标为中心缩放的基础原理:
既然我们明白了这两者缩放存在一个偏移,那么实现以鼠标为中心缩放,可以考虑先使用以图片为中心的缩放,再在缩放后,进行偏移的修正,具体做法是计算出鼠标对应的图片中的位置在缩放前后的偏移量,然后把图片整体朝相反方向进行移动,从而保证鼠标位置处的像素在屏幕上没有动。
这里清楚的几个坐标是:鼠标位置坐标(屏幕坐标系),鼠标位置坐标转成的图片局部坐标(以图片轴心为原点,右上是正方向),Unity有提供鼠标位置坐标转成图片局部坐标的API函数(具体见下面代码)
具体偏移修正是这样的:假设图片上像素点A,它的局部坐标是localPos,在放大x倍后,localPos不会变化,但是像素点A在屏幕上的位置会朝着localPos的方向被放大x倍,因此需要将图片整体朝localPos相反的方向移动x倍,即:imgRectTransform.anchoredPosition-=localPos*x; -
鼠标拖拽的基础原理:
其实鼠标拖拽十分基础,并不是我这篇帖子的重点,但有一个点容易被忽视(AI也忽视了)。在鼠标拖拽中有一步从屏幕坐标转局部坐标时会以图片位置为参照,因此在修改图片的位置后,在记录上一次的鼠标局部坐标时,不能直接将图片位置修改前的局部坐标拿来赋值,而应该拿屏幕坐标再转换一次来赋值。
好了,原理和一些细节我讲完了,下面是最重要的环节--可复用的代码,毕竟可复用的代码才是硬道理(代码中有些逻辑本文没有提到,但也是交互的重点,如:拖拽鼠标倍率问题)。对于那些只差临门一脚的同行们,上面的原理他们想必了然于胸,只是某些细节被忽视了,或者对于像我这样的初学者,可运行的代码才是最好的学习资料。
PS:由于这篇博客是笔者第一次做的博客分享,带着对初次写博客的新奇,可能略显得行文啰嗦,但是其中表达了我对博客内容的一些态度,后续博客会减少这部分的比例,用实际内容输出。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;using UnityEngine.EventSystems;[RequireComponent(typeof(RectTransform))]
public class RawImageDragZoom : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IScrollHandler
{[Header("缩放参数")]public float zoomSpeed = 0.2f; // 滚轮灵敏度public float minScale = 0.2f; // 最小缩放public float maxScale = 5f; // 最大缩放[Header("拖动参数")]public float moveSpeed = 1f;[Header("边界缓冲")]public Vector2 padding = Vector2.zero; // 距离边缘留白private RectTransform rt; // 自己的 RectTransformprivate Vector2 lastMouseLocal; // 上次鼠标局部坐标private Vector2 originalSize; // 初始尺寸(用于限制边界)private Canvas canvas; // 所在 Canvasvoid Awake(){rt = transform as RectTransform;canvas = GetComponentInParent<Canvas>();originalSize = rt.rect.size;}/* -------------------- 拖拽 -------------------- */public void OnBeginDrag(PointerEventData eventData){RectTransformUtility.ScreenPointToLocalPointInRectangle(rt, eventData.position, canvas.worldCamera, out lastMouseLocal);}public void OnDrag(PointerEventData eventData){Vector2 mouseLocal;RectTransformUtility.ScreenPointToLocalPointInRectangle(rt, eventData.position, canvas.worldCamera, out mouseLocal);Vector2 delta = mouseLocal - lastMouseLocal;rt.anchoredPosition += delta * moveSpeed * rt.transform.localScale.x; //拖拽放大后会导致屏幕坐标映射到rt上的局部坐标以对应倍数缩小lastMouseLocal = mouseLocal;// rt.anchoredPosition变了,ScreenPointToLocalPointInRectangle输出的位置也会变化,需要重新算一遍新的mouseLocal来赋值给lastMouseLocalRectTransformUtility.ScreenPointToLocalPointInRectangle(rt, eventData.position, canvas.worldCamera, out lastMouseLocal);ClampPosition(); // 拖完立刻限制边界}public void OnEndDrag(PointerEventData eventData) { }/* -------------------- 滚轮缩放 -------------------- */public void OnScroll(PointerEventData eventData){float delta = eventData.scrollDelta.y * zoomSpeed;float _newScale = Mathf.Clamp(rt.localScale.x + delta, minScale, maxScale);float newScale = _newScale / rt.localScale.x;Debug.Log($"rt.localScale: {rt.localScale}");Vector3 scale = rt.localScale * newScale;// 以鼠标为中心缩放:先计算鼠标在 Rect 内的归一化坐标Vector2 mouseLocal;Debug.Log($"eventData: {eventData}");RectTransformUtility.ScreenPointToLocalPointInRectangle(rt, eventData.position, canvas.worldCamera, out mouseLocal);delta = _newScale - rt.localScale.x; //缩放倍速被限制情况,不移动rt.localScale = scale;rt.anchoredPosition -= mouseLocal * delta; //ClampPosition();}/* -------------------- 边界限制 -------------------- */private void ClampPosition(){Vector2 scale = rt.localScale;Vector2 size = originalSize * scale;Vector2 min = -size * rt.pivot + padding;Vector2 max = (Vector2.one - rt.pivot) * size - padding;Vector2 pos = rt.anchoredPosition;pos.x = Mathf.Clamp(pos.x, min.x, max.x);pos.y = Mathf.Clamp(pos.y, min.y, max.y);rt.anchoredPosition = pos;}
}