0%

LayoutGroup以及ContentSizeFitter

声明:本文为 jechyang原创文章,欢迎转载,请在明显位置注明出处。

开发过程中在集成ui的时候,有时会出现”Parent has a type of layout group component.A child of a layout group should not have a Content Size Fitter component, since it should be driven by the layout group“的warning,其实就是告诉我们,父节点已经有一个layoutGroup了,即使使用contentSizeFitter,也是应该父节点使用来控制子节点的size,那么这里是如何实现控制的呢,如何集成才能不出现这个warning呢,这里通过源码的阅读来解答这个问题。

这里可以先直接给出去除warning的方法,LayoutGroup勾选ChildControlSize,然后LayoutGroup挂ContentSizeFitter,就可以使用child的PreferredSize/MinSize来控制child了,只要child之间能通过LayoutGroup来传递size,都不用再挂contentsizefitter。

HorizontalLayoutGroup和VerticalLayoutGroup

这两个类其实都继承自类HorizontalOrVerticalLayoutGroup只是对相同方法的不同调用而已,直接以HorizontalLayoutGroup为例进行代码展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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);
}
}

HorizontalOrVerticalLayoutGroup

这个类才是我们水平布局和垂直布局主要实现的地方,直接从上面调用的方法依次看下来,这里需要先说明的是,不管是Calculate的方法,还是SetLayout的方法,我们都是先进行Horizontal方向上的,再进行Vertical方向上的,这个在后面的代码中可以看到。

计算布局部分

首先是调用了base.CalculateLayoutInputHorizontal这个方法,在LayoutGroup里面定义,其实这里调用的四个方法LayoutGroup里也只实现了这个方法,其他都是virtual的空函数体,其实这个方法作用就是获取子节点的所有RectTransform并且去除那些挂有IgnoreLayout属性的节点。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public virtual void CalculateLayoutInputHorizontal()
{
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();
}

接着就是在HorizontalOrVerticalLayoutGroup中实际调用的方法,其实也就是调用了CalcAlongAxis方法,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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);
}

首先是combinedPadding,controlSize以及childForceExpandSize三个参数,分别对应控件面板上的参数选项,就不再多说,比较难以理解的可能是alongOtherAxis这个参数,这个参数的意思就是在计算布局的时候是否在计算它对应的轴向,举个例子,如果我们在HorizontalLayoutGroup中计算当前垂直方向上的大小,这个值就是true。

理解了参数含义之后接着往下看,接下来就是分别获取子节点的preferredSize, minSize,以及flexibleSize的大小,即GetChildSizes方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}

我们在面板中的参数就决定了如何去计算child的size,如果是非contolSize的话,就直接使用child的rect的size来作为min以及preferred的值,如果没有勾选childForceExpand的话flexible就为0,即当我们什么都不勾选的时候,child的size就是面板里的值。这种就适合静态界面的布局。

然后就是controlSize的情况,我们会调用LayoutUtility.GetXxxxxSize方法进行获取大小,这个直接用其中一个方法来看即可,在这里使用GetPreferredSize方法来进行举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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, 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;
}

其实就是通过获取子节点实现了ILayoutElement的Components,然后根据component的layoutPriority的值来获取高优先级的对应值。即同一个child,高layoutProiority的属性值会覆盖低layoutProiority的。

回到上面计算的代码,在得到child的各个类型的size之后,就可以根据alongAxis参数来进行高度的计算了,其实就是如果是对应轴的话,即非alongAxis的情况,对应的size就是叠加的,否则就是取最大值。

最后通过SetLayoutInputForAxis方法将所得到的size存储起来,方法如下

1
2
3
4
5
6
protected void SetLayoutInputForAxis(float totalMin, float totalPreferred, float totalFlexible, int axis)
{
m_TotalMinSize[axis] = totalMin;
m_TotalPreferredSize[axis] = totalPreferred;
m_TotalFlexibleSize[axis] = totalFlexible;
}

这个是继承自LayoutGroup的方法,LayoutGroup也实现了ILayoutElement接口,对应的xxxxWidth/xxxxHeight即从上面的变量中拿到对应轴向的size,0为水平,1为垂直。

