Sizing a RectTransform based on Aspect Ratio, or alternatively restricted Content Size Fitter

I would also love some input from UT on this, if it’s not already possible with UI components.

To summarise, I’m trying to implement a message box that will automatically resize based on its contents.
We previously did this manually in NGUI, but I’ve been trying to use the new UI components to automate it more. (as I feel it should be possible in a robust UI system)

Let me show you what we have and what we want, and if this needs to be something Unity include in their API.

Desired Behaviour


So this is our message box, pretty basic, a title Text, a content Text, and a Button (which sits within a Horizontal Layout Group, but that’s unrelated).

When more text is added, the box should resize to fit it. This is possible using a Vertical Layout Group and a Content Size Fitter set to Vertical Fit: Preferred Size. This then scales the height up to fit the new text, this is great, but looks a bit odd when it gets too tall.

Ideally we would want it to scale in an even manner, or at a specifiable aspect ratio (for want of a better word).
I have been unable to get this working with the standard UI components. If there is a way, I’d love to know how.

The problem with using the Horizontal Fit, is that the text will make the width of the box too large, as the text you see above is one line. See:

This is what we want:

As you can see, the width has scaled up just enough to keep the general aspect of the box, while fitting all contents. This is done through a (pretty messy right now) small script sitting on the RectTransform of the message box.

Solution (sort of)

So, I won’t go into the details of how the whole box is set up right now, but I can do if it becomes relevant/people want to know. I can upload an example project potentially at some point too, if needed.

The script:

using UnityEngine;
using System.Collections;

/// <summary>
/// This component resizes a panel (or any object with a RectTransform), to try to fit the specified aspect ratio (within a threshold)
/// </summary>
[ExecuteInEditMode]
public class PanelRatioFitter : MonoBehaviour
{
    #region Public Variables
    /// <summary>
    /// The targetted aspect ratio of the RectTransform
    /// </summary>
    public Vector2 targetAspect = new Vector2(16, 9);
    #endregion

    #region Private Variables
    /// <summary>
    /// A ref of the local RectTransform for performance
    /// </summary>
    RectTransform cachedRect;
    /// <summary>
    /// The percentage 'boundary' within which the aspect ratio is seen to be acceptable ie 1.78 +/- x%
    /// </summary>
    float aspectDelta = 5f;
    /// <summary>
    /// A store of the previous frames rect height, used to check if the height has changed
    /// </summary>
    float previousHeight;

    /// <summary>
    /// A check to see if the resize is attempting to flick between 2 close-to-target ratios
    /// </summary>
    bool loopingCheck1;
    /// <summary>
    /// A check to see if the resize is attempting to flick between 2 close-to-target ratios
    /// </summary>
    bool loopingCheck2;

    bool resizing;
    #endregion

    #region Unity Functions
    // Use this for initialization
    void Start ()
    {
        cachedRect = GetComponent<RectTransform>();
    }

    void Update()
    {
        if (cachedRect)
        {
            // If the height of the rect has changed, check and perform the resize
            if (Mathf.Abs(cachedRect.sizeDelta.y - previousHeight) > 0.01f)
            {
                StartCoroutine(Resize());
                previousHeight = cachedRect.sizeDelta.y;
            }
            else
            {
                loopingCheck1 = loopingCheck2 = false;
            }
        }
        else
        {
            cachedRect = GetComponent<RectTransform>();
        }
    }
    #endregion

