Search Unity

Scroll to the bottom of a Scrollrect in code

Discussion in 'Unity UI (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.
     
  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.
     
    planargazer likes this.
  6. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    1,811
    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,106
    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:
    1,811
    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,713
    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.
     
  11. qoobit

    qoobit

    Joined:
    Aug 19, 2014
    Posts:
    40
    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
    MamaCatDev and qoobit like this.
  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:
    96
    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,042
    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
     
    JackofTraes, Novack and giggioz like this.
  16. karsnen

    karsnen

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

    Leslie-Young

    Joined:
    Dec 24, 2008
    Posts:
    1,042
    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:
    633
    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:
    8
    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.
     
    cloutiertyler likes this.
  20. Projekt-Krieg

    Projekt-Krieg

    Joined:
    Apr 5, 2013
    Posts:
    2
    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.