UGUI为我们提供了强大的布局系统,而且使用非常简单,只需要在对应的GameObject节点下,添加相应的Layout组件(如HorizontalLayoutGroup,GridLayoutGroup等)即可,本篇文章来看看它们是如何实现的。
CanvasScaler.cs在之前的文章中已经介绍过了,这里就不介绍了,我们着重看下其他Layout的实现原理。
关于各个Layout组件的使用,请查看这篇文章Auto Layout 自动布局,强烈建议看本篇文章之前,详细看下该篇文章。
1.0 Layout System类图
依旧我们先给出UGUI整个Layout Stystem的类图结构,如下:
- ILayoutGroup接口控制子物品的RectTransfrom大小。
- ILayoutSelfConotroller接口可以根据子物体的RectTrasfrom大小,设置自身的RectTransfrom的大小。
- ILayoutIgnorer接口用于LayoutElement组件,忽略Ignore Layout使用。
2.0 Layout System入口
之前的文章中有讲过,当UI Mesh网格需要Rebuild时,会自动调用LayoutRebuilder的Rebuild()方法,更新需要重新布局的元素。
PerformLayoutCalculation()方法会递归计算UI元素的宽高(先计算子元素,在计算自身元素)
ILayoutElement.CalculateLayoutInputXXXXXX()在具体的实现类中计算该UI的大小
PerformLayoutControl()方法会递归设置UI元素的宽高(先设置自身元素,在设置子元素)
ILayoutController.SetLayoutXXXXX()在具体的实现类中设置该UI的大小
因此我们主要来看一下这俩个方法的逻辑CalculateLayoutInputXXXXXX()和SetLayoutXXXXX(),前者是ILayoutElement接口提供的方法,用于获取自身元素的大小,后者是ILayoutController接口提供的方法,用于设置自身元素或者子元素的大小。
LayoutRebuilder.cs部分源码如下:
public void Rebuild(CanvasUpdate executing)
{
switch (executing)
{
case CanvasUpdate.Layout:
// It's unfortunate that we'll perform the same GetComponents querys for the tree 2 times,
// but each tree have to be fully iterated before going to the next action,
// so reusing the results would entail storing results in a Dictionary or similar,
// which is probably a bigger overhead than performing GetComponents multiple times.
PerformLayoutCalculation(m_ToRebuild, e => (e as ILayoutElement).CalculateLayoutInputHorizontal());
PerformLayoutControl(m_ToRebuild, e => (e as ILayoutController).SetLayoutHorizontal());
PerformLayoutCalculation(m_ToRebuild, e => (e as ILayoutElement).CalculateLayoutInputVertical());
PerformLayoutControl(m_ToRebuild, e => (e as ILayoutController).SetLayoutVertical());
break;
}
}
3.0 LayoutElement组件
为了开发者方便使用,UGUI为我们提供了LayoutElement组件,该组件实现了ILayoutElement和ILayoutIgnorer接口。
可以看出LayoutElement提供了各种宽度和高度值,如m_MinWidth ,m_PreferredWidth ,m_FlexibleHeight 相关的作用可以看文章开头推荐的文章Auto Layout 自动布局。
LayoutElement.cs部分源码如下:
public class LayoutElement : UIBehaviour, ILayoutElement, ILayoutIgnorer
{
[SerializeField] private bool m_IgnoreLayout = false;
[SerializeField] private float m_MinWidth = -1;
[SerializeField] private float m_MinHeight = -1;
[SerializeField] private float m_PreferredWidth = -1;
[SerializeField] private float m_PreferredHeight = -1;
[SerializeField] private float m_FlexibleWidth = -1;
[SerializeField] private float m_FlexibleHeight = -1;
[SerializeField] private int m_LayoutPriority = 1;
public virtual void CalculateLayoutInputHorizontal() {}
public virtual void CalculateLayoutInputVertical() {}
protected override void OnEnable()
{
base.OnEnable();
SetDirty();
}
protected override void OnTransformParentChanged()
{
SetDirty();
}
protected override void OnDisable()
{
SetDirty();
base.OnDisable();
}
protected override void OnDidApplyAnimationProperties()
{
SetDirty();
}
protected override void OnBeforeTransformParentChanged()
{
SetDirty();
}
protected void SetDirty()
{
if (!IsActive())
return;
LayoutRebuilder.MarkLayoutForRebuild(transform as RectTransform);
}
}
CalculateLayoutInputHorizontal(),CalculateLayoutInputVertical虚方法用于派生类重写,用于对各种宽度和高度值赋值。
m_IgnoreLayout 用于决定该LayoutElement是否有效。
当有多个 LayoutElement组件,使用m_LayoutPriority决定优先级。
3.0 LayoutUtility静态类
上文提到ILayoutController.SetLayoutXXXXX()会设置元素的尺寸,那么它是如何获取到LayoutElement的各种尺寸的信息呢,通过查看源码可以看到,它是通过LayoutUtility类实现的。
LayoutUtility是个辅助的静态类,通过该类可以获取各种尺寸信息(MinSize,PreferredSize,FlexibleSize)。
这里以GetPreferredWidth为例介绍,关键方法GetLayoutProperty(),过程如下:
- 首先获取该ILayoutController下的所有的ILayoutElement
- 然后根据layoutPriority获取最大权重的LayoutElement
- 最后通过Func委托,获取e => e.minWidth与 e => e.preferredWidth的最大值当做PreferredWidth。
LayoutUtility.cs部分源码如下:
public static class LayoutUtility
{
public static float GetPreferredSize(RectTransform rect, int axis)
{
if (axis == 0)
return GetPreferredWidth(rect);
return GetPreferredHeight(rect);
}
public static float GetPreferredWidth(RectTransform rect)
{
return Mathf.Max(GetLayoutProperty(rect, e => e.minWidth, 0), GetLayoutProperty(rect, e => e.preferredWidth, 0));
}
public static float GetLayoutProperty(RectTransform rect, System.Func<ILayoutElement, float> property, float defaultValue)
{
ILayoutElement dummy;
return GetLayoutProperty(rect, property, defaultValue, out dummy);
}
public static float GetLayoutProperty(RectTransform rect, System.Func<ILayoutElement, float> property, float defaultValue, out ILayoutElement source)
{
source = null;
if (rect == null)
return 0;
float min = defaultValue;
int maxPriority = System.Int32.MinValue;
var components = ListPool<Component>.Get();
rect.GetComponents(typeof(ILayoutElement), components);
for (int i = 0; i < components.Count; i++)
{
var layoutComp = components[i] as ILayoutElement;
if (layoutComp is Behaviour && !((Behaviour)layoutComp).isActiveAndEnabled)
continue;
int priority = layoutComp.layoutPriority;
// If this layout components has lower priority than a previously used, ignore it.
if (priority < maxPriority)
continue;
float prop = property(layoutComp);
// If this layout property is set to a negative value, it means it should be ignored.
if (prop < 0)
continue;
// If this layout component has higher priority than all previous ones,
// overwrite with this one's value.
if (priority > maxPriority)
{
min = prop;
maxPriority = priority;
source = layoutComp;
}
// If the layout component has the same priority as a previously used,
// use the largest of the values with the same priority.
else if (prop > min)
{
min = prop;
source = layoutComp;
}
}
ListPool<Component>.Release(components);
return min;
}
}
4.0 ILayoutSelfController
ILayoutController分为俩种,一种是控制自身元素尺寸,一种是控制子元素尺寸,这里先介绍前者ILayoutSelfController。
4.1 ContentSizeFitter组件
ContentSizeFitter组件可以Resizes a RectTransform to fit the size of its content,所挂载的对象上必须要有ILayoutElement。
rectTransform属性通过GetComponent()方法获取自身rectTransform,然后通过 LayoutUtility获取对应的数值,最后rectTransform.SetSizeWithCurrentAnchors()方法设置RectTransform的尺寸。
ContentSizeFitter.cs源码如下:
public class ContentSizeFitter : UIBehaviour, ILayoutSelfController
{
private RectTransform rectTransform
{
get
{
if (m_Rect == null)
m_Rect = GetComponent<RectTransform>();
return m_Rect;
}
}
private void HandleSelfFittingAlongAxis(int axis)
{
FitMode fitting = (axis == 0 ? horizontalFit : verticalFit);
if (fitting == FitMode.Unconstrained)
{
// Keep a reference to the tracked transform, but don't control its properties:
m_Tracker.Add(this, rectTransform, DrivenTransformProperties.None);
return;
}
m_Tracker.Add(this, rectTransform, (axis == 0 ? DrivenTransformProperties.SizeDeltaX : DrivenTransformProperties.SizeDeltaY));
// Set size to min or preferred size
if (fitting == FitMode.MinSize)
rectTransform.SetSizeWithCurrentAnchors((RectTransform.Axis)axis, LayoutUtility.GetMinSize(m_Rect, axis));
else
rectTransform.SetSizeWithCurrentAnchors((RectTransform.Axis)axis, LayoutUtility.GetPreferredSize(m_Rect, axis));
}
/// <summary>
/// Calculate and apply the horizontal component of the size to the RectTransform
/// </summary>
public virtual void SetLayoutHorizontal()
{
m_Tracker.Clear();
HandleSelfFittingAlongAxis(0);
}
/// <summary>
/// Calculate and apply the vertical component of the size to the RectTransform
/// </summary>
public virtual void SetLayoutVertical()
{
HandleSelfFittingAlongAxis(1);
}
}
这里以ContentSizeFitter挂载到一个Image上为例,看下Image的尺寸信息。
CalculateLayoutInputHorizontal()方法为空,没有对width造成影响,默认返回minWidth为0,preferredWidth为贴图原始宽度,flexibleWidth 为-1。
Image.cs部分源码如下:
public class Image : MaskableGraphic, ISerializationCallbackReceiver, ILayoutElement, ICanvasRaycastFilter
{
public virtual void CalculateLayoutInputHorizontal() {}
/// <summary>
/// See ILayoutElement.minWidth.
/// </summary>
public virtual float minWidth { get { return 0; } }
/// <summary>
/// If there is a sprite being rendered returns the size of that sprite.
/// In the case of a slided or tiled sprite will return the calculated minimum size possible
/// </summary>
public virtual float preferredWidth
{
get
{
if (activeSprite == null)
return 0;
if (type == Type.Sliced || type == Type.Tiled)
return Sprites.DataUtility.GetMinSize(activeSprite).x / pixelsPerUnit;
return activeSprite.rect.size.x / pixelsPerUnit;
}
}
/// <summary>
/// See ILayoutElement.flexibleWidth.
/// </summary>
public virtual float flexibleWidth { get { return -1; } }
4.2 AspectRatioFitter组件
AspectRationFitter组件可以根据设置的AspectMode的模式,和m_AspectRatio比例,来使自身的元素自动设置宽高比。
AspectRatioFitter.cs部分源码如下:
public class AspectRatioFitter : UIBehaviour, ILayoutSelfController
{
private void UpdateRect()
{
if (!IsActive())
return;
m_Tracker.Clear();
switch (m_AspectMode)
{
case AspectMode.HeightControlsWidth:
{
m_Tracker.Add(this, rectTransform, DrivenTransformProperties.SizeDeltaX);
rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, rectTransform.rect.height * m_AspectRatio);
break;
}
case AspectMode.WidthControlsHeight:
{
m_Tracker.Add(this, rectTransform, DrivenTransformProperties.SizeDeltaY);
rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, rectTransform.rect.width / m_AspectRatio);
break;
}
case AspectMode.FitInParent:
case AspectMode.EnvelopeParent:
{
m_Tracker.Add(this, rectTransform,
DrivenTransformProperties.Anchors |
DrivenTransformProperties.AnchoredPosition |
DrivenTransformProperties.SizeDeltaX |
DrivenTransformProperties.SizeDeltaY);
rectTransform.anchorMin = Vector2.zero;
rectTransform.anchorMax = Vector2.one;
rectTransform.anchoredPosition = Vector2.zero;
Vector2 sizeDelta = Vector2.zero;
Vector2 parentSize = GetParentSize();
if ((parentSize.y * aspectRatio < parentSize.x) ^ (m_AspectMode == AspectMode.FitInParent))
{
sizeDelta.y = GetSizeDeltaToProduceSize(parentSize.x / aspectRatio, 1);
}
else
{
sizeDelta.x = GetSizeDeltaToProduceSize(parentSize.y * aspectRatio, 0);
}
rectTransform.sizeDelta = sizeDelta;
break;
}
}
}
}
AspectRationFitter区别于ContentSizeFitter组件,SetLayoutXXX()方法为空,并不依赖于Layout System。而是,每次改变时,通过调用SetDirty()重新设置尺寸。
/// <summary>
/// Method called by the layout system. Has no effect
/// </summary>
public virtual void SetLayoutHorizontal() {}
/// <summary>
/// Method called by the layout system. Has no effect
/// </summary>
public virtual void SetLayoutVertical() {}
/// <summary>
/// Mark the AspectRatioFitter as dirty.
/// </summary>
protected void SetDirty()
{
UpdateRect();
}
public AspectMode aspectMode
{
get { return m_AspectMode; }
set
{
if (SetPropertyUtility.SetStruct(ref m_AspectMode, value))
SetDirty();
}
}
public float aspectRatio
{
get { return m_AspectRatio; }
set
{
if (SetPropertyUtility.SetStruct(ref m_AspectRatio, value))
SetDirty();
}
}
protected override void OnEnable()
{
base.OnEnable();
SetDirty();
}
5.0 ILayoutGroup
ILayoutGroup是用于控制子元素大小的接口,主要有HorizontalLayoutGroup,VerticalLayoutGroup和GridLayoutGroup组件,除此之外,ScrollRect也实现了该接口。
相关继承关系,可以看文章开头的类图,LayoutGroup实现了ILayoutElement, ILayoutGroup接口,说明自身提供了各种尺寸数值和也控制子元素的尺寸。
这里以HorizontalLayoutGroup组件为例,VerticalLayoutGroup和GridLayoutGroup组件与此相似,请自行查看源码,主要就是重写的CalculateLayoutInputXXXX()和SetLayoutHorizontal()。
HorizontalLayoutGroup.cs部分源码如下:
public class HorizontalLayoutGroup : HorizontalOrVerticalLayoutGroup
{
protected HorizontalLayoutGroup()
{}
public override void CalculateLayoutInputHorizontal()
{
base.CalculateLayoutInputHorizontal();
CalcAlongAxis(0, false);
}
public override void CalculateLayoutInputVertical()
{
CalcAlongAxis(1, false);
}
public override void SetLayoutHorizontal()
{
SetChildrenAlongAxis(0, false);
}
public override void SetLayoutVertical()
{
SetChildrenAlongAxis(1, false);
}
}
CalculateLayoutInputHorizontal()方法中调用父类的CalcAlongAxis()方法,根据子元素的尺寸数据,计算出父元素的totalMin,totalPreferred,totalFlexible。
HorizontalOrVerticalLayoutGroup.cs部分源码如下:
/// <summary>
/// Calculate the layout element properties for this layout element along the given axis.
/// </summary>
/// <param name="axis">The axis to calculate for. 0 is horizontal and 1 is vertical.</param>
/// <param name="isVertical">Is this group a vertical group?</param>
protected void CalcAlongAxis(int axis, bool isVertical)
{
float combinedPadding = (axis == 0 ? padding.horizontal : padding.vertical);
bool controlSize = (axis == 0 ? m_ChildControlWidth : m_ChildControlHeight);
bool childForceExpandSize = (axis == 0 ? childForceExpandWidth : childForceExpandHeight);
float totalMin = combinedPadding;
float totalPreferred = combinedPadding;
float totalFlexible = 0;
bool alongOtherAxis = (isVertical ^ (axis == 1));
for (int i = 0; i < rectChildren.Count; i++)
{
RectTransform child = rectChildren[i];
float min, preferred, flexible;
GetChildSizes(child, axis, controlSize, childForceExpandSize, out min, out preferred, out flexible);
if (alongOtherAxis)
{
totalMin = Mathf.Max(min + combinedPadding, totalMin);
totalPreferred = Mathf.Max(preferred + combinedPadding, totalPreferred);
totalFlexible = Mathf.Max(flexible, totalFlexible);
}
else
{
totalMin += min + spacing;
totalPreferred += preferred + spacing;
// Increment flexible size with element's flexible size.
totalFlexible += flexible;
}
}
if (!alongOtherAxis && rectChildren.Count > 0)
{
totalMin -= spacing;
totalPreferred -= spacing;
}
totalPreferred = Mathf.Max(totalMin, totalPreferred);
SetLayoutInputForAxis(totalMin, totalPreferred, totalFlexible, axis);
}
LayoutGroup.cs部分源码如下:
/// <summary>
/// Used to set the calculated layout properties for the given axis.
/// </summary>
/// <param name="totalMin">The min size for the layout group.</param>
/// <param name="totalPreferred">The preferred size for the layout group.</param>
/// <param name="totalFlexible">The flexible size for the layout group.</param>
/// <param name="axis">The axis to set sizes for. 0 is horizontal and 1 is vertical.</param>
protected void SetLayoutInputForAxis(float totalMin, float totalPreferred, float totalFlexible, int axis)
{
m_TotalMinSize[axis] = totalMin;
m_TotalPreferredSize[axis] = totalPreferred;
m_TotalFlexibleSize[axis] = totalFlexible;
}
SetLayoutHorizontal()方法中调用父类的SetChildrenAlongAxis()方法,设置子元素的尺寸大小。
HorizontalOrVerticalLayoutGroup.cs部分源码如下:
/// <summary>
/// Set the positions and sizes of the child layout elements for the given axis.
/// </summary>
/// <param name="axis">The axis to handle. 0 is horizontal and 1 is vertical.</param>
/// <param name="isVertical">Is this group a vertical group?</param>
protected void SetChildrenAlongAxis(int axis, bool isVertical)
{
float size = rectTransform.rect.size[axis];
bool controlSize = (axis == 0 ? m_ChildControlWidth : m_ChildControlHeight);
bool childForceExpandSize = (axis == 0 ? childForceExpandWidth : childForceExpandHeight);
float alignmentOnAxis = GetAlignmentOnAxis(axis);
bool alongOtherAxis = (isVertical ^ (axis == 1));
if (alongOtherAxis)
{
float innerSize = size - (axis == 0 ? padding.horizontal : padding.vertical);
for (int i = 0; i < rectChildren.Count; i++)
{
RectTransform child = rectChildren[i];
float min, preferred, flexible;
GetChildSizes(child, axis, controlSize, childForceExpandSize, out min, out preferred, out flexible);
float requiredSpace = Mathf.Clamp(innerSize, min, flexible > 0 ? size : preferred);
float startOffset = GetStartOffset(axis, requiredSpace);
if (controlSize)
{
SetChildAlongAxis(child, axis, startOffset, requiredSpace);
}
else
{
float offsetInCell = (requiredSpace - child.sizeDelta[axis]) * alignmentOnAxis;
SetChildAlongAxis(child, axis, startOffset + offsetInCell);
}
}
}
else
{
float pos = (axis == 0 ? padding.left : padding.top);
if (GetTotalFlexibleSize(axis) == 0 && GetTotalPreferredSize(axis) < size)
pos = GetStartOffset(axis, GetTotalPreferredSize(axis) - (axis == 0 ? padding.horizontal : padding.vertical));
float minMaxLerp = 0;
if (GetTotalMinSize(axis) != GetTotalPreferredSize(axis))
minMaxLerp = Mathf.Clamp01((size - GetTotalMinSize(axis)) / (GetTotalPreferredSize(axis) - GetTotalMinSize(axis)));
float itemFlexibleMultiplier = 0;
if (size > GetTotalPreferredSize(axis))
{
if (GetTotalFlexibleSize(axis) > 0)
itemFlexibleMultiplier = (size - GetTotalPreferredSize(axis)) / GetTotalFlexibleSize(axis);
}
for (int i = 0; i < rectChildren.Count; i++)
{
RectTransform child = rectChildren[i];
float min, preferred, flexible;
GetChildSizes(child, axis, controlSize, childForceExpandSize, out min, out preferred, out flexible);
float childSize = Mathf.Lerp(min, preferred, minMaxLerp);
childSize += flexible * itemFlexibleMultiplier;
if (controlSize)
{
SetChildAlongAxis(child, axis, pos, childSize);
}
else
{
float offsetInCell = (childSize - child.sizeDelta[axis]) * alignmentOnAxis;
SetChildAlongAxis(child, axis, pos + offsetInCell);
}
pos += childSize + spacing;
}
}
}
GetChildSizes()方法中,通过LayoutUtility.GetMinSize(),LayoutUtility.GetPreferredSize(),LayoutUtility.GetFlexibleSize()获取各种尺寸数据。
private void GetChildSizes(RectTransform child, int axis, bool controlSize, bool childForceExpand,
out float min, out float preferred, out float flexible)
{
if (!controlSize)
{
min = child.sizeDelta[axis];
preferred = min;
flexible = 0;
}
else
{
min = LayoutUtility.GetMinSize(child, axis);
preferred = LayoutUtility.GetPreferredSize(child, axis);
flexible = LayoutUtility.GetFlexibleSize(child, axis);
}
if (childForceExpand)
flexible = Mathf.Max(flexible, 1);
}
最终,通过rect.SetInsetAndSizeFromParentEdge设置子元素尺寸
LayoutGroup.cs部分源码如下:
/// <summary>
/// Set the position and size of a child layout element along the given axis.
/// </summary>
/// <param name="rect">The RectTransform of the child layout element.</param>
/// <param name="axis">The axis to set the position and size along. 0 is horizontal and 1 is vertical.</param>
/// <param name="pos">The position from the left side or top.</param>
protected void SetChildAlongAxis(RectTransform rect, int axis, float pos)
{
if (rect == null)
return;
m_Tracker.Add(this, rect,
DrivenTransformProperties.Anchors |
(axis == 0 ? DrivenTransformProperties.AnchoredPositionX : DrivenTransformProperties.AnchoredPositionY));
rect.SetInsetAndSizeFromParentEdge(axis == 0 ? RectTransform.Edge.Left : RectTransform.Edge.Top, pos, rect.sizeDelta[axis]);
}