Skip to content

Instantly share code, notes, and snippets.

@sttz
Last active November 4, 2023 05:10
Show Gist options
  • Star 34 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sttz/c406aec3ace821738ecd4fa05833d21d to your computer and use it in GitHub Desktop.
Save sttz/c406aec3ace821738ecd4fa05833d21d to your computer and use it in GitHub Desktop.
Method to center an element in a ScrollRect using Unity's new UI system
using UnityEngine;
using UnityEngine.UI;
public static class UIExtensions {
// Shared array used to receive result of RectTransform.GetWorldCorners
static Vector3[] corners = new Vector3[4];
/// <summary>
/// Transform the bounds of the current rect transform to the space of another transform.
/// </summary>
/// <param name="source">The rect to transform</param>
/// <param name="target">The target space to transform to</param>
/// <returns>The transformed bounds</returns>
public static Bounds TransformBoundsTo(this RectTransform source, Transform target)
{
// Based on code in ScrollRect's internal GetBounds and InternalGetBounds methods
var bounds = new Bounds();
if (source != null) {
source.GetWorldCorners(corners);
var vMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
var vMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
var matrix = target.worldToLocalMatrix;
for (int j = 0; j < 4; j++) {
Vector3 v = matrix.MultiplyPoint3x4(corners[j]);
vMin = Vector3.Min(v, vMin);
vMax = Vector3.Max(v, vMax);
}
bounds = new Bounds(vMin, Vector3.zero);
bounds.Encapsulate(vMax);
}
return bounds;
}
/// <summary>
/// Normalize a distance to be used in verticalNormalizedPosition or horizontalNormalizedPosition.
/// </summary>
/// <param name="axis">Scroll axis, 0 = horizontal, 1 = vertical</param>
/// <param name="distance">The distance in the scroll rect's view's coordiante space</param>
/// <returns>The normalized scoll distance</returns>
public static float NormalizeScrollDistance(this ScrollRect scrollRect, int axis, float distance)
{
// Based on code in ScrollRect's internal SetNormalizedPosition method
var viewport = scrollRect.viewport;
var viewRect = viewport != null ? viewport : scrollRect.GetComponent<RectTransform>();
var viewBounds = new Bounds(viewRect.rect.center, viewRect.rect.size);
var content = scrollRect.content;
var contentBounds = content != null ? content.TransformBoundsTo(viewRect) : new Bounds();
var hiddenLength = contentBounds.size[axis] - viewBounds.size[axis];
return distance / hiddenLength;
}
/// <summary>
/// Scroll the target element to the vertical center of the scroll rect's viewport.
/// Assumes the target element is part of the scroll rect's contents.
/// </summary>
/// <param name="scrollRect">Scroll rect to scroll</param>
/// <param name="target">Element of the scroll rect's content to center vertically</param>
public static void ScrollToCeneter(this ScrollRect scrollRect, RectTransform target)
{
// The scroll rect's view's space is used to calculate scroll position
var view = scrollRect.viewport != null ? scrollRect.viewport : scrollRect.GetComponent<RectTransform>();
// Calcualte the scroll offset in the view's space
var viewRect = view.rect;
var elementBounds = target.TransformBoundsTo(view);
var offset = viewRect.center.y - elementBounds.center.y;
// Normalize and apply the calculated offset
var scrollPos = scrollRect.verticalNormalizedPosition - scrollRect.NormalizeScrollDistance(1, offset);
scrollRect.verticalNormalizedPosition = Mathf.Clamp(scrollPos, 0f, 1f);
}
}
@giuz09
Copy link

giuz09 commented Jan 26, 2019

hi! ty for the code, but i have a error
"The name 'corners' does not exist in the current context"

@KWaldt
Copy link

KWaldt commented Mar 26, 2019

Code works if you add this to TransformBoundsTo (before "source.GetWorldCorners(corners);"):
Vector3[] corners = new Vector3[4];

Thank you for the code! It was very helpful.

@sttz
Copy link
Author

sttz commented Apr 9, 2019

Oops, sorry, the shared corners array got lost. Fixed the gist.

@WereWolfACE
Copy link

Thank you!

@furic
Copy link

furic commented May 13, 2020

