[Solved] Can't get the rect.width / rect.height of an element when using layouts

Hi,
i need to get the width and height dimension of a ui rect element during runtime to adjust the tiling/offset of a ui material texture.

width = this.GetComponent<RectTransform>().rect.width;
height = this.GetComponent<RectTransform>().rect.height;
ratio = width / height;

Everything works correctly until the elements are inside a panel with a vertical/horizontal layout component. Then even though the rect transform of the elements shows the correct values (grayed out), the above code returns zero for both width and height. I just need to read the above values, not change them (i know that the elements size is now controlled by the vertical/horizontal layout component)
Is that functionality on purpose? The only way i can think of getting the values atm is temporarily disabling the layout component fetch the values and then reenabling it? Is there an easier way to get the values?

Thanks

6 Likes

Figured it out after a lot of search in the unity answers. I will add it here too in case someone has the same issue in the future.
The problem was that i tried to get the width/height in my start function. But as it turns out the layout group scirpt (as well as content size fitters) need a frame to update before you can get the correct element size values. So you can just Invoke(“GetSize”, 0.01); on your start function to delay it a bit.

29 Likes

Moure - Awesome share. I made an automatic content size fitter, but when you instantiate all the elements at run time they wouldn’t space out evenly within the Layout group ( Horizontal). I used unity’s guide - Redirecting to latest version of com.unity.ugui

Anyway, found out the Parent UI element needed a Min size which would show up in the Rect-transform Height but wouldn’t pass it at all. Then only will the Layout group’s register the spacing. So I needed to steal that info and slap it into the Layout Elements Min size. Fortunately what you did worked, in combination to the Guide above. The small bit of code is below.

using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class DyanamicUIFitter : MonoBehaviour {
 
    // Use this for initialization
    void Start () { 

        //Get the Layout Min Height from the Rect Transform Height
        //Wait for the first frame
        Invoke("GetRectSet", 0.01f); ///on your start function to delay it a bit.     
 
    }

    void GetRectSet(){
        //Set the Layout Min Value equivaline to RectTransform
        this.GetComponent<LayoutElement>().minHeight = this.GetComponent<RectTransform> ().rect.height;

    }
 

}

Thanks again!
OJ

7 Likes

This saved my life. Thanks a ton for the solution.

1 Like

I just hit the same snag.
Glad to know I was on the right track.
Also sad to know I was on the right track. I’ll have to eat a 1-frame jump in my UI, which is ugly.
Oh well. Sorry about the necro, but this saved me creating a new thread.

1 Like

Just a note. As this is valid in most of the cases. take into account that if the first frame take more time than 0.01f it won’t calculate it as it should.

I recommend to use a Courutine waiting for the end of frame

  private void Start()
    {
        StartCoroutine(WaitUntilEndOfFrame());
    }


  IEnumerator WaitUntilEndOfFrame()
    {
        yield return new WaitForEndOfFrame();
        //Do your stuff
    }
    }
9 Likes

This also works:

IEnumerator Start()
{
   //normal Start stuff...
   yield return null;
   // get rect.width... (runs after 1 frame)
}
3 Likes

In your Start method you could use
Canvas.ForceUpdateCanvases();
before accessing the rectTransform.rect.width property. That will ensure layout dimensions are calculated and rect.width and rect.height will output proper values.
Edit: It’s fine to call it once in Awake or Start, but don’t use it in Update.

5 Likes

Thanks Tymianek, that works…
I agree it’s an absurd bug and quite embarrassing it has been around for 3 years without being fixed or addressed in any way by Unity.

You can also try

LayoutRebuilder.ForceRebuildLayoutImmediate((RectTransform) HorizontalLayoutGroup.transform);
2 Likes

It is also important where your script is in the Execution Order.
In the example below I needed to force canvas script before TMPro is calculated, otherwise it would get wrong width on the first frame and my Scroll View was getting scrolled for no reason.
5390355--546654--upload_2020-1-20_17-23-38.png

using UnityEngine;

public class ForceCanvasUpdate : MonoBehaviour
{
    void Start()
    {
        Canvas.ForceUpdateCanvases();
    }
}

Thanks dear, you cleared the concept of waiting for some time and @Suduckgames gave a valid point to wait till the end of the frame.

Here is my code

void Start()
{
      WaitUntilEndOfFrame();
}
IEnumerator WaitUntilEndOfFrame()
    {
        yield return new WaitForEndOfFrame();
        //Do your stuff
        UpdateDimTextBC();
    }

