Search Unity

Scroll to the bottom of a Scrollrect in code

Discussion in 'UGUI & TextMesh Pro' started by Richbk, Mar 15, 2015.

  1. Richbk

    Richbk

    Joined:
    May 26, 2014
    Posts:
    3
    I'm using a ScrollRect and when I add an item to it, I want the ScrollRect to scroll down so that the latest item is visible at the bottom. (Imagine a chat box).

    I've found the code to do it:

    Code (csharp):
    1. GetComponent<ScrollRect>().verticalNormalizedPosition = 0;
    This doesn't seem to work though unless I put it in Update which is fine but it prevents scrolling back up the box when the user wants to, to see earlier items.

    I'm guessing it's something to do with calling it between frames?

    Any advice would be greatly appreciated. Thanks.
     
    Madsen likes this.
  2. Feaver1968

    Feaver1968

    Joined:
    Nov 16, 2014
    Posts:
    70
    I have a little extension method that I use for scrolling to top. It should work for scrolling to bottom.
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.UI;
    3. public static class ScrollRectExtensions
    4. {
    5.     public static void ScrollToTop(this ScrollRect scrollRect)
    6.     {
    7.         scrollRect.normalizedPosition = new Vector2(0, 1);
    8.     }
    9.     public static void ScrollToBottom(this ScrollRect scrollRect)
    10.     {
    11.         scrollRect.normalizedPosition = new Vector2(0, 0);
    12.     }
    13. }
     
  3. Richbk

    Richbk

    Joined:
    May 26, 2014
    Posts:
    3
    Thanks for the reply, I'll give it a go although on first glance, isn't it doing exactly the same thing?
     
  4. Feaver1968

    Feaver1968

    Joined:
    Nov 16, 2014
    Posts:
    70
    Well, yeah the API is confusing on the matter. All I know is that my ScrollToTop works for me.
     
    ImmutableBox likes this.
  5. Suguma

    Suguma

    Joined:
    May 29, 2015
    Posts:
    26
    @Richbk Dont'know if you're still struggling with this, but your code probably works only on Update because it takes one frame to update your scroll rect. If you put that code on a corroutine and wait for the end of the frame, it should work.
     
  6. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,476
    ScrollToBottom or set verticalNormalizedPosition = 0; only works when scrollrect showing. If it is hidden (gameobject.setactive(false)), this code does not work. Are there no way to scroll bottom even when it is hidden?
     
  7. tswalk

    tswalk

    Joined:
    Jul 27, 2013
    Posts:
    1,109
    showing or just enabled?
    I would think you could reset it in OnDisable, and OnEnable for either top or bottom positioning.
     
  8. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,476
    I just maintain it showing (setactive) and just change its position to 0,0,0 when need to be shown. When not, just make it as 2000,2000,0 solved.
     
  9. TharosTheDragon

    TharosTheDragon

    Joined:
    Sep 2, 2017
    Posts:
    13
    ScrollViewItem item = Instantiate( scrollViewItem, scrollRect.content ) ;
    scrollRect.verticalNormalizedPosition = 0 ;

    What's happening when I set verticalNormalizedPosition to 0 is that it scrolls to the bottom too soon, before the scroll bar has been updated to account for the enlarged content. How do I react to the scroll bar updating? Is there some event I can respond to?
     
  10. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    Once I wrote a chat box/log display, and I believe you can yield until the end of the frame, then set the veritcal normalized position.
     
    Novack and mannyhams like this.
  11. qoobit

    qoobit

    Joined:
    Aug 19, 2014
    Posts:
    51
    I ran into this issue just now, in random scenarios it would work but to be 100% certain you should actually do it at EndOfFrame when things are being recalculated. It has something to with the UGUI scheduler of when it starts to deal with the recalculation of the ScrollRect size.

    Original bug report: https://issuetracker.unity3d.com/is...-do-it-just-after-setting-size-of-the-content

    It claims it's fixed but it's not in this use case.

    The work around is to write an IEnumerator the runs at the end of frame. Ie:

    Code (csharp):
    1. IEnumerator ScrollToTop()
    2. {
    3.     yield return new WaitForEndOfFrame();
    4.     scrollRect.gameObject.SetActive(true);
    5.     scrollRect.verticalNormalizedPosition = 1f;      
    6. }
    In my example I hide the gameobject before shifting the scrollview, so the user doesn't see a blip of the scrollview in a random (usually centre) position.

    Running it at end of frame in this manner works. Just use StartCouroutine(ScrollToTop()) as needed after creating updates. If scroll to bottom is needed, just change the position from 1f to 0f.
     
  12. JackofTraes

    JackofTraes

    Joined:
    May 28, 2014
    Posts:
    10
    Okay so @qoobit helped me a bunch with that post, but I had to go an extra step when trying to force the scroll bar down to the bottom (not sure why). The workaround that worked for me might help someone else so here it is:

    Code (CSharp):
    1. // Called at the end of instantiation function.
    2.     IEnumerator ForceScrollDown () {
    3.         // Wait for end of frame AND force update all canvases before setting to bottom.
    4.         yield return new WaitForEndOfFrame ();
    5.         Canvas.ForceUpdateCanvases ();
    6.         comScroll.verticalNormalizedPosition = 0f;
    7.         Canvas.ForceUpdateCanvases ();
    8.     }
     
    Last edited: Apr 14, 2018
  13. Mckally

    Mckally

    Joined:
    Jul 24, 2018
    Posts:
    3
    if i want to make a piano tile game and want to make it autoscroll and the speed will gradually increase..how should i make it work ?
     
  14. hottabych

    hottabych

    Joined:
    Apr 18, 2015
    Posts:
    107
    Don't use UI. Instantiate piano tile sprites and change their transform.position. Don't forget to destroy them when they're below the screen.
     
  15. Leslie-Young

    Leslie-Young

    Joined:
    Dec 24, 2008
    Posts:
    1,148
    I found that you might have to wait two frames when adding variable height items, else it will not scroll to the absolute bottom of the last added object.

    This is what my complete Log panel looks like. It can auto scroll to top, if new items added to top of log, or bottom when items added to end of list.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.UI;
    5. using TMPro;
    6.  
    7. namespace YOUR_NS_HERE
    8. {
    9.    public class LogPanel : MonoBehaviour
    10.    {
    11.        [SerializeField] private ScrollRect scrollRect = null;
    12.        [SerializeField] private RectTransform container = null;
    13.        [SerializeField] private GameObject msgFab = null;
    14.        [SerializeField] private bool addNewToTop = false;
    15.  
    16.        public static LogPanel Instance { get; private set; }
    17.  
    18.        private Queue<GameObject> messages = new Queue<GameObject>();
    19.  
    20.        // ------------------------------------------------------------------------------------------------------------
    21.  
    22.        private void Awake()
    23.        {
    24.            Instance = this;
    25.  
    26.            // I keep the "message template" in the scene itself, so disable it now
    27.            msgFab.gameObject.SetActive(false);
    28.        }
    29.  
    30.        private void OnDestroy()
    31.        {
    32.            Instance = null;
    33.        }
    34.  
    35.        // ------------------------------------------------------------------------------------------------------------
    36.  
    37.        public void ToggleVisible()
    38.        {
    39.            if (gameObject.activeSelf)
    40.            {
    41.                Hide();
    42.            }
    43.            else
    44.            {
    45.                Show();
    46.            }
    47.        }
    48.  
    49.        public void Show()
    50.        {
    51.            gameObject.SetActive(true);
    52.            StartCoroutine(AutoScroll());
    53.        }
    54.  
    55.        public void Hide()
    56.        {
    57.            gameObject.SetActive(false);
    58.        }
    59.  
    60.        public void ClearAllMesssages()
    61.        {
    62.            GameObject go;
    63.            while (messages.Count > 0)
    64.            {
    65.                go = messages.Dequeue();
    66.                Destroy(go);
    67.            }
    68.        }
    69.  
    70.        public void AddMessage(string message)
    71.        {
    72.            GameObject go = Instantiate(msgFab, container);
    73.            go.transform.GetChild(0).GetComponent<TMP_Text>().text = message;
    74.            go.gameObject.SetActive(true);
    75.            messages.Enqueue(go);
    76.  
    77.            if (addNewToTop)
    78.            {
    79.                go.transform.SetAsFirstSibling();
    80.            }
    81.  
    82.            // remove older messages if there are too many
    83.            if (messages.Count > Const.MaxLogMessages)
    84.            {
    85.                go = messages.Dequeue();
    86.                Destroy(go);
    87.            }
    88.  
    89.            // auto-scroll
    90.            if (gameObject.activeSelf)
    91.            {
    92.                StartCoroutine(AutoScroll());
    93.            }
    94.        }
    95.  
    96.        private IEnumerator AutoScroll()
    97.        {
    98.            LayoutRebuilder.ForceRebuildLayoutImmediate(container);
    99.            yield return new WaitForEndOfFrame();
    100.            yield return new WaitForEndOfFrame();
    101.            scrollRect.verticalNormalizedPosition = addNewToTop ? 1 : 0;
    102.        }
    103.  
    104.        // ------------------------------------------------------------------------------------------------------------
    105.    }
    106. }
    I've attached the scene setup in case this is useful to anyone.

    LogPanel.png
     
  16. karsnen

    karsnen

    Joined:
    Feb 9, 2014
    Posts:
    65
  17. Leslie-Young

    Leslie-Young

    Joined:
    Dec 24, 2008
    Posts:
    1,148
    That alone will not scroll for you, and is already being used anyway since that is how you get the object to scale as more or less text is added to it.

    There will also be layout problems, like elements overlapping, if you do not force a rebuild (well, that was the case when I last tested this and also in Unity 2017.4).
     
  18. Novack

    Novack

    Joined:
    Oct 28, 2009
    Posts:
    844
    I guess it depends on the setup, in my case I had to wait for 3 frames in order to scroll to bottom properly.
    The info from this thread was very useful, thanks for sharing experiences.
     
  19. exodrifter

    exodrifter

    Joined:
    Mar 17, 2013
    Posts:
    10
    I've been trying to fix this problem for months and I have come to this thread again and again while looking for a suitable solution.

    None of the solutions posted work for me because, like some others have mentioned, you would often need to wait a few frames before you could actually jump to the bottom. This often resulted in the ui jumping around for a bit, which makes the game look really buggy and unpolished.

    I stumbled into a StackOverflow thread that let me find a solution that allows us to jump straight to the bottom without waiting. Here's the solution:

    Code (csharp):
    1. using UnityEngine;
    2. using UnityEngine.UI;
    3.  
    4. public static class UIX
    5. {
    6.     /// <summary>
    7.     /// Forces the layout of a UI GameObject and all of it's children to update
    8.     /// their positions and sizes.
    9.     /// </summary>
    10.     /// <param name="xform">
    11.     /// The parent transform of the UI GameObject to update the layout of.
    12.     /// </param>
    13.     public static void UpdateLayout(Transform xform)
    14.     {
    15.         Canvas.ForceUpdateCanvases();
    16.         UpdateLayout_Internal(xform);
    17.     }
    18.  
    19.     private static void UpdateLayout_Internal(Transform xform)
    20.     {
    21.         if (xform == null || xform.Equals(null))
    22.         {
    23.             return;
    24.         }
    25.  
    26.         // Update children first
    27.         for (int x = 0; x < xform.childCount; ++x)
    28.         {
    29.             UpdateLayout_Internal(xform.GetChild(x));
    30.         }
    31.  
    32.         // Update any components that might resize UI elements
    33.         foreach (var layout in xform.GetComponents<LayoutGroup>())
    34.         {
    35.             layout.CalculateLayoutInputVertical();
    36.             layout.CalculateLayoutInputHorizontal();
    37.         }
    38.         foreach (var fitter in xform.GetComponents<ContentSizeFitter>())
    39.         {
    40.             fitter.SetLayoutVertical();
    41.             fitter.SetLayoutHorizontal();
    42.         }
    43.     }
    44. }
    Use it like this:
    Code (csharp):
    1.  
    2. UIX.UpdateLayout(canvasTransform); // This canvas contains the scroll rect
    3. scrollRect.verticalNormalizedPosition = 0f;
    4.  
    I'm guessing that the reason for why this works is apparently because the LayoutGroups and ContentSizeFitters are not normally considered when you try to call Canvas.ForceUpdateCanvases or LayoutRebuilder.ForceRebuildLayoutImmediate by themselves. But, I don't know for sure. I'll try putting together a bug report for Unity and see how they respond.

    I don't know how performant this is and there are probably ways to make this faster, but for now this seems to work.
     
    Gone2Plaid, xt85, CZauX and 2 others like this.
  20. Projekt-Krieg

    Projekt-Krieg

    Joined:
    Apr 5, 2013
    Posts:
    4
    Code (CSharp):
    1. public static class ExtensionMethods
    2. {
    3.     public static void END_F_ScrollToTop(this ScrollRect sr)
    4.     {
    5.         ScrollRectScrollToTop ie = sr.gameObject.AddComponent<ScrollRectScrollToTop>();
    6.  
    7.     }
    8. }
    9. public class ScrollRectScrollToTop : MonoBehaviour
    10. {
    11.     private Coroutine coroutine;
    12.     void OnEnable()
    13.     {
    14.         ScrollRect sr = gameObject.GetComponent<ScrollRect>();
    15.         if (sr != null)
    16.         {
    17.             if (coroutine != null) StopCoroutine(coroutine);
    18.             coroutine = StartCoroutine("iScrollToTop", sr);
    19.         }
    20.     }
    21.     IEnumerator iScrollToTop(ScrollRect sr)
    22.     {
    23.         yield return new WaitForEndOfFrame();
    24.         sr.verticalNormalizedPosition = 1;
    25.     }
    26. }
    27.  
    28.  
    29. //And just call the ScollRect to Reorganise it with:
    30.  
    31. // objforcontentgeneration.GetComponentInParent<ScrollRect>().END_F_ScrollToTop();

    Some bugs you hate so hard, you just want a simple callable solution that works on GeneratedContent Windows.
     
  21. MikD

    MikD

    Joined:
    Oct 28, 2018
    Posts:
    3
    I was looking for the solution to this. That vector position did the trick. Are you sure you aren’t overthinking the problem. I just added that line to the end of my WriteToDropdown function and, every time I add a line, it scrolls to the bottom. Calling it from Update every time seems like overkill. You only want it to scroll down to show the new content, right? If you have a problem with it not scrolling to the bottom if it isn’t enabled, then add the line after the line you use to SetActive your item. If the problem is more complicated than that, I apologize.
     
    bruceweir1 likes this.
  22. CZauX

    CZauX

    Joined:
    Oct 24, 2019
    Posts:
    1
    Forcing UiContentSizeFitter to update with SetLayoutVerticle worked beautifully, thank you.
     
  23. darrenunity

    darrenunity

    Joined:
    Apr 7, 2017
    Posts:
    2
    Canvas.ForceUpdateCanvases (); is exactly what I needed, thanks
     
  24. SkywardRoy

    SkywardRoy

    Joined:
    Jan 14, 2017
    Posts:
    22
    I really wanted to find a way to keep a ScrollRect scrolled to the bottom. This thread was very helpful in finding ways to make sure the rect scrolled correctly. Unfortunately there doesn't seem to be an easy way to register whether the user has scrolled up to see older entries. After scrolling up the auto-scrolling should disable until the user scrolls back down.

    I've resorted to a toggle under the scroll bar and just auto-scrolling every update while the toggle is on. It's not pretty or ideal for performance but at least it works without hacks and workarounds. I hope the UIElements package will be better about stuff like this when it's good mature enough to use.
     
  25. kellyreyw

    kellyreyw

    Joined:
    Apr 24, 2012
    Posts:
    1
    Hopefully, this will help others that encounter this issue:

    I was able to get the text to only scroll when the player is viewing the end of the text list by checking the vertical scroll bar and only starting the coroutine when the scroll bar is at the end.

    //Forces the scrollrect to update the text control containing the
    //text when the player is looking at the end of the text.
    IEnumerator ForceScrollDown()
    {
    // Wait for the end of the frame then force an update of all canvases before setting to bottom.
    yield return new WaitForEndOfFrame();
    Canvas.ForceUpdateCanvases();
    scrollRect.verticalNormalizedPosition = 0f;
    Canvas.ForceUpdateCanvases();
    verticalScrollbar.value = 0;
    }

    /// <summary>
    /// Prints a line of formatted text to the log window.
    /// </summary>
    /// <param name="format"></param>
    /// <param name="param"></param>
    /// <remarks>
    /// Rich text tags can be used.
    /// </remarks>
    public void Print(string format, params object[] param)
    {
    text.text += string.Format(format, param) + "\n";

    if (Mathf.Approximately(verticalScrollbar.value, 0f))
    {
    StartCoroutine("ForceScrollDown");
    }
    }
     
  26. JeetYudiz

    JeetYudiz

    Joined:
    Dec 9, 2020
    Posts:
    1
    Worked LIke Charm......CHeers
     
  27. Wichael

    Wichael

    Joined:
    Apr 7, 2018
    Posts:
    6
    Add this to your Scroll Rect:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.UI;
    5. using UnityEngine.Events;
    6.  
    7. public class AutoSnapScrollBar : MonoBehaviour
    8. {
    9.     public bool snapped = true;
    10.     ScrollRect scroll;
    11.     int contentSize;
    12.    
    13.     void Awake()
    14.     {
    15.         scroll = this.GetComponent<ScrollRect>();
    16.         contentSize = scroll.content.transform.GetChildCount();
    17.     }
    18.    
    19.     void OnEnable()
    20.     {
    21.         scroll.onValueChanged.AddListener(ScrollValueChanged);
    22.         scroll.verticalScrollbar.onValueChanged.AddListener(VerticalValueChanged);
    23.     }
    24.    
    25.     void OnDisable()
    26.     {
    27.         scroll.onValueChanged.RemoveListener(ScrollValueChanged);
    28.         scroll.verticalScrollbar.onValueChanged.RemoveListener(VerticalValueChanged);
    29.     }
    30.    
    31.     void ScrollValueChanged(Vector2 value)
    32.     {
    33.         Canvas.ForceUpdateCanvases();
    34.        
    35.         if(snapped)
    36.         {
    37.             //scroll.normalizedPosition = new Vector2(0, 0);
    38.             StartCoroutine(ForceScrollDown());
    39.         }
    40.        
    41.         Canvas.ForceUpdateCanvases();
    42.     }
    43.    
    44.     void VerticalValueChanged(float value)
    45.     {
    46.         Canvas.ForceUpdateCanvases();
    47.         int currentSize = scroll.content.transform.GetChildCount();
    48.         Vector2 bottom = new Vector2(0f,0f);
    49.        
    50.         if(currentSize != contentSize)
    51.         {
    52.             // Content Added
    53.             contentSize = scroll.content.transform.GetChildCount();
    54.             Canvas.ForceUpdateCanvases();
    55.            
    56.             if(snapped)
    57.             {
    58.                 //scroll.normalizedPosition = new Vector2(0, 0);
    59.                 StartCoroutine(ForceScrollDown());
    60.             }
    61.         }
    62.         else
    63.         {
    64.             if(scroll.normalizedPosition == bottom)
    65.             {
    66.                 snapped = true;
    67.             }
    68.             else
    69.             {
    70.                 snapped = false;
    71.             }
    72.         }
    73.        
    74.         Canvas.ForceUpdateCanvases();
    75.     }
    76.    
    77.     IEnumerator ForceScrollDown ()
    78.     {
    79.         yield return new WaitForEndOfFrame ();
    80.         Canvas.ForceUpdateCanvases ();
    81.         scroll.gameObject.SetActive(true);
    82.         scroll.verticalNormalizedPosition = 0f;
    83.         scroll.verticalScrollbar.value = 0;
    84.         Canvas.ForceUpdateCanvases ();
    85.     }
    86. }
    You'll need to make sure the content transform has a Content Size Fitter with Vertical Fit set to "Preffered Size".
    Disable the "Snapped" bool manually to stop it auto snapping to the bottom from the start. If enabled it will always auto snap until you've actually scrolled. If you scroll back down to the bottom it goes back into "Snapped".

    One small issue I've noticed is that if you add content way too fast it seems to unsnap. But that requires spamming. Have not tested it with multiple content added by code yet though.
     
    Timmy-Hsu likes this.
  28. TimO234

    TimO234

    Joined:
    Feb 15, 2021
    Posts:
    1
    amazing but, i am adding text to the content with a script and that makes it auto unsnap so it never actually snaps
     
  29. dangnguyen113

    dangnguyen113

    Joined:
    Aug 8, 2017
    Posts:
    7
  30. Super_RafyYT

    Super_RafyYT

    Joined:
    Oct 5, 2021
    Posts:
    11
    freakin' life saver, thank you :D
     
  31. ezhaac

    ezhaac

    Joined:
    Feb 16, 2015
    Posts:
    4
    Thank you for your solution, it worked for me, finally!
    I'm using a content fitter in the "content" object of my scrollview, and couldn't scroll to the end.