Search Unity

How to position ScrollRect to another item?

Discussion in 'UGUI & TextMesh Pro' started by LastGecko, Sep 16, 2014.

  1. AverageProg

    AverageProg

    Joined:
    Jun 25, 2015
    Posts:
    38
    I am not using anybody's code, and we shouldn't even reference to anyone's code just to not make another mess out of this.

    And sorry if My post is kinda criptic, It takes someone else to see where you did wrong, and my var naming could have been better.

    The containerHeight is the scrollRect rect.height, the parent of your scrollable items.
    The panel height in my case is the whole canvas height
    (but you could have a panel that holds the scroll rect so in that case we use that, I haven't tested that out, but it works in my head, which is no guarantee ofc)

    ItemPosition is indeed the position of the scrollRect item you wish to center on.
    So currentChild.rectTransform.localposition.y.

    I think I could simplify the above by using a relative position (itempos and total number of items) but it works like this, and I don't see why optimize something that doesn't even show up in the profiler.

    the normalized position is the ScrollRect.verticalNormalizedPosition.

    I don't really get this, or maybe I should take a coffee since I just woke up =(

    Ok I just read the code of Lan14n, and I refuse to even try to understand that Oo

    No need for conditionals, the working algorithm is 2 lines of code.
    If you need further assistance I can post you the code in a pm, although really I would advise getting there yourself.

    Note that to center the items that algorithm is incomplete, but after you get the formula it will all be much clearer.
     
    Last edited: Jul 1, 2015
  2. thylaxene

    thylaxene

    Joined:
    Oct 10, 2005
    Posts:
    716
    Thanks @ZZantal for injecting some sanity into this. Was hitting my head against a wall thinking there must be easier way of doing this. Your pseudo code nailed it.

     
  3. Nub3h

    Nub3h

    Joined:
    Aug 23, 2012
    Posts:
    56

    That pseudo code assumes that all the child in the scrollview have the same height, which is not true in my case.
     
  4. AverageProg

    AverageProg

    Joined:
    Jun 25, 2015
    Posts:
    38
    And how would you change it to fit it your scenario?

    Edit: Apart from the fact that it doesn't use the element height, you need to know its position.
     
  5. DottorFeelgood

    DottorFeelgood

    Joined:
    Jan 5, 2015
    Posts:
    5
    Check this out!

     
  6. xucian

    xucian

    Joined:
    Mar 7, 2016
    Posts:
    846
    Looks cool, but for large data sets maybe you'll want to optimize a little. check this
     
  7. petrapparent

    petrapparent

    Joined:
    Jun 6, 2011
    Posts:
    28
    // Here's my take. Scrolls the selected object (in target_gobj) to the top of the scrollview
    // This code should be called via update () with target_gobj set to the element to move
    // to the top
    //
    // obj_rect is the RectTransform of target_gobj
    // content_rect is the RectTransform of the scrollview content panel
    // scroll_rect is the RectTransform of the scrollview
    // h_scrollbar_rect is the RectTransform of the horizontal scrollbar of the scrollview (optional)
    // scroll_speed is the quickness of the repositioning. Lower number is slower (e.g. 5)

    float scroll_percentage = -1*(obj_rect.localPosition.y + obj_rect.rect.height);
    scroll_percentage /= content_rect.rect.height - scroll_rect.rect.height + h_scrollbar_rect.rect.height;
    float y_normalized_position = 1 - scroll_percentage - (scroll_percentage / content_rect.rect.height);

    scroll_rect.verticalNormalizedPosition = Mathf.Lerp (scroll_rect.verticalNormalizedPosition,
    y_normalized_position,
    scroll_speed * Time.deltaTime);​

    // If target position reached, zero out the target object accurate to several decimal places
    if (System.Math.Abs (g_scroll_rect.verticalNormalizedPosition - y_normalized_position) < 0.00001 )
    {
    target_gobj = null;​
    }
     
    Last edited: Mar 28, 2017
  8. templewulf

    templewulf

    Joined:
    Dec 29, 2013
    Posts:
    52
    How is everyone getting the target object to the scrolling management script? For some reason, my buttons' OnSelect methods are not getting called. I tried throwing in breakpoints and Debug.Log, but nothing ever happens.

    For reference, I'm dynamically populating the buttons with Instantiate, so I'm not sure if that makes a difference to the Event System.
     
  9. _Adriaan

    _Adriaan

    Joined:
    Nov 12, 2009
    Posts:
    481
    Hey everyone,

    Here's the function I wrote, which:
    - only works with vertical scrolls.
    - can only take direct RectTransforms children of the 'Content' transform as a parameter.
    - doesn't assume all RectTransforms inside content have the same height.
    - assumes all RectTransforms inside content have their anchor point at the top.

    Code (CSharp):
    1.     private void ScrollToChildOfContent(RectTransform rectToScrollTo)
    2.     {
    3.         float scrollViewHeight = scroll.viewport.sizeDelta.y;
    4.         float scrollViewMid = -scrollViewHeight / 2f;
    5.      
    6.         if(rectToScrollTo.anchoredPosition.y > scrollViewMid)
    7.         {
    8.             //Debug.Log("the rect is in the first half of the scroll view: no need to scroll");
    9.          
    10.             return;
    11.         }
    12.      
    13.         float contentEnd = -scroll.content.sizeDelta.y;
    14.      
    15.         if(rectToScrollTo.anchoredPosition.y < contentEnd - scrollViewMid)
    16.         {
    17.             //Debug.Log("the rect is at the very bottom of the content, so just scroll there");
    18.          
    19.             scroll.verticalNormalizedPosition = 0f;
    20.          
    21.             return;
    22.         }
    23.      
    24.         float normPosTopToBottom = Mathf.InverseLerp(scrollViewMid, contentEnd - scrollViewMid, rectToScrollTo.anchoredPosition.y);
    25.         float normPosBottomToTop = 1f - normPosTopToBottom;
    26.      
    27.         scroll.verticalNormalizedPosition = normPosBottomToTop;
    28.     }
     
    r35 likes this.
  10. ferretnt

    ferretnt

    Joined:
    Apr 10, 2012
    Posts:
    412
    After reading this thread, and after banging our heads against Unity UI for 18 months, we've gone back to using NGUI.
     
  11. YondernautsGames

    YondernautsGames

    Joined:
    Nov 24, 2014
    Posts:
    353
    Hey all. I'm late to the party, I know, but I recently knocked out a similar thing. It's probably not the most efficient implementation since it uses GetWorldCorners() and it could do with some bomb proofing, but it doesn't have any restrictions on anchors, etc. It does only work with vertical scroll since that's what I needed, but it wouldn't be hard to extend to horizontal too.

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.UI;
    3.  
    4. public class SlightlyEnhancedScrollRect : ScrollRect
    5. {
    6.     private Vector3[] m_Corners = new Vector3[4];
    7.  
    8.     public void ShowChild (RectTransform showRect)
    9.     {
    10.         // Check if content is large enough to require scrolling
    11.         RectTransform rt = transform as RectTransform;
    12.         if (content.rect.height > rt.rect.height)
    13.         {
    14.             rt.GetWorldCorners (m_Corners);
    15.  
    16.             float worldBottom = m_Corners [0].y;
    17.             float worldTop = m_Corners [1].y;
    18.             float worldHeight = worldTop - worldBottom;
    19.             float worldOversize = (content.rect.height / content.lossyScale.y) - worldHeight;
    20.  
    21.             showRect.GetWorldCorners (m_Corners);
    22.  
    23.             // Check if off bottom of scroll rect
    24.             if (m_Corners [0].y < worldBottom)
    25.             {
    26.                 float diff = worldBottom - m_Corners [0].y;
    27.                 Vector2 tempNormPos = normalizedPosition;
    28.                 tempNormPos.y -= diff / worldOversize;
    29.                 normalizedPosition = tempNormPos;
    30.             }
    31.             else
    32.             {
    33.                 // Check if off top of scroll rect
    34.                 if (m_Corners [1].y > worldTop)
    35.                 {
    36.                     float diff = m_Corners [1].y - worldTop;
    37.                     Vector2 tempNormPos = normalizedPosition;
    38.                     tempNormPos.y += diff / worldOversize;
    39.                     normalizedPosition = tempNormPos;
    40.                 }
    41.             }
    42.         }
    43.     }
    44. }
    45.  
     
  12. cradiff

    cradiff

    Joined:
    Feb 7, 2015
    Posts:
    66
    It took me an hour before I found this code snippet. It work really well for me

    Code (CSharp):
    1. protected ScrollRect scrollRect;
    2. protected RectTransform contentPanel;
    3.  
    4. public void SnapTo(RectTransform target)
    5.     {
    6.         Canvas.ForceUpdateCanvases();
    7.  
    8.         contentPanel.anchoredPosition =
    9.             (Vector2)scrollRect.transform.InverseTransformPoint(contentPanel.position)
    10.             - (Vector2)scrollRect.transform.InverseTransformPoint(target.position);
    11.     }
    If you only need the horizontal then use this code

    Code (csharp):
    1.  
    2. private void _snapToHorizontal(RectTransform target)
    3.     {
    4.         Canvas.ForceUpdateCanvases();
    5.         Vector2 pos =
    6.             (Vector2)stageScrollRect.transform.InverseTransformPoint(contentRect.position)
    7.             - (Vector2)stageScrollRect.transform.InverseTransformPoint(target.position);
    8.         pos.y = contentRect.anchoredPosition.y;
    9.         contentRect.anchoredPosition = pos;
    10.     }
    11.  
    12.  
     
  13. MathiasBlank

    MathiasBlank

    Joined:
    May 27, 2018
    Posts:
    3
    Hi guys,

    Here is my solution for snap a element (horizontaly and verticaly) to the center of a scrollview. I used it for center my levels in my map.

    Free to you to adapt it to you game ;)

    Code (CSharp):
    1.     protected ScrollRect scrollRect;
    2.     protected RectTransform contentPanel;
    3.     protected RectTransform scrollTransform;
    4.  
    5.     private void Awake() {
    6.         GameObject myMap = GameObject.Find("MapSW"); // - Set the name of your scrollView element or use the inspector to pass it through a public variable
    7.         scrollRect = myMap.GetComponent<ScrollRect>();
    8.         scrollTransform = myMap.GetComponent<RectTransform>();
    9.         contentPanel = scrollRect.content;
    10.     }
    11.  
    12.     public IEnumerator CenterMapTo(RectTransform target) {
    13.  
    14.         Vector2 tPos = target.position;  // - In that case target equal to my level UI element
    15.  
    16.         // - Calculate difference between target and map center
    17.         float w = scrollTransform.rect.size.x;
    18.         float h = scrollTransform.rect.size.y;
    19.         Vector2 mapCenter = new Vector2(w / 2, h / 2);
    20.         Vector3 diff = mapCenter - tPos;
    21.  
    22.         // - The limits will be managed by Unity elastic movement.
    23.         scrollRect.movementType = ScrollRect.MovementType.Elastic;
    24.         scrollRect.elasticity = 0.00000000000001f;
    25.  
    26.         // - Set new position
    27.         contentPanel.position = contentPanel.position + diff;
    28.  
    29.         Canvas.ForceUpdateCanvases(); // - better to use when manipulate scrollView by code
    30.  
    31.         // - Let a few time for reposition the map in limits
    32.         yield return new WaitForSeconds(0.1f);
    33.  
    34.         scrollRect.movementType = ScrollRect.MovementType.Clamped; // - In my case my map is campled but you can store movement type before and reset it here
    35.  
    36.     }
     
  14. Adrian

    Adrian

    Joined:
    Apr 5, 2008
    Posts:
    1,065
    The source code of the ScrollRect is publicly available here:
    https://bitbucket.org/Unity-Technol...ea2c500f/UnityEngine.UI/UI/Core/ScrollRect.cs

    Looking at SetNormalizedPosition in the source, it's clear the the scroll position is normalized over how much the ScrollRect's viewport cuts off from the content. I.e. if the content is 20% larger than the viewport, the normalized scroll position will be multiplied by that 20% and all calculations are done in the viewport's coordinate space (or the ScrollRect's own space if viewport is null).

    I used the source as a starting point to implement another version of this method. Since it's based on the actual source and uses the same calculations, it should produce the least surprises.

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using UnityEngine.UI;
    4.  
    5. public static class UIExtensions {
    6.     /// <summary>
    7.     /// Transform the bounds of the current rect transform to the space of another transform.
    8.     /// </summary>
    9.     /// <param name="source">The rect to transform</param>
    10.     /// <param name="target">The target space to transform to</param>
    11.     /// <returns>The transformed bounds</returns>
    12.     public static Bounds TransformBoundsTo(this RectTransform source, Transform target)
    13.     {
    14.         // Based on code in ScrollRect's internal GetBounds and InternalGetBounds methods
    15.         var bounds = new Bounds();
    16.         if (source != null) {
    17.             source.GetWorldCorners(corners);
    18.  
    19.             var vMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
    20.             var vMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
    21.  
    22.             var matrix = target.worldToLocalMatrix;
    23.             for (int j = 0; j < 4; j++) {
    24.                 Vector3 v = matrix.MultiplyPoint3x4(corners[j]);
    25.                 vMin = Vector3.Min(v, vMin);
    26.                 vMax = Vector3.Max(v, vMax);
    27.             }
    28.  
    29.             bounds = new Bounds(vMin, Vector3.zero);
    30.             bounds.Encapsulate(vMax);
    31.         }
    32.         return bounds;
    33.     }
    34.  
    35.     /// <summary>
    36.     /// Normalize a distance to be used in verticalNormalizedPosition or horizontalNormalizedPosition.
    37.     /// </summary>
    38.     /// <param name="axis">Scroll axis, 0 = horizontal, 1 = vertical</param>
    39.     /// <param name="distance">The distance in the scroll rect's view's coordiante space</param>
    40.     /// <returns>The normalized scoll distance</returns>
    41.     public static float NormalizeScrollDistance(this ScrollRect scrollRect, int axis, float distance)
    42.     {
    43.         // Based on code in ScrollRect's internal SetNormalizedPosition method
    44.         var viewport = scrollRect.viewport;
    45.         var viewRect = viewport != null ? viewport : scrollRect.GetComponent<RectTransform>();
    46.         var viewBounds = new Bounds(viewRect.rect.center, viewRect.rect.size);
    47.  
    48.         var content = scrollRect.content;
    49.         var contentBounds = content != null ? content.TransformBoundsTo(viewRect) : new Bounds();
    50.  
    51.         var hiddenLength = contentBounds.size[axis] - viewBounds.size[axis];
    52.         return distance / hiddenLength;
    53.     }
    54.  
    55.     /// <summary>
    56.     /// Scroll the target element to the vertical center of the scroll rect's viewport.
    57.     /// Assumes the target element is part of the scroll rect's contents.
    58.     /// </summary>
    59.     /// <param name="scrollRect">Scroll rect to scroll</param>
    60.     /// <param name="target">Element of the scroll rect's content to center vertically</param>
    61.     public static void ScrollToCeneter(this ScrollRect scrollRect, RectTransform target)
    62.     {
    63.         // The scroll rect's view's space is used to calculate scroll position
    64.         var view = scrollRect.viewport != null ? scrollRect.viewport : scrollRect.GetComponent<RectTransform>();
    65.  
    66.         // Calcualte the scroll offset in the view's space
    67.         var viewRect = view.rect;
    68.         var elementBounds = target.TransformBoundsTo(view);
    69.         var offset = viewRect.center.y - elementBounds.center.y;
    70.  
    71.         // Normalize and apply the calculated offset
    72.         var scrollPos = scrollRect.verticalNormalizedPosition - scrollRect.NormalizeScrollDistance(1, offset);
    73.         scrollRect.verticalNormalizedPosition = Mathf.Clamp(scrollPos, 0f, 1f);
    74.     }
    75. }
    76.  
    Also available as a Gist: https://gist.github.com/sttz/c406aec3ace821738ecd4fa05833d21d
     
    ilyagutnikov and FernandoHC like this.
  15. PandaArcade

    PandaArcade

    Joined:
    Jan 2, 2017
    Posts:
    128
    Sigh... another one of those simple things Unity UI should do so we can spend more time making fun games! :(
     
    Razputin likes this.
  16. Anveena

    Anveena

    Joined:
    Apr 28, 2018
    Posts:
    1

    Thank you very much, it working ;)
     
  17. Wolfderic

    Wolfderic

    Joined:
    May 9, 2019
    Posts:
    7
    In my case I've solved the problem in this intuitive way.

    Code (CSharp):
    1.  private Vector3 GetCenteredContentPosition(RectTransform child, ScrollRect scrollRect) {
    2.         Vector3[] viewportCorners = new Vector3[4];
    3.         RectTransform viewport = scrollRect.viewport;
    4.         viewport = viewport != null ? viewport : (RectTransform) scrollRect.transform;
    5.         viewport.GetWorldCorners(viewportCorners);
    6.         Vector3 centreWorldPos = ((viewportCorners[1] - viewportCorners[0]) / 2f) + viewportCorners[0];
    7.         float h = centreWorldPos.y - child.position.y;
    8.         Vector3 displacement = new Vector3(0, h, 0);
    9.         Vector3[] contentCorners = new Vector3[4];
    10.         scrollRect.content.GetWorldCorners(contentCorners);
    11.        
    12.         if (contentCorners[1].y + displacement.y < viewportCorners[1].y) {
    13.             displacement.y = viewportCorners[1].y - contentCorners[1].y;
    14.         } else if (contentCorners[0].y + displacement.y > viewportCorners[0].y) {
    15.             displacement.y = viewportCorners[0].y - contentCorners[0].y;
    16.         }
    17.         return scrollRect.content.position + displacement;
    18.     }
     
  18. DoomGoober

    DoomGoober

    Joined:
    Dec 12, 2013
    Posts:
    7
    In 2019, this seem easier than previously. @Adrian points out the key point: horizontalNormalizedPosition(0, 1) is normalized to contentLeftEdge (0, (scrollRectWidth - contentWidth)). So if content is 5 wide and the scrollRect is 2 wide then horizontalNormalizedPosition = 0 means the left edge of contents is at 0. horizontalNormalizedPosition = 1 means the left edge of contents is at -3. The importance of the 3 value is that it's the difference between the width of the content and the scrollRect.

    With that in mind, the code to scroll the scrollRect so the nth item is showing on the far left is very simple:
    Code (CSharp):
    1.         float itemX = items[selectedIndex].GetComponent<RectTransform>().localPosition.x;
    2.         float contentWidth = scrollRect.content.rect.width;
    3.         float scrollRectWidth = scrollRect.GetComponent<RectTransform>().rect.width;
    4.         float difference = contentWidth - scrollRectWidth;
    5.         scrollRect.horizontalNormalizedPosition = itemX / difference;
    "items" is an array or list of items in the "content" with each item having a (0,0) pivot. "selectedIndex" in the index of the item whose left edge want to line up with the left edge of the scrollRect. In my working example, the items are just members of a HorizontalLayoutGroup with a ContentSizeFitter. The "scrollRect" should be set to "Unrestricted" or else scrolling to the end items will just scroll as far as possible.
     
    Last edited: Mar 15, 2020
    ilyagutnikov likes this.
  19. jason_yak

    jason_yak

    Joined:
    Aug 25, 2016
    Posts:
    531
    Here's yet another solution which is much simpler. It centers an item horizontally. I'm not bothering to use the normalised positioning which over complicates things.

    Code (CSharp):
    1.  
    2. public ScrollRect ScrollRect;
    3. public RectTransform Child; // obviously replacing this with however you target the item you want to center
    4.  
    5. // horizontally center (in update event)
    6. var content = ScrollRect.content;
    7. var scrollWidth = (ScrollRect.transform as RectTransform).rect.width;
    8. var center = ( 0f - Child.localPosition.x ) + ( scrollWidth * 0.5f );
    9. var x = Mathf.Clamp( center, -( content.rect.width - scrollWidth ), 0f );
    10. content.anchoredPosition = Vector2.Lerp( content.anchoredPosition, new Vector2( x, content.anchoredPosition.y ), 6f * Time.deltaTime );
    note: this assumes the child object you want to center is using a 0.5 pivot on the x, if you're using something else like 0.0 you'll need to adjust accordingly to factor in half the width of the child item.
     
    Last edited: Aug 26, 2021
    rubicogames likes this.
  20. shujatech

    shujatech

    Joined:
    Jan 15, 2020
    Posts:
    1
    Simply Do that

    private void _snapToHorizontal(RectTransform target)
    {
    Canvas.ForceUpdateCanvases();
    Vector2 pos =
    (Vector2)mScrollRect.transform.InverseTransformPoint(mContent.position)
    - (Vector2)mScrollRect.transform.InverseTransformPoint(target.position);
    pos.y = mContent.anchoredPosition.y;
    pos.x += xOffset;

    mContent.DOAnchorPos(pos, _AnimTime);
    }
     
  21. _eternal

    _eternal

    Joined:
    Nov 25, 2014
    Posts:
    304
    I really struggled with this for some reason. My situation ended up being more complicated because I had to place the item at the bottom of the viewport rather than the center (like scrolling to a specific text message on a phone).

    If it's helpful to anyone, I eventually figured it out by using world space (thereby sidestepping some annoying problems with non-centered pivots) and by using Mathf.InverseLerp to handle the normalization (I already had an extension method for this).

    Code (CSharp):
    1.  
    2. public void ScrollToMessageInstant()
    3. {
    4.     float desiredPos = itemRect.BottomWorld().y;
    5.     float lowestPossiblePos = contentRect.BottomWorld().y;
    6.     float highestPossiblePos = contentRect.TopWorld().y - viewportRect.WorldSize().y;
    7.  
    8.     float relativePos;
    9.  
    10.     //this means the content is smaller than the viewport, so scrolling is irrelevant
    11.     if (highestPossiblePos < lowestPossiblePos)
    12.     {
    13.         relativePos = 1f;
    14.     }
    15.     else
    16.     {
    17.         relativePos = desiredPos.Normalized(lowestPossiblePos, highestPossiblePos);
    18.     }
    19.  
    20.     chatScroll.verticalNormalizedPosition = relativePos;
    21. }
    22.  
    23. /// <summary>
    24. /// Returns this float normalized to a range of 0-1.
    25. /// </summary>
    26. public static float Normalized(this float value, float min, float max)
    27. {
    28.     return Mathf.InverseLerp(min, max, value);
    29. }
    30.  
    31. public static Vector2 WorldSize(this RectTransform rt)
    32. {
    33.     Vector3[] corners = new Vector3[4];
    34.     rt.GetWorldCorners(corners);
    35.     //get the bottom left corner.
    36.     Vector3 position = corners[0];
    37.     Vector2 size = new Vector2(
    38.         rt.lossyScale.x * rt.rect.size.x,
    39.         rt.lossyScale.y * rt.rect.size.y);
    40.     return new Rect(position, size).size;
    41. }
    42.  
    It would take a bit more copy-paste to get BottomWorld and TopWorld, but it's basically just

    Code (CSharp):
    1. rectTransform.TransformPoint(new Vector2(rectTransform.rect.center.x, rectTransform.rect.yMin));
    This probably could be done more efficiently or intelligently, but I'll just leave it here for anyone who has a non-standard use case like I did.
     
    Last edited: Feb 1, 2024