计算布局部分就到这里结束了,这个部分之后我们就得到了LayoutGroup的各种size。接着就是根据size去设置child的位置以及size了。

设置child位置以及大小的部分

LayoutGroup实现了ILayoutController的接口,即需要实现SetLayoutHorizontal/Vertical方法,在上面代码展示中我们看到,其实本质也就是调用SetChildrenAlongAxis的方法,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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;
}
}
}

首先是相比于CalcAlongAxis方法多了一个参数alignmentOnAxis,这个参数也就是根据面板里的Child Alignment来获得child应该偏移的位置。然后接着就是根据是否alongOtherAxis来确定child的size以及位置。

首先看alongOtherAxis的情况,我们同样使用GetChildSizes方法来获得child的各种size,然后根据是否是controlSize来对child的pos和size进行设置,这里不要忘记对应的各种size在GetChildSizes方法中也根据是否controlSize进行了处理。然后就是SetChildAlongAxis方法,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void SetChildAlongAxis(RectTransform rect, int axis, float pos, float size)
{
if (rect == null)
return;

m_Tracker.Add(this, rect,
DrivenTransformProperties.Anchors |
(axis == 0 ?
(DrivenTransformProperties.AnchoredPositionX | DrivenTransformProperties.SizeDeltaX) :
(DrivenTransformProperties.AnchoredPositionY | DrivenTransformProperties.SizeDeltaY)
));

rect.SetInsetAndSizeFromParentEdge(axis == 0 ? RectTransform.Edge.Left : RectTransform.Edge.Top, pos, size);
}

即调用SetInsetAndSizeFromParentEdge方法来进行设置,方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void SetInsetAndSizeFromParentEdge(RectTransform.Edge edge, float inset, float size)
{
int index = edge == RectTransform.Edge.Top || edge == RectTransform.Edge.Bottom ? 1 : 0;
bool flag = edge == RectTransform.Edge.Top || edge == RectTransform.Edge.Right;
float num = !flag ? 0.0f : 1f;
Vector2 anchorMin = this.anchorMin;
anchorMin[index] = num;
this.anchorMin = anchorMin;
Vector2 anchorMax = this.anchorMax;
anchorMax[index] = num;
this.anchorMax = anchorMax;
Vector2 sizeDelta = this.sizeDelta;
sizeDelta[index] = size;
this.sizeDelta = sizeDelta;
Vector2 anchoredPosition = this.anchoredPosition;
anchoredPosition[index] = !flag ? inset + size * this.pivot[index] : (float) (-(double) inset - (double) size * (1.0 - (double) this.pivot[index]));
this.anchoredPosition = anchoredPosition;
}

这个方法是RectTransform里面的方法,参数edge代表以父对象的哪一边为基准,inset代表距离所指定的edge的距离,size代表对应edge上rect的size(即top和bottom对应高度,left和right对应宽度)

了解了设置的方法,就可以回到上面的方法接着看各个size的含义了,首先是requiredSpace,即当前的child所需要的空间大小,根据child是否flex,将max设定在了LayoutGroup的rect的size或者child的prefered size,min即为child的min值(这里感觉说起来有点不太好理解,可以自行使用一个Horizontal Layout,然后把child的高度设置的比LayoutGroup的高度小,然后勾选Child Force Expand和control size试试)。

这里的startOffset是根据设置的childAlignment以及layoutGroup的rect的size以及传入的size来获取需要的偏移量。

然后就是根据是否contolSize来对child进行size的设置,如果是controlSize的话就会传入requiredSize来进行设置,非controlSize的话就会调用SetChildAlongAxis的同名重载函数,其实也就是使用了child本身的rectSize来作为size进行设置。

这里会有一个参数比较迷糊就是offsetInCell,其实就是因为上面的startOffset是根据requireSize来进行计算的,这里因为我们不使用requireSize来对childSize进行设置,而是使用child的rect的size,因此要加上差值。

直到这里,alongAxis的情况就看完了,接着就是非alongAxis的情况

