Search Unity

Content size fitter refresh problem

Discussion in 'UGUI & TextMesh Pro' started by drHogan, Oct 3, 2017.

  1. drHogan

    drHogan

    Joined:
    Sep 26, 2012
    Posts:
    201
    Hi guys,

    I am fixing an old tooltip system of mine, and I am going nuts on an apparently fairly simple issue. What it should to, is basically that the tooltip size should adapt to the text size. (the movement, not going out of screen, etc etc is all implemented and working)

    This is my tooltip



    The root is an empty object with the script, going down one we find the "container" that has a canvas, a canvas group and moves around (i could probably skip this one and bring it to the root) :



    Then ContentLayout is the one with Content Size Fitter and Horizontal Layout :



    And Tooltip Text is a simple TextMeshPro UI element of text.

    Now, the problem is, the whole system works, but not as it should. I.e. if I move my mouse pointer over an element, i get a tooltip with correct text, but resized according to the previous element hovered (so if I hover and leave and hover again a button, i get the correct size for that button because of having hovered it twice).

    This is a sample of what happens for example (the tooltip before was smaller)



    I guess the problem is that I have no idea on how to force an immediate refresh of the content size fitter. I tried deactivating it and re-activating it, i tried to wait for the end of frame with a co-routine before making it visible, etc etc. But the problem remains.

    Any hints on what i might tackle to solve it?

    Cheers,
    H
     
    Tymianek and gustavoluciogd like this.
  2. FunRobDev

    FunRobDev

    Joined:
    Jun 3, 2017
    Posts:
    11
    Hi there. I had a very similar setup for tooltips with the same issue. This solved my problem:

    Code (CSharp):
    1. tooltTipText = "new tooltip \n second row";
    2. Canvas.ForceUpdateCanvases();  // *
    3. tooltTipText.transform.parent.GetComponent<HorizontalLayoutGroup>().enabled = false; // **
    4. tooltTipText.transform.parent.GetComponent<HorizontalLayoutGroup>().enabled = true;
    * According to documentation, a canvas (which controls your UI elements) performs its layout and content generation calculations at the end of a frame, just before rendering, in order to ensure that it's based on all the latest changes that may have happened during that frame. Code that relies on up-to-date layout or content can call Canvas.ForceUpdateCanvasaes() to ensure it before executing code that relies on it.

    ** Sometimes the reenabling of the HorizontalLayoutGroup or VercticalLayoutGroup is also needed, but it depends on your configuration.

    Cheers :)
     
    Last edited: Apr 3, 2018
    Blascolmo, ZoDy222, 777yur0k and 27 others like this.
  3. ramkaentertainment

    ramkaentertainment

    Joined:
    Sep 23, 2018
    Posts:
    2
    Hello,

    I had created a start menu for my VR game using vertical and horizontal layout group functionalities and the content size fitter component to arrange the buttons automatically.

    After my Unity update from version 2018.2.8f1 to version 2018.3.0f2, my box collider of the buttons didn't work anymore. I calculate the box collider sizes via script in the start method, depending on the rect transform of the according buttons.
    I found out, that the box collider sizes were zero.

    With Unity 2018.2.8f1 the content size fitter has calculated the rect transform before the start method is called.
    With Unity 2018.3.0f2 the content size fitter calculates the rect transform obviously after the start method is called.

    Calling Canvas.ForceUpdateCanvasaes() before I calculate the size of the box collider in the start method worked for me too.

    Thanks!
     
  4. Cloppi

    Cloppi

    Joined:
    Sep 16, 2015
    Posts:
    2
    @FunRobDev ,
    Thank you! Your approach with additional re-enabling of HorizontalLayoutGroup component helped me to solve the problem as well. I've got an array of similar UI elements with dependence to Text.text.Lenght and they didn't want to placing correctly in the parent panel, but re-enabling replaced them right way.

    Thanks!
     
  5. doctorpangloss

    doctorpangloss

    Joined:
    Feb 20, 2013
    Posts:
    270
    This is a little wonky, you probably don't want to just disable/enable/ForceUpdateCanvases because it'll give UGUI people a heart attack. Implement `ILayoutElement` on your tooltip script, and in your CalculateLayoutInputHorizontal implementation, call CalculateLayoutInputHorizontal on the child. Also, implement preferredWidth using the child text's preferredWidth. Attach a content size fitter. That's it.
     
    Deleted User likes this.
  6. dadude123

    dadude123

    Joined:
    Feb 26, 2014
    Posts:
    789
    Canvas.ForceUpdateCanvasaes()

    costs a ton of cpu cycles for no reason.
    Use LayoutRebuilder.ForceRebuildLayoutImmediate instead.
    You can pass in the transform for which you want to do the refresh.

    When using it with content size fitter you might want to call it twice with the same object in some situations (when you have a contentsizefitter + layout control at the same time).

    Calling that method twice will still be 100x faster than forcing a full refresh of the whole canvas.
     
  7. losingisfun

    losingisfun

    Joined:
    May 26, 2016
    Posts:
    36
    Have to agree with dadude123 on this. However, should clarify you will need the RectTransform of the content size fitter / layout component. Simply cache this and you'll have it working much quicker.

    I would HIGHLY recommend that you do not use GetComponent, rather cache the reference, because a GetComponent call is ridiculously expensive.

    EDIT:
    Or if you have the Transform already you can use
    LayoutRebuilder.ForceRebuildLayoutImmediate(transform as RectTransform)
    to cast it.
     
    Last edited: Jun 23, 2022
  8. Masea1

    Masea1

    Joined:
    Jun 9, 2018
    Posts:
    41
    I am not quite sure why but in my case, it was enough for me to only force update the canvas when Pixel Perfect option was enabled in the Canvas component to get use of the size fitter. However, when I disabled the option, I had to add those two lines and so refresh the Layout component as well. This needs to be revisited ASAP.
     
    Will0, vinnieg and unnanego like this.
  9. uzisho

    uzisho

    Joined:
    Jul 3, 2019
    Posts:
    14
    LayoutRebuilder.ForceRebuildLayoutImmediate worked wonders for me.
    Thanks dadude123!

     
    bettywhite likes this.
  10. houskyd

    houskyd

    Joined:
    Feb 10, 2020
    Posts:
    3
    It's worth noting that LayoutRebuilder.ForceRebuildLayoutImmediate will only work properly if the game object is active.
     
  11. CubeGameStudio

    CubeGameStudio

    Joined:
    Dec 1, 2019
    Posts:
    18
    only this worked for me:


    Code (CSharp):
    1. messageContainer.GetComponent<VerticalLayoutGroup>().enabled = false;
    2. messageContainer.GetComponent<VerticalLayoutGroup>().enabled = true;
     
    Indrit_Vaka, awsapps and akuno like this.
  12. pofu_lu

    pofu_lu

    Joined:
    Feb 5, 2019
    Posts:
    4
  13. WILEz1975

    WILEz1975

    Joined:
    Mar 23, 2013
    Posts:
    375
    To me it only works in a coroutine:

    Code (CSharp):
    1.     public IEnumerator UpdateRect() {
    2.         if (GetComponent<VerticalLayoutGroup>())
    3.         GetComponent<VerticalLayoutGroup>().enabled = false;
    4.         yield return new WaitForSeconds(0.1F);
    5.         if (GetComponent<VerticalLayoutGroup>())
    6.             GetComponent<VerticalLayoutGroup>().enabled = true;
    7.     }
     
    KingsHere and DerDicke like this.
  14. KingsHere

    KingsHere

    Joined:
    Nov 3, 2015
    Posts:
    3
    Thank you!

    Yes, it needs an update between disable and enable.

    To the new guys, I would recommend caching if you're calling this coroutine too often :
    Code (CSharp):
    1.  
    2.     public GameObject rootOfScrollContent;
    3.  
    4.     VerticalLayoutGroup verticalLayoutGroup;
    5.  
    6.     void Start()
    7.     {
    8.         verticalLayoutGroup = rootOfScrollContent.GetComponent<VerticalLayoutGroup>();
    9.         if (verticalLayoutGroup == null)
    10.         {
    11.             Debug.LogError("No VerticalLayoutGroup component on rootOfScrollContent exists.");
    12.         }
    13.         else
    14.         {
    15.             StartCoroutine(UpdateRect());
    16.         }
    17.     }
    18.  
    19.  
    20.     IEnumerator UpdateRect()
    21.     {
    22.         if (verticalLayoutGroup != null)
    23.         {
    24.             verticalLayoutGroup.enabled = false;
    25.             yield return new WaitForSeconds(0.1F);
    26.             verticalLayoutGroup.enabled = true;
    27.         }
    28.     }

    [It was working all fine once, all of a sudden started to get this error on 2019.4; where ContentSizeFitter wouldn't update the height of scrollContent. Spent almost 3 hours on this trivial thing . Really didn't want to write this hack, as there should be some editor only thing.]
     
    alargastudio likes this.
  15. JonesEri07

    JonesEri07

    Joined:
    Aug 21, 2019
    Posts:
    1

    THANK YOU! Looked everywhere for a solution and this worked perfectly.
     
  16. gustavoluciogd

    gustavoluciogd

    Joined:
    Oct 23, 2019
    Posts:
    1
    Spend a day looking for this, thanks so much
     
  17. Tymianek

    Tymianek

    Joined:
    May 16, 2015
    Posts:
    97
    I tried all of these and none of them work. My UI still takes 3 frames to stabilise. I tried ForceRebuild on every UI element, disabling and enabling all LayoutGroups OnEnable, and more.
     
    Last edited: Feb 16, 2021
  18. look001

    look001

    Joined:
    Mar 23, 2017
    Posts:
    111
    It is important to rebuild the layout from the bottom up, sice the parent objects will need the size of its children. You have to make sure to rebuild the children first an then the parent. Here is an example how you could implement it:
    Code (CSharp):
    1.  
    2. public class ContentFitterRefresh : MonoBehaviour
    3. {
    4.     private void Awake()
    5.     {
    6.         RefreshContentFitters();
    7.     }
    8.  
    9.     public void RefreshContentFitters()
    10.     {
    11.         var rectTransform = (RectTransform)transform;
    12.         RefreshContentFitter(rectTransform);
    13.     }
    14.  
    15.     private void RefreshContentFitter(RectTransform transform)
    16.     {
    17.         if (transform == null || !transform.gameObject.activeSelf)
    18.         {
    19.             return;
    20.         }
    21.      
    22.         foreach (RectTransform child in transform)
    23.         {
    24.             RefreshContentFitter(child);
    25.         }
    26.  
    27.         var layoutGroup = transform.GetComponent<LayoutGroup>();
    28.         var contentSizeFitter = transform.GetComponent<ContentSizeFitter>();
    29.         if (layoutGroup != null)
    30.         {
    31.             layoutGroup.SetLayoutHorizontal();
    32.             layoutGroup.SetLayoutVertical();
    33.         }
    34.  
    35.         if (contentSizeFitter != null)
    36.         {
    37.             LayoutRebuilder.ForceRebuildLayoutImmediate(transform);
    38.         }
    39.     }
    40. }
    Attach the script to your upper most contentsizefitter. Call RefreshContentFitters to rebuild contentsizefitters and layoutgroups in the right order.
     
    Last edited: Feb 20, 2021
    _eternal, ahan_pm, nh92 and 21 others like this.
  19. raydekk

    raydekk

    Joined:
    Mar 14, 2013
    Posts:
    100
    You, sir, are a life saver.

    Thank you!
     
  20. subramaniyanvgatmoback

    subramaniyanvgatmoback

    Joined:
    Oct 13, 2016
    Posts:
    15
    Crystal clear explanation thank you so much.
     
    L0tan likes this.
  21. steril

    steril

    Joined:
    Nov 30, 2014
    Posts:
    8
    Thanks, this is great which solved my issue. FYI, this worked for me when I changed Awake to Start. (2019.2.12f)
     
    Last edited: Aug 9, 2022
  22. alexanderlarsen

    alexanderlarsen

    Joined:
    Feb 26, 2015
    Posts:
    13
    This simple code worked for me on a similar problem. Attach the script to the uppermost GameObject with a ContentSizeFitter and call RebuildLayout when necessary. Hope this spares someone a headache!

    Code (CSharp):
    1. public class RebuildUILayoutHelper : MonoBehaviour
    2. {
    3.     public void RebuildLayout()
    4.     {
    5.         StartCoroutine(WaitOneFrameThenRebuild());
    6.     }
    7.  
    8.     private IEnumerator WaitOneFrameThenRebuild()
    9.     {
    10.         yield return new WaitForEndOfFrame();
    11.         LayoutRebuilder.ForceRebuildLayoutImmediate((RectTransform)transform);
    12.     }
    13. }

    (Unity 2020.3.0)
     
  23. ifightnoman

    ifightnoman

    Joined:
    Jan 15, 2022
    Posts:
    15
    Sometimes I see UI flicker when doing the rebuild async, but calling rebuild upfront *and* after a frame avoids that. So, here's my version:

    Code (CSharp):
    1. public static class MonoBehaviorExtensions {
    2.     /// <summary>
    3.     /// Performs ForceRebuildLayoutImmediate some number of times based on how
    4.     /// many nested layout panels we have.
    5.     /// </summary>
    6.     public static void RebuildLayout(this MonoBehaviour b, int layoutNesting = 1) {
    7.         b.StartCoroutine(RebuildLayout(b.transform as RectTransform, layoutNesting));
    8.     }
    9.  
    10.     /// <summary>
    11.     /// This seems unhinged but it covers all our bases.
    12.     ///
    13.     /// Unity layout is trash: a single rebuild call is ineffective for nested
    14.     /// content size fitters or layout panels that depend on them. And it
    15.     /// (sometimes, but not always) needs to wait a frame before rebuilding
    16.     /// will have an effect. We *also* don't want UI flicker from doing this
    17.     /// async, so we rebuild once before waiting, and then again after waiting.
    18.     /// </summary>
    19.     private static IEnumerator RebuildLayout(RectTransform rectTransform, int iterations) {
    20.         for (int i = 0; i < iterations; i++) {
    21.             LayoutRebuilder.ForceRebuildLayoutImmediate(rectTransform);
    22.         }
    23.  
    24.         yield return new WaitForEndOfFrame();
    25.  
    26.         for (int i = 0; i < iterations; i++) {
    27.             LayoutRebuilder.ForceRebuildLayoutImmediate(rectTransform);
    28.         }
    29.     }
    30. }
     
    guillemllovera and losingisfun like this.
  24. Munchy2007

    Munchy2007

    Joined:
    Jun 16, 2013
    Posts:
    1,735
    For a UI element that has multiple nested layout groups, this method worked for me in Unity 2022. This would be on the root game object of the UI element that contains the layout groups.

    Code (CSharp):
    1.         private void Awake()
    2.         {
    3.             layoutGroups = GetComponentsInChildren<LayoutGroup>();
    4.         }
    5.  
    6.         public virtual void Show(QuestInfo questInfo)
    7.         {
    8.             // Do stuff with the layout elements
    9.             // ...
    10.             // ...
    11.             // ...
    12.             // ...
    13.  
    14.             foreach (var layoutgroup in layoutGroups)
    15.             {
    16.                 LayoutRebuilder.MarkLayoutForRebuild(layoutgroup.transform as RectTransform);
    17.             }
    18.         }
    19.  
    20.  
     
  25. MasterZ0

    MasterZ0

    Joined:
    Jul 24, 2020
    Posts:
    18
    If the gameobject is disabled, after enabling is necessary to call ForceRebuildLayoutImmediate after the end of the frame. Apparently this works in any situation, I'm using version 2022.

    Code (CSharp):
    1.  
    2.      private IEnumerator RebuildLayout(RectTransform layoutGroupTransform)
    3.      {
    4.         LayoutRebuilder.ForceRebuildLayoutImmediate(layoutGroupTransform);
    5.         yield return new WaitForEndOfFrame();
    6.         LayoutRebuilder.ForceRebuildLayoutImmediate(layoutGroupTransform);
    7.      }
    8.  
    To make it easier you can turn this into a static extension method

    Code (CSharp):
    1.  
    2.     public static class MonoBehaviourExtensions
    3.     {
    4.         public static void RebuildLayout(this MonoBehaviour monoBehaviour)
    5.         {
    6.             monoBehaviour.StartCoroutine(RebuildLayout(monoBehaviour.transform as RectTransform));
    7.         }
    8.  
    9.         private static IEnumerator RebuildLayout(RectTransform rectTransform)
    10.         {
    11.             LayoutRebuilder.ForceRebuildLayoutImmediate(rectTransform);
    12.             yield return new WaitForEndOfFrame();
    13.             LayoutRebuilder.ForceRebuildLayoutImmediate(rectTransform);
    14.         }
    15.     }
     
    Last edited: Oct 27, 2022
    grentle likes this.
  26. nxtboyIII

    nxtboyIII

    Joined:
    Jun 4, 2015
    Posts:
    281
    thank you so much! I didn't realize I had to rebuild the layout for each element. I assumed that LayoutRebuilder.ForceRebuildLayoutImmediate was already recursive to its children
     
  27. Saturn1004

    Saturn1004

    Joined:
    Nov 14, 2014
    Posts:
    42
    I get so tired of fixing Unity bugs that should have been fixed years ago... latest LTS version and late into 2023 and this bug still exists...
     
    mfth84845 and alphdevcode like this.
  28. Tymianek

    Tymianek

    Joined:
    May 16, 2015
    Posts:
    97
    For some cases this works brilliantly:
    Code (CSharp):
    1. contentSizeFitter.enabled = false;
    2. contentSizeFitter.enabled = true;
     
  29. Sabelo93

    Sabelo93

    Joined:
    Jan 28, 2022
    Posts:
    1
    For me it the ContentSizeFitter worked correctly only after my 1st hover over the gameObject. So after trying all of the above solutions my solution was to just artificially make it seem like I had hovered 2 times and it works for me.


    Code (CSharp):
    1.  
    2.  
    3.     [SerializeField] private GameObject _textGameObject;
    4.     [SerializeField] private TextMeshProUGUI _text;
    5.     private bool hasChangedText = false;
    6.  
    7.  
    8. public void OnMouseEnter()
    9.     {
    10.         if (hasChangedText == false )
    11.         {
    12.             StartCoroutine(MouseEntered());
    13.         }
    14.         else
    15.             _textGameObject.SetActive(true);
    16.  
    17.     }
    18.  
    19.     public void OnMouseExit()
    20.     {
    21.         _textGameObject.SetActive(false);
    22.     }
    23.  
    24.     IEnumerator MouseEntered()
    25.     {
    26.         _textGameObject.SetActive(true);
    27.         yield return new WaitForEndOfFrame();
    28.         _textGameObject.SetActive(false);
    29.         yield return new WaitForSeconds(0.01F);
    30.         _textGameObject.SetActive(true);
    31.         hasChangedText = true;
    32.  
    33.     }
     
  30. Tom163

    Tom163

    Joined:
    Nov 30, 2007
    Posts:
    1,290
    It appears that LayoutRebuilder doesn't exist anymore in the current Unity versions.

    They could just fix things instead, but it appears they spent their time on more important things.
     
  31. jakesee

    jakesee

    Joined:
    Jan 7, 2020
    Posts:
    79
    This is what works for me after trying many different ways:

    Code (CSharp):
    1.  
    2. void OnEnable()
    3. {
    4.    StartCoroutine(_OnEnable());
    5. }
    6.  
    7. IEnumerator _OnEnable() {
    8.      yield return new WaitForEndOfFrame();
    9.     LayoutRebuilder.ForceRebuildLayoutImmediate(this.transform as RectTransform);
    10. }
    My dialogbox gameobject is always in the scene and set to inactive. When it is needed, its contents get generated at runtime BEFORE it is set to active via this.gameObject.SetActive(true), then OnEnable() gets called by Unity, and the coroutine starts.
     
  32. jlnorris

    jlnorris

    Joined:
    May 17, 2022
    Posts:
    7
    I just used it in 2023.1.16f1, so not sure why you can't find it. Which version were you using?

    (And it solved my problem and got the content size fitter to immediately update, thanks dadude123!)
     
    Last edited: Feb 18, 2024