For anyone that want horizontal center too, here's our code:

		public static void ScrollToCenter(this ScrollRect scrollRect, RectTransform target, RectTransform.Axis axis = RectTransform.Axis.Vertical)
		{
			// The scroll rect's view's space is used to calculate scroll position
			var view = scrollRect.viewport ?? scrollRect.GetComponent<RectTransform>();

			// Calcualte the scroll offset in the view's space
			var viewRect = view.rect;
			var elementBounds = target.TransformBoundsTo(view);

			// Normalize and apply the calculated offset
			if (axis == RectTransform.Axis.Vertical) {
				var offset = viewRect.center.y - elementBounds.center.y;
				var scrollPos = scrollRect.verticalNormalizedPosition - scrollRect.NormalizeScrollDistance(1, offset);
				scrollRect.verticalNormalizedPosition = Mathf.Clamp(scrollPos, 0, 1);
			} else {
				var offset = viewRect.center.x - elementBounds.center.x;
				var scrollPos = scrollRect.horizontalNormalizedPosition - scrollRect.NormalizeScrollDistance(0, offset);
				scrollRect.horizontalNormalizedPosition = Mathf.Clamp(scrollPos, 0, 1);
			}
		}

@furic
Copy link

furic commented May 13, 2020

Also, make sure to call this 2+ frames after Awake, so the scroll view and children slots are positioned.

@calebjacob
Copy link

Thanks so much! This was exactly what I needed. 👍

@lklejnberg
Copy link

lklejnberg commented May 21, 2022

Hey, thanks a lot, and this can be helpfully for people looking to smooth scroll.

public static class ScrollToCenterHelper
    {
        private const int MaxCornersCount = 4;
        private const float ScrollTimeStep = 0.25f;
        private const int MaxScrollTimeSec = 2;

        private static readonly Vector3[] _corners = new Vector3[MaxCornersCount];
        private static readonly WaitForEndOfFrame _waitForEndOfFrame = new WaitForEndOfFrame();
        private static Coroutine _coroutine;

        public static void ScrollToCenter(this ScrollRect scrollRect, RectTransform target, MonoBehaviour monoBehaviour)
        {
            // The scroll rect's view's space is used to calculate scroll position
            var view = scrollRect.viewport != null ? scrollRect.viewport : scrollRect.GetComponent<RectTransform>();

            // Calcualte the scroll offset in the view's space
            var viewRect = view.rect;
            var elementBounds = target.TransformBoundsTo(view);
            var offset = viewRect.center.y - elementBounds.center.y;

            // Normalize and apply the calculated offset
            var scrollPos = scrollRect.verticalNormalizedPosition - scrollRect.NormalizeScrollDistance(1, offset);

            if (_coroutine != null)
            {
                monoBehaviour.StopCoroutine(_coroutine);
            }

            _coroutine = monoBehaviour.StartCoroutine(VerticalNormalizedPositionSmooth(scrollRect, Mathf.Clamp(scrollPos, 0f, 1f)));
        }

        private static Bounds TransformBoundsTo(this RectTransform source, Transform target)
        {
            var bounds = new Bounds();

            if (source != null)
            {
                source.GetWorldCorners(_corners);

                var vMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
                var vMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);

                var matrix = target.worldToLocalMatrix;

                for (int j = 0; j < MaxCornersCount; j++)
                {
                    Vector3 v = matrix.MultiplyPoint3x4(_corners[j]);
                    vMin = Vector3.Min(v, vMin);
                    vMax = Vector3.Max(v, vMax);
                }

                bounds = new Bounds(vMin, Vector3.zero);
                bounds.Encapsulate(vMax);
            }

            return bounds;
        }

        private static float NormalizeScrollDistance(this ScrollRect scrollRect, int axis, float distance)
        {
            var viewport = scrollRect.viewport;
            var viewRect = viewport != null ? viewport : scrollRect.GetComponent<RectTransform>();
            var viewBounds = new Bounds(viewRect.rect.center, viewRect.rect.size);

            var content = scrollRect.content;
            var contentBounds = content != null ? content.TransformBoundsTo(viewRect) : new Bounds();

            var hiddenLength = contentBounds.size[axis] - viewBounds.size[axis];

            return distance / hiddenLength;
        }

        private static IEnumerator VerticalNormalizedPositionSmooth(ScrollRect scrollRect, float position)
        {
            var maxTime = DateTime.Now.AddSeconds(MaxScrollTimeSec).Second;

            while (true)
            {
                scrollRect.verticalNormalizedPosition = Mathf.Lerp(scrollRect.verticalNormalizedPosition, position, ScrollTimeStep);

                yield return _waitForEndOfFrame;

                var pos1 = Mathf.Round(scrollRect.verticalNormalizedPosition * 1000.0f) * 0.001f;
                var pos2 = Mathf.Round(position * 1000.0f) * 0.001f;

                if (pos1 == pos2 || maxTime <= DateTime.Now.Second)
                {
                    scrollRect.verticalNormalizedPosition = position;

                    yield break;
                }
            }
        }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment