系列
UGUI源码分析系列总览
相关前置:
UGUI CanvasUpdateSystem源码分析
UGUI源码分析:LayoutSystem布局系统
文章目录
- 系列
- UML图一览
- LayoutGroup
- 属性介绍
- 布局过程
UML图一览
LayoutGroup
BaseClass: UIBehaviour
Interface: ILayoutElement, ILayoutGroup
Intro:布局系统的实施组件的基类
虽然UGUI组件中有一些组件都继承了ILayoutElement接口,但并不会涉及对接口方法的实现。这是因为这些组件主要是布局操作的接收方,只需要通过该接口被布局实施方所发现即可。
LayoutGroup,是布局组件的基类(GridLayoutGroup、HorizontalOrVerticalLayoutGroup)。本文主要针对纵横布局组件(HorizontalLayoutGroup、VerticalLayoutGroup)进行分析。
属性介绍
- Padding:内部边距,调整实际用于布局区域的大小
- Spacing :子物体直接的间隔
- Child Alignment :子物体对齐方式
- Child Controls Size :组件控制子物体尺寸开关,开启时组件可以更改物体尺寸。
- Child Force Expand :组件控制子物体填充区域开关,若可以修改尺寸则会改变子物体尺寸填充区域,若不可以修改尺寸,则根据区域大小均衡分布子物体。
布局过程
接下来,作者以HorizontalLayoutGroup为例,逐步进行布局实现的分析。
延续LayoutRebuilder的Rebuild方法(详情:UGUI源码分析:LayoutSystem布局系统),首先被执行的是ILayoutElement的CalculateLayoutInputHorizontal方法。该方法将收集其子节点下所有没有被标记ignoreLayout的物体(m_RectChildren)。
// HorizontalLayoutGroup
public override void CalculateLayoutInputHorizontal()
{
// 基类(LayoutGroup)方法
base.CalculateLayoutInputHorizontal();
CalcAlongAxis(0, false);
}
//LayoutGroup
public virtual void CalculateLayoutInputHorizontal()
{
//清空list,准备收集子节点下没有被ignoreLayout的物体
m_RectChildren.Clear();
var toIgnoreList = ListPool<Component>.Get();
for (int i = 0; i < rectTransform.childCount; i++)
{
var rect = rectTransform.GetChild(i) as RectTransform;
if (rect == null || !rect.gameObject.activeInHierarchy)
continue;
rect.GetComponents(typeof(ILayoutIgnorer), toIgnoreList);
if (toIgnoreList.Count == 0)
{
m_RectChildren.Add(rect);
continue;
}
for (int j = 0; j < toIgnoreList.Count; j++)
{
var ignorer = (ILayoutIgnorer)toIgnoreList[j];
if (!ignorer.ignoreLayout)
{
m_RectChildren.Add(rect);
break;
}
}
}
ListPool<Component>.Release(toIgnoreList);
m_Tracker.Clear();
}
CalcAlongAxis 主要是做LayoutGroup的一些初始化参数的计算。
//LayoutGroup
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);
//根据轴设置 m_TotalXXX值
SetLayoutInputForAxis(totalMin, totalPreferred, totalFlexible, axis);
}
STEP2:接着会执行ILayoutController的SetLayoutHorizontal方法。这在GridLayoutGroup、HorizontalLayoutGroup、VerticalLayoutGroup中有不同的处理。
//HorizontalLayoutGroup
public override void SetLayoutHorizontal()
{
//根据轴设置子物体的布局
SetChildrenAlongAxis(0, false);
}
LayoutGroup组件如何调整子物体的位置与大小一句话概括:利用了Unity RectTransform中的一个方法
SetInsetAndSizeFromParentEdge(RectTransform.Edge edge, float inset, float size);
布局物体的方法主要是在 选择出目标边(Edge),计算出距离(inset),计算出子物体的大小(size)。
//LayoutGroup
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)); // 当二者不同时为true 例(水平 y轴,垂直 x轴)
if (alongOtherAxis)
{
//在水平或垂直布局中,另外一条轴的布局操作相对简单一些
//实际尺寸,根据padding计算
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)
{
// 根据轴选取矩形的边,以及距离、尺寸,设置子物体的位置(API:SetInsetAndSizeFromParentEdge)
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)
{
// 根据轴选取矩形的边,以及距离、尺寸,设置子物体的位置(API:SetInsetAndSizeFromParentEdge)
SetChildAlongAxis(child, axis, pos, childSize);
}
else
{
float offsetInCell = (childSize - child.sizeDelta[axis]) * alignmentOnAxis;
SetChildAlongAxis(child, axis, pos + offsetInCell);
}
//更新距离,累计子物体尺寸与间隔
pos += childSize + spacing;
}
}
}
到此,纵横布局组件的布局分析就完毕啦,下篇文章作者会对GridLayoutGroup(网格布局组件)以及ILayoutController另一个衍生ContentSizeFitter(尺寸调节组件)进行详细分析。
.
.
.
.
.
嗨,我是作者Vin129,逐儿时之梦正在游戏制作的技术海洋中漂泊。知道的越多,不知道的也越多。希望我的文章对你有所帮助:)