//For calling a method UpdateDimTextBC

    public void UpdateDimTextBC()
    {
        foreach (Transform dimtext1 in canvas1.transform)
        {
            if (dimtext1.gameObject.activeSelf)
            {
                var widthDT = dimtext1.gameObject.GetComponent<RectTransform>().rect.width;
            var heightDT = dimtext1.gameObject.GetComponent<RectTransform>().rect.height;
            Debug.LogWarningFormat("Box collider width:{0} height:{1}", widthDT, heightDT);
            var UIPrefabBC = dimtext1.gameObject.GetComponent<BoxCollider>();
            UIPrefabBC.size = new Vector3(widthDT, heightDT, 0);
            }
        }
    }

Anyone wanting to use content size fitter and box collider and wants to update it automatically, this combination worked for me.

It is so irritating. I have been wasting time days and days to find this out. It is really demotivating me. I am an indie developer and I am demotivated by so many quirks like that I face every day. It is so tiring. Thanks for saving my time.

Canvas.ForceUpdateCanvases();

worked for me.

I would like to thank all of you for pointing this out and giving potential solutions.

When it comes to dynamic adaptation of the UI for mobile devices, Unity as an engine requires a lot of foundation work, hence why so little games and app actually go the whole mile to make something new and exciting to use.

I’m making use of layer groups to manage rows (or columns) of buttons and I’m adapting the height of certain groups based on the width of their individual button. (Especially useful for square/circle buttons.) I’m doing all the “manual” work because the screen ratio difference between devices can be really damn huge nowadays. Taking the whole height and width of a screen, looking up how much “space” is available, putting conditions whenever the space is within A, B, C or D so that the UI looks good on all sizes without having button too small or too big or taking too much of the screen… or too little.

It’s an even bigger nightmare when you allow the user to rotate their device.
(Before someone mention that you can lock the screen rotation of the app, one of the most common complain of certain games is how the device has to be held to play the game. For example, some people like to be able to play when coming back from work where 1 hand might hold a briefcase while some people might be moving in a tight space where holding the device vertically offers a bit less chance to hit someone close by with the device by mistake.

Hence, most of my mobile projects (when applicable) are made to be usable in both vertical and horizontal which involve 2 UI with different layout that fits into any kind of screen ratio.

Using the Coroutine wit WaitForEndOfFrame() is the simplest and easiest way for me since it can be stopped and started when necessary without any risk of “bleeding” the processes.

Reading through this thread and one other, we arrived at:

I had the same problem and I want to thanks you all for the ideas I got from this post. I had a similar problem. For me the best solution was the WaitForEndofFrame. However since I was updating the content dynamically several types I was not too happy to need to always use coroutines to get the correct height and width.
So i come auto with (another) solution

    private void OnRectTransformDimensionsChange()
    {
        rectWidth = contentRectTransform.rect.width;
        rectHeight = contentRectTransform.rect.height;
     }

where contentRectTransform is the recttransform of the object to which the script is attached. Using on recttransformDimensionsChange everytime the recttransform changes the width and height are automatically updated.
Just posting in case this can help as the the options helped me

$(document).ready in JavaScript ensures the code runs after the HTML is fully loaded and parsed before images and other resources.
If Javascript code manipulate image or refer to a property in image run before image loaded, these code will not work properly.

The same thing apply to canvas in Unity.
A canvas 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. This means that in the Start callback and the first Update callback, the layout and content under the canvas may not be up-to-date.
Source: Unity - Scripting API: Canvas.ForceUpdateCanvases

Canvas.ForceUpdateCanvases() will help force all canvases to update their content.

Imagine elements are inside a panel with a vertical/horizontal layout component or a grid layout group.
Only after run ForceUpdateCanvases() function, you can call to:

width = this.GetComponent<RectTransform>().rect.width;
height = this.GetComponent<RectTransform>().rect.height;
position = this.transform.localPosition.y;

This is because, as I say above, you can only call to a property of Image after Image loaded.
Similarly, you can only call to a property of a cell inside grid layout group after grid layout group is rendered.

+Using Invoke(“GetRectSet”, 0.01f); as mention by OJ3D ,
+Using Coroutine as mention by Suduckgames
->are just temporary fixes , not recommended.

+This is NOT a “absurd bug and quite embarrassing it has been around for 3 years without being fixed or addressed in any way by Unity.” as mention by ehudcandivore.

Using Canvas.ForceUpdateCanvases(); as mention by Tymianek is the best overall solution to the problem.

1 Like