Scroll to the bottom of a Scrollrect in code

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:

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.

1 Like

I have a little extension method that I use for scrolling to top. It should work for scrolling to bottom.

using UnityEngine;
using UnityEngine.UI;
public static class ScrollRectExtensions
{
    public static void ScrollToTop(this ScrollRect scrollRect)
    {
        scrollRect.normalizedPosition = new Vector2(0, 1);
    }
    public static void ScrollToBottom(this ScrollRect scrollRect)
    {
        scrollRect.normalizedPosition = new Vector2(0, 0);
    }
}
23 Likes

Thanks for the reply, I’ll give it a go although on first glance, isn’t it doing exactly the same thing?

Well, yeah the API is confusing on the matter. All I know is that my ScrollToTop works for me.

1 Like

@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.

3 Likes

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?

showing or just enabled?
I would think you could reset it in OnDisable, and OnEnable for either top or bottom positioning.

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.

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?

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.

2 Likes

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: Unity Issue Tracker - Setting ScrollRect&#39;s normalized position does not work if you 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:

IEnumerator ScrollToTop() 
{ 
    yield return new WaitForEndOfFrame(); 
    scrollRect.gameObject.SetActive(true);
    scrollRect.verticalNormalizedPosition = 1f;       
}

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 Likes

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:

// Called at the end of instantiation function.
    IEnumerator ForceScrollDown () {
        // Wait for end of frame AND force update all canvases before setting to bottom.
        yield return new WaitForEndOfFrame ();
        Canvas.ForceUpdateCanvases ();
        comScroll.verticalNormalizedPosition = 0f;
        Canvas.ForceUpdateCanvases ();
    }
10 Likes

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 ?

1 Like

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.

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.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;

namespace YOUR_NS_HERE
{
   public class LogPanel : MonoBehaviour
   {
       [SerializeField] private ScrollRect scrollRect = null;
       [SerializeField] private RectTransform container = null;
       [SerializeField] private GameObject msgFab = null;
       [SerializeField] private bool addNewToTop = false;

       public static LogPanel Instance { get; private set; }

       private Queue<GameObject> messages = new Queue<GameObject>();

       // ------------------------------------------------------------------------------------------------------------

       private void Awake()
       {
           Instance = this;

           // I keep the "message template" in the scene itself, so disable it now
           msgFab.gameObject.SetActive(false);
       }

       private void OnDestroy()
       {
           Instance = null;
       }

       // ------------------------------------------------------------------------------------------------------------

       public void ToggleVisible()
       {
           if (gameObject.activeSelf)
           {
               Hide();
           }
           else
           {
               Show();
           }
       }

       public void Show()
       {
           gameObject.SetActive(true);
           StartCoroutine(AutoScroll());
       }

       public void Hide()
       {
           gameObject.SetActive(false);
       }

       public void ClearAllMesssages()
       {
           GameObject go;
           while (messages.Count > 0)
           {
               go = messages.Dequeue();
               Destroy(go);
           }
       }

       public void AddMessage(string message)
       {
           GameObject go = Instantiate(msgFab, container);
           go.transform.GetChild(0).GetComponent<TMP_Text>().text = message;
           go.gameObject.SetActive(true);
           messages.Enqueue(go);

           if (addNewToTop)
           {
               go.transform.SetAsFirstSibling();
           }

           // remove older messages if there are too many
           if (messages.Count > Const.MaxLogMessages)
           {
               go = messages.Dequeue();
               Destroy(go);
           }

           // auto-scroll
           if (gameObject.activeSelf)
           {
               StartCoroutine(AutoScroll());
           }
       }

       private IEnumerator AutoScroll()
       {
           LayoutRebuilder.ForceRebuildLayoutImmediate(container);
           yield return new WaitForEndOfFrame();
           yield return new WaitForEndOfFrame();
           scrollRect.verticalNormalizedPosition = addNewToTop ? 1 : 0;
       }

       // ------------------------------------------------------------------------------------------------------------
   }
}

I’ve attached the scene setup in case this is useful to anyone.

5 Likes

Guys, this works like charm. No code required.

3 Likes

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).

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.

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:

using UnityEngine;
using UnityEngine.UI;

public static class UIX
{
	/// <summary>
	/// Forces the layout of a UI GameObject and all of it's children to update
	/// their positions and sizes.
	/// </summary>
	/// <param name="xform">
	/// The parent transform of the UI GameObject to update the layout of.
	/// </param>
	public static void UpdateLayout(Transform xform)
	{
		Canvas.ForceUpdateCanvases();
		UpdateLayout_Internal(xform);
	}

	private static void UpdateLayout_Internal(Transform xform)
	{
		if (xform == null || xform.Equals(null))
		{
			return;
		}

		// Update children first
		for (int x = 0; x < xform.childCount; ++x)
		{
			UpdateLayout_Internal(xform.GetChild(x));
		}

		// Update any components that might resize UI elements
		foreach (var layout in xform.GetComponents<LayoutGroup>())
		{
			layout.CalculateLayoutInputVertical();
			layout.CalculateLayoutInputHorizontal();
		}
		foreach (var fitter in xform.GetComponents<ContentSizeFitter>())
		{
			fitter.SetLayoutVertical();
			fitter.SetLayoutHorizontal();
		}
	}
}

Use it like this:

UIX.UpdateLayout(canvasTransform); // This canvas contains the scroll rect
scrollRect.verticalNormalizedPosition = 0f;

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.

5 Likes
public static class ExtensionMethods
{
    public static void END_F_ScrollToTop(this ScrollRect sr)
    {
        ScrollRectScrollToTop ie = sr.gameObject.AddComponent<ScrollRectScrollToTop>();

    }
}
public class ScrollRectScrollToTop : MonoBehaviour
{
    private Coroutine coroutine;
    void OnEnable()
    {
        ScrollRect sr = gameObject.GetComponent<ScrollRect>();
        if (sr != null)
        {
            if (coroutine != null) StopCoroutine(coroutine);
            coroutine = StartCoroutine("iScrollToTop", sr);
        }
    }
    IEnumerator iScrollToTop(ScrollRect sr)
    {
        yield return new WaitForEndOfFrame();
        sr.verticalNormalizedPosition = 1;
    }
}


//And just call the ScollRect to Reorganise it with:

// objforcontentgeneration.GetComponentInParent<ScrollRect>().END_F_ScrollToTop();

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