'How do I get a Unity Scroll Rect to scroll to the bottom after the content's Rect Transform is updated by a Content Size Fitter?

I have a vertical scroll view that I want to add content to dynamically. In order to do this I've attached a Content Size Fitter component and a Vertical Layout Group component to the Content game object, so that its Rect Transform will automatically grow whenever I instantiate new game objects as children of it. If the scroll bar is already at the bottom, I want to keep the scroll bar at the bottom after the new object is added at the bottom. So I'm doing that like this:

    if ( scrollRect.verticalNormalizedPosition == 0 )
    {
        isAtBottom = true ;
    }

    ScrollViewItem item = Instantiate( scrollViewItem, scrollRect.content ) ;

    if ( isAtBottom )
    {
        scrollRect.verticalNormalizedPosition = 0 ;
    }

However, this doesn't work because the newly-instantiated scroll view item hasn't increased the size of the Rect Transform by the time I set verticalNormalizedPosition to zero. So when the Rect Transform is finally updated, it's too late to scroll to the bottom.

To illustrate, let's say my content was 400 pixels tall and the scroll bar was all the way at the bottom. Now I add an object to it that's 100 pixels tall. Then I send the scroll bar to the bottom, but it still thinks the content is 400 pixels tall. Then the content size gets updated to 500 pixels, but the scroll bar is 400 pixels down so it's only 80% of the way down instead of 100%.

There are two possible ways to solve this problem. I'd like either a way to force the Content Size Fitter to update right away or a way to respond to the Content Size Fitter updating as an event.

Through research and experimentation, I've almost succeeded in the first option by putting these lines in this exact order:

Canvas.ForceUpdateCanvases();
scrollRect.content.GetComponent<VerticalLayoutGroup>().CalculateLayoutInputVertical() ;
scrollRect.content.GetComponent<ContentSizeFitter>().SetLayoutVertical() ;
scrollRect.verticalNormalizedPosition = 0 ;

However, it doesn't quite scroll all the way to the bottom. It's always about 20 pixels away. So I'm wondering if there are still some layout operations that I'm not forcing to happen. Perhaps it's the padding or something.



Solution 1:[1]

Okay, I believe I've figured it out. In most cases, Canvas.ForceUpdateCanvases(); is all you need to do before setting verticalNormalizedPosition to zero. But in my case, the item I'm adding to the content itself also has a Vertical Layout Group component and a Content Size Fitter component. So I gotta perform these steps in this order:

Canvas.ForceUpdateCanvases();

item.GetComponent<VerticalLayoutGroup>().CalculateLayoutInputVertical() ;
item.GetComponent<ContentSizeFitter>().SetLayoutVertical() ;

scrollRect.content.GetComponent<VerticalLayoutGroup>().CalculateLayoutInputVertical() ;
scrollRect.content.GetComponent<ContentSizeFitter>().SetLayoutVertical() ;

scrollRect.verticalNormalizedPosition = 0 ;

It's a bit of a shame there's so little documentation surrounding these methods.

Solution 2:[2]

Proper method without Canvas.ForceUpdateCanvases and crazy iteration. Confirmed work in Unity 2018.3.12

// Assumes
ScrollRect m_ScrollRect;

And somewhere that you update ScrollRect content and want to backup scroll bar position

float backup = m_ScrollRect.verticalNormalizedPosition;

/* Content changed here */

StartCoroutine( ApplyScrollPosition( m_ScrollRect, backup ) );

And to apply new scroll position without jitter, it needs to be end of frame, we use Coroutine to wait for that timing and then use LayoutRebuilder.ForceRebuildLayoutImmediate to trigger layout rebuild only on that portion.

IEnumerator ApplyScrollPosition( ScrollRect sr, float verticalPos )
{
    yield return new WaitForEndOfFrame( );
    sr.verticalNormalizedPosition = verticalPos;
    LayoutRebuilder.ForceRebuildLayoutImmediate( (RectTransform)sr.transform );
}

Credit to:

Solution 3:[3]

Signed up just to answer this; I found a much quicker way:

Just set the anchor & pivot of the content object (which can also have a content fitter component), and it'll start from the bottom and you'll scroll upwards.

My setup was:

  1. preset 'scroll view' from unity with disabled scroll bars
  2. 'content' object has vertical layout component
  3. 'content' object has content fitter component
  4. 'content' object is anchor is set to 'bottom stretch' with anchor & pivot set as well

Hope this helps to anyone who doesn't wanna do something custom and needs code anyway, cheers

Solution 4:[4]

Here's a solution that works regardless of how many nested LayoutGroups and ContentSizeFitters there are:

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;

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Kyle Delaney
Solution 2 Wappenull
Solution 3 order
Solution 4 exodrifter