3

Goal: Make a general component that supports pinching to zoom on device and scroll mouse to zoom in editor. While pinching, set pivot to the middle point of two fingers, so it scales around where you pinch.

I provided my scripts in the followings. However it does not work well in scroll view on devices with two fingers pinching, when it "jumps" or jitters a lot. It works with mouse scroll in the Editor though. I think the reason is perhaps related with how the scroll view updates the layout internally. In a single frame my scripts changed the pivot and position of the content however the scroll view can not handle these changes properly. That's why I also tested my scripts using 1 LateUpdate, 2 yield WaitForEndOfFrame, 3 register call back in scroll view's OnValueChanged event, but all failed.

Does anyone know how to fix the problem in my script or any other new solutions how to make a scroll view to support pinch to zoom? Thanks!

My codes are something like this:

using System.Collections; 
using System.Collections.Generic; 
using UnityEngine; 
using UnityEngine.EventSystems;

public class PinchZoom : MonoBehaviour {
    public float zoomSpeedPinch = 0.001f;
    public float zoomSpeedMouseScrollWheel = 0.05f;
    public float zoomMin = 0.1f;
    public float zoomMax = 1f;
    RectTransform rectTransform;
    public int type = 1; // for device testing type 1 use LateUpdate; type 2 use Update

    private void Awake()
    {
        rectTransform = GetComponent<RectTransform>();
    }

    //public void OnValueChanged(Vector2 v) // test failed: called by scroll view event
    //{
    //    //Zoom();
    //}

    void Update()
    {
        //Zoom();
        if (type == 2)
        {
            if (Input.touchCount == 2)
                StartCoroutine(ZoomInTheEndOfFrame(Input.mouseScrollDelta.y, Input.touchCount, Input.GetTouch(0), Input.GetTouch(1), Input.mousePosition));
            else
                StartCoroutine(ZoomInTheEndOfFrame(Input.mouseScrollDelta.y, Input.touchCount, default(Touch), default(Touch), Input.mousePosition));
        }
    }

    private void LateUpdate()
    {
        if (type == 1) Zoom();
    }

    void Zoom()
    {
        var mouseScrollWheel = Input.mouseScrollDelta.y;
        float scaleChange = 0f;
        Vector2 midPoint = Vector2.zero;
        if (Input.touchCount == 2)
        {
            Touch touchZero = Input.GetTouch(0);
            Touch touchOne = Input.GetTouch(1);

            Vector2 touchZeroPrevPos = touchZero.position - touchZero.deltaPosition;
            Vector2 touchOnePrevPos = touchOne.position - touchOne.deltaPosition;

            float prevTouchDeltaMag = (touchZeroPrevPos - touchOnePrevPos).magnitude;
            float touchDeltaMag = (touchZero.position - touchOne.position).magnitude;

            float deltaMagnitudeDiff = touchDeltaMag - prevTouchDeltaMag;

            scaleChange = deltaMagnitudeDiff * zoomSpeedPinch;

            midPoint = (touchOne.position + touchZero.position) / 2;
        }

        if (mouseScrollWheel != 0)
        {
            scaleChange = mouseScrollWheel * zoomSpeedMouseScrollWheel;
            midPoint = Input.mousePosition;
        }

        if (scaleChange != 0)
        {
            var scaleX = transform.localScale.x;
            scaleX += scaleChange;
            scaleX = Mathf.Clamp(scaleX, zoomMin, zoomMax);
            var size = rectTransform.rect.size;
            size.Scale(rectTransform.localScale);
            var parentRect = ((RectTransform)rectTransform.parent);
            var parentSize = parentRect.rect.size;
            parentSize.Scale(parentRect.localScale);
            if (size.x > parentSize.x && size.y > parentSize.y)
            {
                var p1 = Camera.main.ScreenToWorldPoint(midPoint); 
                var p2 = transform.InverseTransformPoint(p1); 
                var pivotP = rectTransform.pivot * rectTransform.rect.size; 
                var p3 = (Vector2)p2 + pivotP; 
                var newPivot = p3 / rectTransform.rect.size;
                newPivot = new Vector2(Mathf.Clamp01(newPivot.x), Mathf.Clamp01(newPivot.y));
                rectTransform.SetPivot(newPivot);
            }
            else
            {
                rectTransform.SetPivot(new Vector2(0.5f, 0.5f));
            }

            transform.localScale = new Vector3(scaleX, scaleX, transform.localScale.z);
        }
    }

