Sizing elements to fit their contents in Unity UI
April 10th, 2019 (edited November 3rd, 2022)You have a UI widget that contains a number of different elements (text, images, etc). These are all children of a background element, which you want to automatically resize to encompass all of these children.
You read a bunch of forum threads and the documentation and it just won't work. Me too. I figured it out, though, so I'm documenting it here. This was written with reference to Unity 2018.3.3.
To demonstrate, I'll be creating a tooltip. I'm starting with a naive implementation - a few different widgets that are children of a VerticalLayoutGroup.
The first important concept to understand is the idea of "preferred" size. Preferred size is an internal value that some UI components have (specifically, ones that implement the ILayoutElement interface). It doesn't have anything to do with any of the values of the RectTransform on that element. Some examples of components that have a preferred size:
- Image - The preferred size is the original size of the sprite being used.
- Text - The preferred size is the size of the actual visible text.
- Layout Groups - The preferred size is the size of the bounds encompassing all of the group's children.
- Layout Element - Allows you manually set the preferred size for an object.
By default, Layout Groups look at the RectTransform size of their children when deciding how to position them, not the preferred size1. Remember that for Text components, the visual size of the rendered text is the preferred size, not the RectTransform size. So in order to include Text in a Layout Group and have it respond to the actual rendered text, we need to set the text's RectTransform size to match its preferred size.
There is a built-in component for this called ContentSizeFitter. When attached to a GameObject with a UI component on it, it can set the RectTransform size of that object to match the preferred size.
I'll attached ContentSizeFitters to each of the Text objects and set the Vertical Fit to "Preferred Size". You may have to disable and re-enable the VerticalLayoutGroup to get it to refresh. You'll also see a warning message on the ContentSizeFitter component about the parent layout group, which you can ignore for now (but see footnote 1).
This is getting closer, but the background (an Image component on the Tooltip object) isn't resizing yet. Remember that the preferred size of the VerticalLayoutGroup is the size of the bounds encompassing all its children. So let's just add a ContentSizeFitter to the VerticalLayoutGroup object and set both fits to "Preferred Size".
And that's pretty much it. You can use the Padding and Spacing properties on the Layout Group to clean it up.
1 Layout Groups have two checkboxes for "Child Controls Size", one for the Width and one for the Height. Checking these boxes causes the Layout Group to automatically perform the function of the ContentSizeFitter - changing the RectTransforms of the children to match their preferred size. This allows you achieve the same result without ContentSizeFitters, and it is apparently how Unity now intends it to be done; however, it affects ALL the children of the layout group. That includes the divider in this case, which we didn't want to resize. We could have fixed this by adding a LayoutElement component to the divider and setting its preferred height to 1.
One more thing
What if I wanted to introduce a second part of the background - a border? Ordinarily, I would make the border a child of the background and set the RectTransform Anchors to stretch both dimensions. But that won't work in this case because the background can no longer find the content items (Layout Groups only operate on their immediate children), so it won't grow to match them.
What we really want in this case is to pass the size up from a Layout Group on the border. I move the Layout Group and ContentSizeFitter from the background to the border. Now the border is sizing correctly, but I need to get the background to match it as well.
Remember that Layout Groups set their own preferred size to the size of the bounds encompassing all of the group's children. So we can achieve this by adding a Horizontal or Vertical Layout Group to the background (they will produce the same result with only one child), which sets the preferred size, and then adding a ContentSizeFitter to the background, resizing the RectTransform to match.