Aspect Ratio Fitter not setting the width of a RectTransform

Hi all,

I am trying to create a simple button with an image and some text to look something like this;

Whatever size of the button’s RectTransform I want the image to fill it vertically, and then maintain a square aspect ratio horizontally. I then want the text to fill the rest of the button.

To do this I have put hierarchy like this:

Where ListButton contains a HorizontalLayoutGroup to order the contents and ImageArea contains a AspectRatioFitter to create a square area of the image.

If the HorizontalLayoutGroup is set to control the width of the children it ends up broken like so;

Otherwise it’s broken like this:

The Child Force Expand option doesn’t help either. The only way I have managed to get the result I want is to add a LayoutElement to the ImageArea object and manually set the MinWidth to what I know it should be, which obviously won’t work in all cases. Adding a script to set that MinWidth automatically to be the same as the height causes all sorts of problems.

Any suggestions?

AspectRatioFitter does not work inside layout groups. You should see this warning in the inspector:

The reason is that AspectRatioFitter directly sets its own size. But to work inside layout groups, it would need to implement ILayoutElement to communicate its constraints and let the layout group do the sizing.

The fundamental design issue is that the layout is done in two separate passes, horizontal first and then vertical. So you can’t do cases where the width of an element depends on its height, which is essential to maintain an aspect ratio in all cases. It works when you constrain the element only horizontally but then let it grow freely vertically. In this case, you could implement your own ILayoutElement (and ILayoutController).

You can work around it by forcing two layout passes and using the height of the first pass to calculate the width in the second. But that is very hard to implement so that works reliably in all situations.

Without hacks, it only really works by determining a fixed height beforehand. Then you can set the width of elements (using the LayoutElement script) so that you get the aspect ratio you want. It also makes sense to turn on “Preserve Aspect” on the Image component, so that at least the sprite doesn’t get stretched if the layout constraints cannot be satisfied.

Note that UI Toolkit has also only just gotten support for the aspect-ratio USS property in Unity 6.3b.

Thanks for the information! That set me down the route of looking into layout groups and elements a little more closely and the solution I have gone for is to create a bespoke layout group for the button that handles the layout of the text and image directly. I will post the script here for anyone doing similar things but am open to suggestions for improvements! One minor thing to note is that I am now not fitting the image inside a square.

using UnityEngine;
using UnityEngine.UI;

namespace FMF
{
    [ExecuteAlways]
    [RequireComponent(typeof(RectTransform))]
    public class FMFButtonLayout : LayoutGroup
    {
        [SerializeField]
        private Image Image;

        [SerializeField]
        private TMPro.TMP_Text Text;

        [SerializeField]
        private int Spacing = 0;

        [SerializeField]
        private int MinContentHeight = 100;

        public override void CalculateLayoutInputHorizontal()
        {
            float contentWidth = padding.left + padding.right;
            int itemCount = 0;
            float contentHeight = 0; 

            if (Text != null &&
                Text.gameObject.activeSelf)
            {
                Vector2 textSize = Text.GetPreferredValues();
                contentWidth += textSize.x;
                contentHeight = textSize.y;
                itemCount++;
            }

            contentHeight = Mathf.Max(contentHeight, MinContentHeight);
            if (Image != null &&
                Image.gameObject.activeSelf &&
                Image.mainTexture != null)
            {
                float aspectRatio = Image.mainTexture.width / (float)Image.mainTexture.height;
                float imageWidth = contentHeight * aspectRatio;
                contentWidth += imageWidth;
                itemCount++;
            }

            contentWidth += Spacing * Mathf.Max(itemCount - 1, 0);
            SetLayoutInputForAxis(contentWidth, contentWidth, -1, 0);
        }

        public override void CalculateLayoutInputVertical()
        {
            float contentHeight = 0;
            if (Text != null &&
                Text.gameObject.activeSelf)
            {
                contentHeight = Text.GetPreferredValues().y;
            }

            contentHeight = Mathf.Max(contentHeight, MinContentHeight);
            float height = contentHeight + padding.top + padding.bottom;
            SetLayoutInputForAxis(height, height, -1, 1);
        }