    #region Custom Functions
    IEnumerator Resize()
    {
        // if the function is found to have 'flicked' between 2 acceptable aspect ratios (and therefor can't decide
        // which to keep), this makes sure it does not continue attempting to resize.
        if ((loopingCheck1 && loopingCheck2) || resizing)
            yield break;

        resizing = true;

        float increment = 10f;
        // First loop to check the current aspect ratio being lower than the targetted ratio, slightly increasing the width until it is acceptable
        while (cachedRect.sizeDelta.x / cachedRect.sizeDelta.y < ((targetAspect.x / targetAspect.y) - (((targetAspect.x / targetAspect.y) / 100f) * aspectDelta)))
        {
            cachedRect.sizeDelta = new Vector2((cachedRect.sizeDelta.x) + increment, cachedRect.sizeDelta.y);

            increment += 10f;
            if (increment > 100)
            {
                break;
            }

            yield return new WaitForEndOfFrame();

            loopingCheck1 = true;
        }
        increment = 10f;
        // Now loop to check the current aspect ratio being higher than the targetted ratio, slightly decreasing the width until it is acceptable
        while (cachedRect.sizeDelta.x / cachedRect.sizeDelta.y > ((targetAspect.x / targetAspect.y) + (((targetAspect.x / targetAspect.y) / 100f) * aspectDelta)))
        {
            cachedRect.sizeDelta = new Vector2((cachedRect.sizeDelta.x) - increment, cachedRect.sizeDelta.y);

            increment += 10f;
            if (increment > 100)
            {
                break;
            }

            yield return new WaitForEndOfFrame();
         
            loopingCheck2 = true;
        }

        resizing = false;
    }
    #endregion
}

It’s a pretty bad way of doing it, but it works for the most part.
There are a few bugs, but this is a really quick test implementation.

The main problem (other than the fact it’s not built in AFAIK), which admittedly is minor, is that it doesn’t update correctly in the editor. I’ve tried a few things to get it working, a custom editor script, using ExecuteInEditMode, manually calling SceneView redraws. But my thinking eventually was, should this just be something the UI system can do natively?

And that’s my real question, though input on the rest of the implementation also welcome, of course.
And if this was to be in Unity (Assuming it isn’t already and I just don’t know how to do it), should it be a separate component (like my script), or a ‘restrict ratio’ or ‘restrict x/y’ input to the Content Size Fitter?

What are peoples thoughts? And UT’s, if anyone’s about?

Sorry for the super long post.

TL DR - Is there a way/Should there be a way, to restrict the Content Size Fitter by a ratio or specific size?

Hey There,

What I would suggest is not using Update to do this. Unity has a lot of built in interfaces that you should use.

using UnityEngine.UI;

public class AspectBoxFitter : UIBehaviour, ILayoutSelfController
{
protected override void OnBeforeTransformParentChanged()
{
//Do your resizing logic here
}

public void SetLayoutHorizontal()
{
//You don’t really need this but the interface is important
}

public void SetLayoutVertical()
{
//You don’t really need this but the interface is important
}
}

Thanks for the input, that is certainly a neater way of structuring it.

I guess my reason for the thread is more that I’m unsure if the way I’ve gone about doing this is the best way. As it feels like this is something the new UI should be able to handle on it’s own, it handles resizing automatically, but there is just no way to control it in any finer way.

Looking back my original post is far too long considering what I’m actually asking, oops!

There’s no way in the auto layout system to make layout elements size up along both axes to fit a certain content while keeping a certain aspect ratio.

The reason is that the layout is calculated in two distinct phases. First horizontally and then vertically. In the horizontal phase, nothing is known about vertical sizes since they haven’t been calculated yet. In the vertical phase, the horizontal sizes are known but can’t be changed.

The only reason the AspectRatioFitter can work is that it doesn’t take content into account, but it solely dependent on the width, height, or parent rect (depending on mode) while ignoring if the content will fit or not.

It would be possible to implement the feature by iteratively re-calculating the entire layout many times until a size is found with the desired constraints, but it would be rather bad for performance. We have no plans to built-in something like that at the moment.

Thank you for the reply.

Yes I can certainly understand that performance of the iteration could be a concern. I’m going to refine the mess of code in my original post, perhaps using the code BMayne suggested. After that I’ll look into making it more performance friendly where possible.

I think we’ll have to make a decision about whether to use this and take a small perf bump when showing a message initially, or to use pre-defined widths. We may just end up using fixed widths as that is what we currently use in projects with NGUI.

Thanks again for the detailed explanation.