'What’s the proper way to zoom a scroll view content with pinch gesture in Unity?

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
    }
}


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source