其实和alongOtherAxis的情况差不多,只是pos会递增而已,这里不做多余的解释,就对涉及到的参数做一个讲解

  • pos:每个child在对应axis上的偏移量,如果LayoutGroup的rect有剩余空间并且不会被子物体expand(即totalFlex==0)的情况下就会根据设置的childAlignment有个初始位置偏移

  • minMaxLerp:即当前的size小于LayoutGroup所需的PreferredSize的话,就会对所有的child进行一个等比例缩小。

  • itemFlexibleMultiplier:即提供了flexSize的子物体,要占据剩余空间的每一份的大小,然后子物体的size会加上他们的flexSize*itemFlexibleMultiplier。举个例子就是有ABC三个子物体,totalPreferredSize的大小是100,然后LayoutGroup的size是200,A和C的flexible的值分别为1和9,那么这里的itemFlexibleMultiplier就是10,然后A和C的size分别会加上10和90。

理解完参数之后再代入下面累加size,非along情况下设置child位置以及大小的部分也就结束了。

其实这个地方的代码很多感觉讲解都不是很明白,可以实际在项目里操作一下就知道每个参数是怎么控制的了。重要的就是了解非control size的情况下,我们所有的布局计算和设置都是依赖child当前的rect的size的。

GridLayoutGroup

GridLayoutGroup直接继承LayoutGroup类,同样实现了四个方法,我们依然通过这四个方法来进行查看,首先是CalculateLayoutXXXXx的方法

计算布局部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public override void CalculateLayoutInputHorizontal()
{
base.CalculateLayoutInputHorizontal();

int minColumns = 0;
int preferredColumns = 0;
if (m_Constraint == Constraint.FixedColumnCount)
{
minColumns = preferredColumns = m_ConstraintCount;
}
else if (m_Constraint == Constraint.FixedRowCount)
{
minColumns = preferredColumns = Mathf.CeilToInt(rectChildren.Count / (float)m_ConstraintCount - 0.001f);
}
else
{
minColumns = 1;
preferredColumns = Mathf.CeilToInt(Mathf.Sqrt(rectChildren.Count));
}

SetLayoutInputForAxis(
padding.horizontal + (cellSize.x + spacing.x) * minColumns - spacing.x,
padding.horizontal + (cellSize.x + spacing.x) * preferredColumns - spacing.x,
-1, 0);
}

public override void CalculateLayoutInputVertical()
{
int minRows = 0;
if (m_Constraint == Constraint.FixedColumnCount)
{
minRows = Mathf.CeilToInt(rectChildren.Count / (float)m_ConstraintCount - 0.001f);
}
else if (m_Constraint == Constraint.FixedRowCount)
{
minRows = m_ConstraintCount;
}
else
{
float width = rectTransform.rect.size.x;
int cellCountX = Mathf.Max(1, Mathf.FloorToInt((width - padding.horizontal + spacing.x + 0.001f) / (cellSize.x + spacing.x)));
minRows = Mathf.CeilToInt(rectChildren.Count / (float)cellCountX);
}

float minSpace = padding.vertical + (cellSize.y + spacing.y) * minRows - spacing.y;
SetLayoutInputForAxis(minSpace, minSpace, -1, 1);
}

依然从Horizontal看起来,代码较为简单,其实就是如果没有指定Constraint的话就用1作为minColumns,否则的话其他的就直接使用指定的值,因为是grid布局,所以我们限定了一个方向的cell个数,另一个方向有多少个cell也就明了了。然后根据cellsize计算需要的控空间。

Vertical方向上的话,根据horizontal不同的情况计算出对应的cell个数,然后计算size即可。

设置child位置以及大小部分

