using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; // decompiled source for ScrollRect can be found here: // https://github.com/jamesjlinden/unity-decompiled/blob/master/UnityEngine.UI/UI/ScrollRect.cs public class PinchableScrollRect : ScrollRect, IPointerEnterHandler, IPointerExitHandler { public enum ZoomMode { LocalScale, RectSize } public ZoomMode _zoomMode = ZoomMode.LocalScale; [SerializeField] protected float _minZoom = .1f; [SerializeField] protected float _maxZoom = 10; [SerializeField] protected float _zoomLerpSpeed = 10f; protected float _currentZoom = 1; public float CurrentZoom => _currentZoom; protected float _targetZoom = 1; protected float _initialWidth; protected float _initialHeight; protected float _horizMargin = 0f; protected float _vertMargin = 0f; bool _isPinching = false; float _startPinchDist; float _startPinchZoom; Vector2 _startPinchCenterPosition; Vector2 _startPinchScreenPosition; float _mouseWheelSensitivity = 1; Vector2 posFromBottomLeft; bool blockPan = false; bool mouseIsInside = false; protected override void Awake() { base.Awake(); Input.multiTouchEnabled = true; } protected override void Start() { base.Start(); if (_zoomMode == ZoomMode.RectSize) { _initialWidth = content.rect.width; _initialHeight = content.rect.height; //Debug.Log($"ScrollRect content _initialWidth: {_initialWidth}, _initialHeight: {_initialHeight}"); } } public void OnPointerEnter(PointerEventData eventData) { //Debug.Log(" MOUSE ENTER "); mouseIsInside = true; } public void OnPointerExit(PointerEventData eventData) { //Debug.Log(" MOUSE EXIT "); mouseIsInside = false; } protected virtual void Update() { if (Input.touchCount == 2) { if (!_isPinching) _isPinching = OnPinchStart(); if (_isPinching) OnPinch(); } else { _isPinching = false; if (Input.touchCount == 0) { blockPan = false; } } //pc input if (mouseIsInside) { float scrollWheelInput = Input.GetAxis("Mouse ScrollWheel"); if (Mathf.Abs(scrollWheelInput) > float.Epsilon) { _targetZoom *= 1 + scrollWheelInput * _mouseWheelSensitivity; _targetZoom = Mathf.Clamp(_targetZoom, _minZoom, _maxZoom); _startPinchScreenPosition = (Vector2)Input.mousePosition; RectTransformUtility.ScreenPointToLocalPointInRectangle(content, _startPinchScreenPosition, null, out _startPinchCenterPosition); Vector2 pivotPosition = new Vector2(content.pivot.x * content.rect.size.x, content.pivot.y * content.rect.size.y); posFromBottomLeft = pivotPosition + _startPinchCenterPosition; // Debug.Log($"posFromBottomLeft: {posFromBottomLeft} as fraction: {posFromBottomLeft.x / content.rect.width},{posFromBottomLeft.y / content.rect.height}"); //SetPivot(content, new Vector2(posFromBottomLeft.x / content.rect.width, posFromBottomLeft.y / content.rect.height)); } } //pc input end if (Mathf.Abs(_currentZoom - _targetZoom) > 0.001f) SetZoom(_targetZoom); //SetZoom(Mathf.Lerp(_currentZoom, _targetZoom, _zoomLerpSpeed * Time.deltaTime)); } public virtual void SetZoom(float z) { if (_zoomMode == ZoomMode.LocalScale) { SetPivot(content, new Vector2(posFromBottomLeft.x / content.rect.width, posFromBottomLeft.y / content.rect.height)); content.localScale = Vector3.one * z; } else if (_zoomMode == ZoomMode.RectSize) { Vector2 spcp; Vector2 pp = new Vector2(content.pivot.x * content.rect.size.x, content.pivot.y * content.rect.size.y); if (mouseIsInside) { RectTransformUtility.ScreenPointToLocalPointInRectangle(content, (Vector2)Input.mousePosition, null, out spcp); posFromBottomLeft = pp + spcp; } else { //RectTransformUtility.ScreenPointToLocalPointInRectangle(content, (transform as RectTransform).rect.center, null, out spcp); posFromBottomLeft = pp; } float curImgW = _initialWidth * _currentZoom; float newImgW = _initialWidth * z; float deltaW = newImgW - curImgW; float MX = posFromBottomLeft.x - _horizMargin; float mx = MX / curImgW; float Px = (deltaW==0.0)? content.pivot.x : (mx * newImgW - MX) / deltaW; float curImgH = _initialHeight * _currentZoom; float newImgH = _initialHeight * z; float deltaH = newImgH - curImgH; float MY = posFromBottomLeft.y - _vertMargin; float my = MY / curImgH; float Py = (deltaH == 0.0) ? content.pivot.y : (my * newImgH - MY) / deltaH; bool setPivot = true; if (Px < -2 || Px > 3) { Px = Mathf.Clamp(Px, -1f, 2f); setPivot = false; } if (Py < -2 || Py > 3) { Py = Mathf.Clamp(Py, -1f, 2f); setPivot = false; } if (setPivot) SetPivot(content, new Vector2(Px, Py)); content.sizeDelta = new Vector2(newImgW + 2*_horizMargin, newImgH + 2 * _vertMargin); } _currentZoom = z; } protected override void SetContentAnchoredPosition(Vector2 position) { if (_isPinching || blockPan) return; base.SetContentAnchoredPosition(position); } bool OnPinchStart() { Vector2 pos1 = Input.touches[0].position; Vector2 pos2 = Input.touches[1].position; if (!RectTransformUtility.RectangleContainsScreenPoint((transform as RectTransform), pos1) || !RectTransformUtility.RectangleContainsScreenPoint((transform as RectTransform), pos2)) return false; _startPinchDist = Distance(pos1, pos2) * content.localScale.x; _startPinchZoom = _targetZoom; _startPinchScreenPosition = (pos1 + pos2) / 2; RectTransformUtility.ScreenPointToLocalPointInRectangle(content, _startPinchScreenPosition, null, out _startPinchCenterPosition); Vector2 pivotPosition = new Vector3(content.pivot.x * content.rect.size.x, content.pivot.y * content.rect.size.y); Vector2 posFromBottomLeft = pivotPosition + _startPinchCenterPosition; SetPivot(content, new Vector2(posFromBottomLeft.x / content.rect.width, posFromBottomLeft.y / content.rect.height)); blockPan = true; return true; } void OnPinch() { float currentPinchDist = Distance(Input.touches[0].position, Input.touches[1].position) * content.localScale.x; _targetZoom = (currentPinchDist / _startPinchDist) * _startPinchZoom; _targetZoom = Mathf.Clamp(_targetZoom, _minZoom, _maxZoom); } float Distance(Vector2 pos1, Vector2 pos2) { RectTransformUtility.ScreenPointToLocalPointInRectangle(content, pos1, null, out pos1); RectTransformUtility.ScreenPointToLocalPointInRectangle(content, pos2, null, out pos2); return Vector2.Distance(pos1, pos2); } static void SetPivot(RectTransform rectTransform, Vector2 pivot) { if (rectTransform == null) return; Vector2 size = rectTransform.rect.size; Vector2 deltaPivot = rectTransform.pivot - pivot; Vector3 deltaPosition = new Vector3(deltaPivot.x * size.x, deltaPivot.y * size.y) * rectTransform.localScale.x; rectTransform.pivot = pivot; rectTransform.localPosition -= deltaPosition; } }