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).
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?
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);
}
}
@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.
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?
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?
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.
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.
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 ();
}
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.
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.