    //private IEnumerator ZoomInTheEndOfFrame(float mouseScrollWheel, int touchCount, Touch touchZero, Touch touchOne, Vector3 mousePosition) // testing failed
    //{
    //    yield return new WaitForEndOfFrame();
    //    ZoomWithData(mouseScrollWheel, touchCount, touchZero, touchOne, mousePosition);
    //}

}

For changing pivot without get the image "jumped" I used an extension script:

using UnityEngine;

public static class RectTranformExtension
{
    /// <summary>
    /// Set pivot without changing the position of the element
    /// </summary>
    public static void SetPivot(this RectTransform rectTransform, Vector2 pivot)
    {
        Vector3 deltaPosition = rectTransform.pivot - pivot;    // get change in pivot
        deltaPosition.Scale(rectTransform.rect.size);           // apply sizing
        deltaPosition.Scale(rectTransform.localScale);          // apply scaling
        deltaPosition = rectTransform.rotation * deltaPosition; // apply rotation

        rectTransform.pivot = pivot;                            // change the pivot
        rectTransform.localPosition -= deltaPosition;           // reverse the position change
    }
}
Ruzihm
  • 19,749
  • 5
  • 36
  • 48
ArtS
  • 1,730
  • 1
  • 20
  • 43
  • I did a similar thing once for AR. Instead of f*cking around with pivots and the scrollrect I simply used [this `ScaleAround`](http://answers.unity.com/answers/1527837/view.html) and directly moved and scaled the `Content` object via its `transform` component. The `ScrollRect` was updated accordingly automaticly... – derHugo Nov 18 '19 at 08:07
  • @derHugo Thank you for your information. I did some test with the linked codes you provided, however it's not what I wanted or maybe I used it wrong. What I got using your code is the content still scales around the original pivot(0.5, 0.5), not the middle of my two fingers. Could you explain more on this please? In my case what should be passed to Transform target and what to Transform pivot? P.S. The scripts in my questions are also supposed to be used on the Content object. – ArtS Nov 18 '19 at 08:50
  • I changed the code actually to take `Transform target, Vector3 pivot` as parameters. They way the poster there used it was scaling the target around another pivot transform. – derHugo Nov 18 '19 at 09:14
  • I don't think it works in my case. I also tried to create a new empty object, put it in the middle point of the two fingers, then use it as target for scaling the content. Still it scales around its original pivot position, rather than where the new empty object is. – ArtS Nov 18 '19 at 10:19

1 Answers1

2

After working on this for days, I finally solved it. The solution it is to call Zoom() in OnDrag(), and block the scollView component to receive dragging related events while zooming. If you want to use it, just copy my codes under, do not do any changes. This is what I got after tons of testing on device. There're some minor potential gotcha with too many details that I don't want to explain, just copy and use it. And also I suggest to use a speed of current content.scale.x * 0.001

Codes:

// please note that scrollRect is the component on the scroll view game object, not where this script is

public void OnDrag(PointerEventData eventData)
{
    Zoom();
    if (Input.touchCount <= 1) scrollRect.OnDrag(eventData);
}

public void OnEndDrag(PointerEventData eventData)
{
    scrollRect.OnEndDrag(eventData);
}

public void OnBeginDrag(PointerEventData eventData)
{
    if (Input.touchCount <= 1) scrollRect.OnBeginDrag(eventData);
}

P.S. In the original script, there's a part that if a check for boundary is not passed then set pivot to (0.5,0.5). This part should be commented out(do not need to change pivot if the check is not passed ).

One more thing, you can use whatever parent-children relations you want, but you must make sure the object that receive the onDrag events has a whole image to catch the events.

ArtS
  • 1,730
  • 1
  • 20
  • 43