I have a component which is using DrivenRectTransformTracker which works well, the RectTransform inspector shows that my component is controlling the values. I have a couple of questions though:
1.) When I move the GameObject from inside a LayoutGroup to outside of it I spot the transition using OnTransformParentChanged() and reapply my changes also calling DrivenRectTransformTracker.Add(), however the inspector does not show any of the RectTransform values as being driven by anything. It’s as though after the OnTransformParentChanged call the LayoutGroup is somehow clearing the fact that I’m driving the values.
2.) How should my component play nice with layout. It implements ILayoutElement and returns the relevant values, but I really need to know when to switch from driving the RectTransform myself and driving it through the values returned by the ILayoutElement interface.
I can see the manual page about Auto Layout doesn’t mention this; I’ll get it updated.
The LayoutUtility class will get the calculated values for minimum, preferred and flexible size, use default values if no ILayoutElement is present, and get the values form the ILayoutElement with the highest priority that overrides that property, if multiple are present.
Just confirming that point 1 is fixed in beta 21, thank you.
I still need some clarification on point 2 though, I think I need to explain my use case better.
My component implements ILayoutElement and returns the relevant sizes etc, which all works well when the component is a child of a LayoutGroup.
If my component is outside of a layout group I want it to drive the SizeDelta of the RectTransform it’s self as my understanding is that the Layout values are ignored in this instance.
My current solution works however I feel uneasy about it as there’s no documentation to say that the behaviour is as expected.
Currently I continue to drive the SizeDelta even when it’s a child of a LayoutGroup. The setting of SizeDelta only occurs when something on the component is changed. In this scenario somehow the LayoutGroup is managing to override the SizeDelta values I set. Which is great.
In OnTransformParentChanged I also update the SizeDelta so that when the component is dragged out of the LayoutGroup it regains it’s directly driven size. But this would not catch all occurrences, for example enabling disabling of the layout group.
I feel there should be a way to be notified when a ILayoutElement begins being controlled by a layout group and when it is no longer controlled.
Children of a layout group should never be driven by anything else than that layout group. RectTransforms are not designed to be driven by more than one object at once.
I would recommend to split your components into two parts: One part that returns the ILayoutElement values, and another that can be used to drive the sizeDelta (if e.g. ContentSizeFitter doesn’t already work for you for this) - the latter should only be used on the GameObject if it’s not the child of a layout group.
We don’t have specific notifications about when an object start being driven or not. This is by design. The intention is that your code will run at all relevant changes, and it will simply drive the properties it should drive. Code involving DrivenRectTransformTracker should always start by calling Clear and then add properties to drive - or not! Even if no properties are added, the Clear should still be called. This is what ensures that only properties that are relevant to be driven in the current conditions are actually set to be driven.
Yes that’s what I’m trying to do; ensure my component does not drive the recttransform if it is being driven by a layout group.
How should my component work out if it is currently controlled by a layout group or not? And how should it update this status which can change outside of my control, as ideally I don’t want it having to scan up the hierarchy in Update to see if there’s an active Layout Group.
My design is that when the component is part of a layout group it will return a recommended size x etc but when it is not it will set the size to x.
I want to ensure the component works wherever it’s put rather than expect the user to toggle a flag or use a different component.
That doesn’t work well with the layout system. The system needs to know what your component controls and it determines this by checking what interfaces your component implements. If it implements ILayoutSelfController it’s expected to drive the RectTransform, and if it doesn’t implement it then it’s expected not to drive it. You can’t have it both ways. You might find workarounds, but it’s not intended to be done with this layout system so there’s no “recommended” way to do this, and it will likely cause trouble down the road and be more confusing for users.
I’m still kind of hoping that we are talking crossed purposes as I do not want to set the RectTransform if the Layout system is controlling it.
If there is a layout group i.e. the layout system is active and controlling my component then I would like my component to be controlled by it, at no point do I wish to override the RectTransform if the component is controlled by the layout system. I just need to know if it is controlled by the layout system.
Here’s a simplified example of what I am trying to achieve, an Image component that uses the native size to drive the layout system if it’s part of one otherwise set the rect transform to that size… My UI is determined by the size of the sprites rather than having sizes hard set in rect transforms.
Give the script a go, it appears to work really well, and copes with dragging the object in and out of layout groups. The only thing that I’ve found that it does not cope with is if a layout group is enabled / disabled, which is quite an edge case but enough to frustrate me.
Note I’m not using ILayoutSelfController I don’t think I want to here as Image implements ILayoutElement and I’m only trying to drive the RectTransform when no layout system is involved.
I’ve also attached a test project to make it quicker to test.
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
// This class extends UnityEngine.UI.Image so that it's rect size always matches the source's native size
[AddComponentMenu("UI/Image Native Size", 10)]
public class UIImageNative : Image
{
protected DrivenRectTransformTracker m_Tracker;
Vector2 size;
void SetSize()
{
if (this.overrideSprite == null) return;
Vector2 prevSize = base.rectTransform.sizeDelta;
m_Tracker.Clear();
size.x = overrideSprite.rect.width / pixelsPerUnit;
size.y = overrideSprite.rect.height / pixelsPerUnit;
if (!IsLayoutControlled())
{
// Only drive the rect transform if we are not driven by a layout system
m_Tracker.Add(this, transform as RectTransform, DrivenTransformProperties.SizeDelta);
base.rectTransform.anchorMax = base.rectTransform.anchorMin;
base.rectTransform.sizeDelta = size;
this.SetAllDirty();
}
if (name.CompareTo("Test") == 0)
{
Debug.Log("SetSize to " + base.rectTransform.sizeDelta + " was=" + prevSize);
}
}
bool IsLayoutControlled()
{
Transform t = transform;
while (t != null)
{
MonoBehaviour layout = t.GetComponent(typeof(ILayoutGroup)) as MonoBehaviour;
if (layout != null && layout.enabled)
{
return true;
}
t = t.parent;
}
return false;
}
public override float preferredHeight
{
get
{
return size.y;
}
}
public override float preferredWidth
{
get
{
return size.x;
}
}
protected override void OnEnable()
{
base.OnEnable();
SetSize();
}
public override void OnAfterDeserialize()
{
base.OnAfterDeserialize();
SetSize();
}
protected override void OnValidate()
{
base.OnValidate();
SetSize();
}
protected override void OnTransformParentChanged()
{
Debug.Log("OnTransformParentChanged");
base.OnTransformParentChanged();
SetSize();
}
protected override void OnDisable()
{
this.m_Tracker.Clear();
base.OnDisable();
}
protected override void OnDidApplyAnimationProperties()
{
this.OnDidApplyAnimationProperties();
SetSize();
}
}
P.S. Thanks so much for making many of Image’s functions virtual in beta 21 making the above much easier. Any chance of making sprite and overrideSprite accessors virtual too so that derived classes can update when the sprite changes?
@runevision Intended or not, the Layout system apparently has no problem with this, except where Unity’s own code rejects / ignores these situations?
My proof that it works (and I’ve made some ridiculously complex GUIs with this, animated them, and added the standard UnityUI components in too - all works, no problems) is to override UIBehaviour. But there are bugs in that class that break non-UnityEditor builds (eg the UnityEngine and UnityEditor copies of the class are mutually incompatible! Which is disturbing in its own right :)), and that is easy to workaround too - except when you try to make DLL builds (and lose the ability to see UnityEditor API calls that allow you to implement layout correctly).
I’m trying to remove all the hacks, so that it’s shippable as DLL’s. But I keep running into things like this - things that appear to be simple mistakes in UnityUI where the Editor creates artificial architectural limits that … don’t exist in the implementation.
TL;DR: if you try to ship a DLL, and work within the artificial constraints that Unity has tacked-on to the layout system, then it cannot do these basic things. If you ship source code, then everything works perfectly.
Also, @runevision it would be great if someone could update the docs to say that the obvious, desirable thing (implement ILayoutSelfController and e.g. ILayoutGroup on the same class - they are interfaces, and the contract that Unity’s docs describe is easy for classes to implement both) is not only “not supported” but actively blocked by Unity.