Last active
April 2, 2025 16:25
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
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.
Oops, sorry, the shared corners array got lost. Fixed the gist.
Thank you!
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);
}
}
Also, make sure to call this 2+ frames after Awake, so the scroll view and children slots are positioned.
Thanks so much! This was exactly what I needed. 👍
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
hi! ty for the code, but i have a error
"The name 'corners' does not exist in the current context"