GridLayout的SetHorizontal以及SetVertical也是调用了同一个方法SetCellsAlongAxis,指定了不同的轴向而已。这里直接看SetCellsAlongAxis方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
private void SetCellsAlongAxis(int axis)
{
// Normally a Layout Controller should only set horizontal values when invoked for the horizontal axis
// and only vertical values when invoked for the vertical axis.
// However, in this case we set both the horizontal and vertical position when invoked for the vertical axis.
// Since we only set the horizontal position and not the size, it shouldn't affect children's layout,
// and thus shouldn't break the rule that all horizontal layout must be calculated before all vertical layout.

if (axis == 0)
{
// Only set the sizes when invoked for horizontal axis, not the positions.
for (int i = 0; i < rectChildren.Count; i++)
{
RectTransform rect = rectChildren[i];

m_Tracker.Add(this, rect,
DrivenTransformProperties.Anchors |
DrivenTransformProperties.AnchoredPosition |
DrivenTransformProperties.SizeDelta);

rect.anchorMin = Vector2.up;
rect.anchorMax = Vector2.up;
rect.sizeDelta = cellSize;
}
return;
}

float width = rectTransform.rect.size.x;
float height = rectTransform.rect.size.y;

int cellCountX = 1;
int cellCountY = 1;
if (m_Constraint == Constraint.FixedColumnCount)
{
cellCountX = m_ConstraintCount;
cellCountY = Mathf.CeilToInt(rectChildren.Count / (float)cellCountX - 0.001f);
}
else if (m_Constraint == Constraint.FixedRowCount)
{
cellCountY = m_ConstraintCount;
cellCountX = Mathf.CeilToInt(rectChildren.Count / (float)cellCountY - 0.001f);
}
else
{
if (cellSize.x + spacing.x <= 0)
cellCountX = int.MaxValue;
else
cellCountX = Mathf.Max(1, Mathf.FloorToInt((width - padding.horizontal + spacing.x + 0.001f) / (cellSize.x + spacing.x)));

if (cellSize.y + spacing.y <= 0)
cellCountY = int.MaxValue;
else
cellCountY = Mathf.Max(1, Mathf.FloorToInt((height - padding.vertical + spacing.y + 0.001f) / (cellSize.y + spacing.y)));
}

int cornerX = (int)startCorner % 2;
int cornerY = (int)startCorner / 2;

int cellsPerMainAxis, actualCellCountX, actualCellCountY;
if (startAxis == Axis.Horizontal)
{
cellsPerMainAxis = cellCountX;
actualCellCountX = Mathf.Clamp(cellCountX, 1, rectChildren.Count);
actualCellCountY = Mathf.Clamp(cellCountY, 1, Mathf.CeilToInt(rectChildren.Count / (float)cellsPerMainAxis));
}
else
{
cellsPerMainAxis = cellCountY;
actualCellCountY = Mathf.Clamp(cellCountY, 1, rectChildren.Count);
actualCellCountX = Mathf.Clamp(cellCountX, 1, Mathf.CeilToInt(rectChildren.Count / (float)cellsPerMainAxis));
}

Vector2 requiredSpace = new Vector2(
actualCellCountX * cellSize.x + (actualCellCountX - 1) * spacing.x,
actualCellCountY * cellSize.y + (actualCellCountY - 1) * spacing.y
);
Vector2 startOffset = new Vector2(
GetStartOffset(0, requiredSpace.x),
GetStartOffset(1, requiredSpace.y)
);

for (int i = 0; i < rectChildren.Count; i++)
{
int positionX;
int positionY;
if (startAxis == Axis.Horizontal)
{
positionX = i % cellsPerMainAxis;
positionY = i / cellsPerMainAxis;
}
else
{
positionX = i / cellsPerMainAxis;
positionY = i % cellsPerMainAxis;
}

if (cornerX == 1)
positionX = actualCellCountX - 1 - positionX;
if (cornerY == 1)
positionY = actualCellCountY - 1 - positionY;

SetChildAlongAxis(rectChildren[i], 0, startOffset.x + (cellSize[0] + spacing[0]) * positionX, cellSize[0]);
SetChildAlongAxis(rectChildren[i], 1, startOffset.y + (cellSize[1] + spacing[1]) * positionY, cellSize[1]);
}
}

首先是一开始的代码直接处理了axis为0的(即设置水平child)情况,上面的注释也说了,在GridLayout中,SetHorizontal方法只是设置了child的anchorMin/Max以及sizeDelta

然后就是SetVertical的情况,这里依然对几个参数进行解释,计算步骤不做多余的记录。

  • cellCountY/X:即根据面板参数算出来的对应axis上的cell个数。

  • cellsPerMainAxis/actualCellCountX/actualCellCountX:其实就是根据StartAxis参数决定以哪边为主轴,然后使用主轴去计算另外一边的cell个数,主要解决指定的ConstraintCount大于了childrenCount或者flex情况下计算的width以及height不够大的情况。

  • cornerX/Y:这个就和之前的Alignment一样,利用%和/来获得是否从右开始以及从下开始。然后如果是的话cell的位置就要进行一个翻转。