        public override void SetLayoutHorizontal()
        {
            RectTransform rectTransform = GetComponent<RectTransform>();
            bool isImageVisible = Image != null && Image.gameObject.activeSelf && Image.mainTexture != null;
            float availibleHeight = Mathf.Max(rectTransform.rect.height - (padding.top + padding.bottom), 0.0f);
            bool isTextVisible = Text != null && Text.gameObject.activeSelf;
            float textPreferedWidth = isTextVisible ? Text.GetPreferredValues().x : 0.0f;
            float usedHeight = isTextVisible ? Mathf.Min(availibleHeight, Text.GetPreferredValues().y) : availibleHeight;
            float imageAspectRatio = Image.mainTexture.height > 0.0f ? (Image.mainTexture.width / (float)Image.mainTexture.height) : 1.0f;
            float imagePreferedWidth = isImageVisible ? usedHeight * imageAspectRatio : 0.0f;
            float spacingWidth = isImageVisible && isTextVisible ? Spacing : 0.0f;
            float availibleWidth = Mathf.Max(rectTransform.rect.width - (padding.right + padding.left + spacingWidth), 0.0f);
            float totalPreferedContentWidth = imagePreferedWidth + textPreferedWidth;
            float contentScale = totalPreferedContentWidth > 0.0f ? Mathf.Min(availibleWidth / totalPreferedContentWidth, 1.0f) : 0.0f;
            float totalContentWidth = contentScale * totalPreferedContentWidth;
            float excessWidth = availibleWidth - totalContentWidth;

            float alignmentPadding = 0.0f;
            switch (childAlignment)
            {
                case TextAnchor.LowerCenter:
                case TextAnchor.MiddleCenter:
                case TextAnchor.UpperCenter:
                {
                    alignmentPadding = excessWidth * 0.5f;
                    break;
                }

                case TextAnchor.LowerRight:
                case TextAnchor.MiddleRight:
                case TextAnchor.UpperRight:
                {
                    alignmentPadding = excessWidth;
                    break;
                }
            }

            float currentPosition = padding.left + alignmentPadding;
            float scaledImageWidth = imagePreferedWidth * contentScale;
            SetChildAlongAxis(Image.GetComponent<RectTransform>(), 0, currentPosition, scaledImageWidth);
            currentPosition += (scaledImageWidth + spacingWidth);
            float scaledTextWidth = textPreferedWidth * contentScale;
            SetChildAlongAxis(Text.GetComponent<RectTransform>(), 0, currentPosition, scaledTextWidth);
        }

        public override void SetLayoutVertical()
        {
            RectTransform rectTransform = GetComponent<RectTransform>();
            float availibleHeight = Mathf.Max(rectTransform.rect.height - (padding.top + padding.bottom), 0.0f);
            bool isTextVisible = Text != null && Text.gameObject.activeSelf;
            float usedHeight = isTextVisible ? Mathf.Min(availibleHeight, Text.GetPreferredValues().y) : availibleHeight;
            float excessHeight = availibleHeight - usedHeight;

            float alignmentPadding = 0.0f;
            switch (childAlignment)
            {
                case TextAnchor.MiddleLeft:
                case TextAnchor.MiddleCenter:
                case TextAnchor.MiddleRight:
                {
                    alignmentPadding = excessHeight * 0.5f;
                    break;
                }

                case TextAnchor.LowerLeft:
                case TextAnchor.LowerCenter:
                case TextAnchor.LowerRight:
                {
                    alignmentPadding = excessHeight;
                    break;
                }
            }

            SetChildAlongAxis(Text.GetComponent<RectTransform>(), 1, padding.top + alignmentPadding, usedHeight);
            SetChildAlongAxis(Image.GetComponent<RectTransform>(), 1, padding.top + alignmentPadding, usedHeight);
        }
    }
}