UGUI源码分析-LayoutGroup

简单画了下关于LayoutGroup的类图。

源码分析以及代码解释

1、CanvasUpdateRegistry 注册Rebuild 事件,Canvas从底层系统调用重新布局刷新方法 LayoutRebuilder.Rebuild(CanvasUpdate executing)。
2、LayoutRebuilder : ICanvasElement 是一个容器类对界面Rebuid请求进行封装。主要负责重建各种布局。
其中非常关键的方法代码:
LayoutRebuilder.cs
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
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;
}
}

//深度优先,效果就是先把子类的布局好,再布局自己
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);
}

//深度优先,效果就是先把子类的布局好,再布局自己
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);
}

3、ILayoutElement:包含自身各种布局相关参数的计算,最基础的布局元素:minWidth ,preferredHeight,minHight ,preferredHeight,layoutPriority 、、、、、。定义了两个重要的方法:
(a)CalculateLayoutInputHorizontal() 水平方向计算
(b)CalculateLayoutInputVertical() 垂直方向计算
4、ILayoutController:主要是对子节点进行布局。
(a) SetLayoutHorizontal()
(b)SetLayoutVertical()

示例

分析一个Horizontal Layout Group 布局相关的实例: 白色的是HorizontalLayoutGroup的RectTransform矩形,红色是子元素Recttransform矩形(5个)。 根据代码执行逻辑会先对子层级ILayoutElement(Image组件是实现ILayoutElement的)按1,2,3,4,5的顺序进行计算其长宽。然后执行ILayoutController,父层级的Horizontal Layout Group 会起作用,排列子子物体。 我们看到白色框是不会自己进行排列的,在开发中需要白色框的长度和子物体大小一样(也就是宽度是红色宽度的总和)的需求,怎么办? 可以添加一个ContentSizeFitter:ILayoutSelfController这个组件的作用正像他的实现接口名字一样设置自身。 父类白色框的宽度和红色框宽度保持一致了。 一些实用性的布局技巧,对Layout Group组件中的ControlChildSize的解释 勾选ControlChildSize。会使用子类的LayoutElement中的布局熟悉(width height等),下图希望子物体中1的宽度200,2~5宽度100,参考以下设置:

代码解析:

LayoutElement组件,默认的UI组件Image,Text等都是实现ILayoutElement,可以利用ILayoutElement.layoutPriority,LayoutGroup和ContentSizeFitter在计算子对象或者自己with和hight时会按优先级生效。
LayoutUtility.cs
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
/// <summary>
/// Gets a calculated layout property for the layout element with the given RectTransform.
/// </summary>
/// <param name="rect">The RectTransform of the layout element to get a property for.</param>
/// <param name="property">The property to calculate.</param>
/// <param name="defaultValue">The default value to use if no component on the layout element supplies the given property</param>
/// <param name="source">Optional out parameter to get the component that supplied the calculated value.</param>
/// <returns>The calculated value of the layout property.</returns>
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的优先级,当一个组件包含多个LayoutElement到底使用哪个的解释。
HorizontalOrVerticalLayoutGroup.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
}
上述代码是LayoutGroup布局子物体是调用HorizontalOrVerticalLayoutGroup.GetChildSize()方法中实现,controlSize必须为True才生效

关于ContentSizeFitter控制自身的LayoutElement
具体在ContentSizeFitter.HandleSelfFittingAlongAxis(int axis)实现的

ContentSizeFitter.cs
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
/// <summary>
/// Calculate and apply the horizontal component of the size to the RectTransform
/// </summary>
public virtual void SetLayoutHorizontal()
{
m_Tracker.Clear();
HandleSelfFittingAlongAxis(0);
}

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));
}