最后就是利用之前提到SetChildAlongAxis的方法设置进去。

ContentSizeFitter

ContentSizeFitter实现了ILayoutSelfController接口,没啥关键的方法,其实就是SetLayoutXXXX的方法里调用了HandleSelfFittingAlongAxis,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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));
}

其实就是设置本身的大小,没啥好看的。

调用时机

了解各个LayoutGroup的计算布局以及设置的方法,接下来需要看一下是如何调用的,顺着代码往上走,发现是在Rebuilder.cs里面的Rebuild方法里调用的,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}
}

然后接着看所调用的两个方法,即PerformLayoutCalculationPerformLayoutControl,这里需要注意的是,他们分别属于不同的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
private void PerformLayoutCalculation(RectTransform rect, UnityAction<Component> action)
{
if (rect == null)
return;

var components = ListPool<Component>.Get();
rect.GetComponents(typeof(ILayoutElement), components);
StripDisabledBehavioursFromList(components);

// If there are no controllers on this rect we can skip this entire sub-tree
// We don't need to consider controllers on children deeper in the sub-tree either,
// since they will be their own roots.
if (components.Count > 0 || rect.GetComponent(typeof(ILayoutGroup)))
{
// Layout calculations needs to executed bottom up with children being done before their parents,
// because the parent calculated sizes rely on the sizes of the children.

for (int i = 0; i < rect.childCount; i++)
PerformLayoutCalculation(rect.GetChild(i) as RectTransform, action);

for (int i = 0; i < components.Count; i++)
action(components[i]);
}

ListPool<Component>.Release(components);
}

private void PerformLayoutControl(RectTransform rect, UnityAction<Component> action)
{
if (rect == null)
return;

var components = ListPool<Component>.Get();
rect.GetComponents(typeof(ILayoutController), components);
StripDisabledBehavioursFromList(components);

// If there are no controllers on this rect we can skip this entire sub-tree
// We don't need to consider controllers on children deeper in the sub-tree either,
// since they will be their own roots.
if (components.Count > 0)
{
// Layout control needs to executed top down with parents being done before their children,
// because the children rely on the sizes of the parents.

// First call layout controllers that may change their own RectTransform
for (int i = 0; i < components.Count; i++)
if (components[i] is ILayoutSelfController)
action(components[i]);

// Then call the remaining, such as layout groups that change their children, taking their own RectTransform size into account.
for (int i = 0; i < components.Count; i++)
if (!(components[i] is ILayoutSelfController))
action(components[i]);

for (int i = 0; i < rect.childCount; i++)
PerformLayoutControl(rect.GetChild(i) as RectTransform, action);
}

ListPool<Component>.Release(components);
}

首先是StripDisabledBehavioursFromList这个方法,其实就是过滤了一些没有启用的component。不做多余展示

然后其实直接看代码就知道大概逻辑了,需要注意的点是

PerformLayoutCalculation方法是先执行递归,再进行action,即计算布局的时候是先计算子节点的size,然后再计算父节点的,而PerformLayoutControl方法是先进行action,然后在执行递归,即在进行布局控制的时候,我们是先控制了父节点的,再控制子节点的。

还有一个需要注意的点就是在布局控制的时候我们先执行了ILayoutSelfController的component的布局控制。即最优先进行了自身的布局控制。

接着往上看Rebuilder调用的地方,就到了CanvasUpdateRegistry.cs这个类,这个类之后会单独通过源代码来看,这里大概就知道在canvas执行更新方法的时候,会从更新队列里拿到所有的rebuilder执行rebuild方法

那么我们是何时将layoutGroup注册到这个更新队列里呢,其实就是在LayoutGroup.cs的SetDirty这个方法里,代码如下

1
2
3
4
5
6
7
8
9
10
protected void SetDirty()
{
if (!IsActive())
return;

if (!CanvasUpdateRegistry.IsRebuildingLayout())
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
else
StartCoroutine(DelayedSetDirty(rectTransform));
}

LayoutGroup会在各个生命流程方法里调用这个方法。这样在canvasUpdate的时候就会对标记的rect进行rebuild layout。