Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 801
Kyle Delaney Avatar asked Dec 02 '17 21:12

Kyle Delaney


3 Answers

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.

like image 114
Kyle Delaney Avatar answered Sep 19 '22 18:09

Kyle Delaney


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:

  • https://ancientcoder.blog/2019/01/23/force-unity-to-scroll-to-the-bottom-of-a-scroll-rect/
  • https://forum.unity.com/threads/scroll-rect-with-dynamic-content-reset-position-properly.518386/
like image 32
Wappenull Avatar answered Sep 18 '22 18:09

Wappenull


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

like image 31
Gerasimos Filippakos Avatar answered Sep 18 '22 18:09

Gerasimos Filippakos