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?

17,468

Solution 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

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

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 filter 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 filter 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

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;
Share:
17,468
Kyle Delaney
Author by

Kyle Delaney

I was until recently on the Microsoft Bot Framework support team, and now I am building bots using the Microsoft Bot Framework

Updated on July 22, 2022

Comments

  • Kyle Delaney
    Kyle Delaney almost 2 years

    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.

  • Homer Wang
    Homer Wang about 5 years
    Thanks! Your solution worked as well in my case. In your code, item is the gameobject "content" inside viewport of the scrollview; scrollRect is the ScrollRect of the scrollview.
  • John Stock
    John Stock about 5 years
    A very bad solution as this forces the whole damn UI to recalculate everything.
  • Kyle Delaney
    Kyle Delaney about 5 years
    @JohnStock - I think it would be a good idea to post an alternative solution if you're going to downvote mine
  • John Stock
    John Stock about 5 years
    @KyleDelaney That's not how this site works, nor makes logical sense.
  • Kyle Delaney
    Kyle Delaney about 5 years
    @JohnStock - Solutions are bad relative to other solutions. If this is the only way to do it then it's certainly better than nothing so why would you downvote it? If your complaint is that there isn't a better way to do it then you're downvoting the wrong person. I didn't design the Unity engine.
  • exodrifter
    exodrifter about 5 years
    This solution worked really well for me and I've added an answer that works in more general cases.
  • PeterT
    PeterT over 2 years
    By "content filter component" I think you meant "content fitter component"?
  • order
    order about 2 years
    Fixed my problem. Thank you so much!
  • Kalib Crone
    Kalib Crone almost 2 years
    I just set sr.VerticalNormalizedPosition = 0: after the end of frame, and that seems to work as well to keep it at the top