抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

简介

演示视频

非分层式树结构自动布局工具,支持横向布局(从上到下)和竖向布局(从左到右)。

什么是非分层结构?

具有不同尺寸节点的树,使得相同深度的所有节点都垂直对齐,称之为分层结构;节点可以垂直放置在彼此之间固定距离,称之为非分层结构。

通过下面的图我们可以更清楚得理解这两种结构:

本项目只提供了非分层式的实现,并且只测试了竖向布局。

原理

本项目参考 Reingold-Tilford 算法,一共分两步执行。

  1. 遍历树,在目标方向(纵向/横向)移动节点。
  2. 递归树,自底向上对同级节点布局进行移动。

在算法中有一个概念是轮廓,轮廓分为左右轮廓,是当前子树能够从左边看到的所有节点和从右边看到的所有节点。

参考文章中把左右轮廓对下一节点的引用称为线程,本项目把左右轮廓所有节点都集中在左右列表中。

节点数据结构

节点数据包含 GraphView 的节点 RootNode 以及宽高、坐标、间隔等基础数据。

节点提供包围盒极限值的获取方法,以及重叠检测和节点移动的方法。

检测重叠的时候,我们只检测新节点相对于旧节点的某一面是否有重叠,即使新节点已经超过旧节点的包围盒,但只要是对于这一面来说属于重叠情况,整体就算重叠,如图所示:

绿色是新节点,红色是旧节点,方向是左右方向,新节点理应在旧节点的右边。

对于上方的情况来说,我们可以直接看到重叠;对于下方的情况来说,虽然绿色的矩形已经越过了红色的矩形,但是对于红色矩形的右边线来说,绿色矩形已经跨越到左边去了,因此依然属于重叠。

移动节点的时候不仅要移动自己,还要移动所有子节点。

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
using System.Collections.Generic;
using GraphViewExtension;
using UnityEngine;

namespace AutoLayout
{
public class NodeData
{
public RootNode data;

public List<NodeData> children = new List<NodeData>();

public float x
{
get { return _x; }
set
{
_x = value;
data.style.left = value + hSpace;
}
}

private float _x;

public float y
{
get { return _y; }
set
{
_y = value;
data.style.top = value + vSpace;
}
}

private float _y;

public float MinX => x;

public float MaxX => x + width;

public float MinY => y;

public float MaxY => y + height;

public float width;

public float height;

public float hSpace;

public float vSpace;

/// <summary>
/// 检测是否重合
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public bool CheckOverlap(NodeData other)
{
if (MaxX < other.MinX || MaxY < other.MinY)
{
return false;
}
else
{
return true;
}
}

public void MoveRight(float dis)
{
x += dis;
foreach (var child in children)
{
child.MoveRight(dis);
}
}

public void MoveBottom(float dis)
{
y += dis;
foreach (var child in children)
{
child.MoveBottom(dis);
}
}

public float GetTotalWidth()
{
float minX = GetMinX();
float maxX = GetMaxX();
return maxX - minX;
}

public float GetTotalHeight()
{
float minY = GetMinY();
float maxY = GetMaxY();
return maxY - minY;
}

private float GetMinX()
{
float minX = MinX;
foreach (var child in children)
{
minX = Mathf.Min(minX, child.GetMinX());
}

return minX;
}

private float GetMaxX()
{
float maxX = MaxX;
foreach (var child in children)
{
maxX = Mathf.Max(maxX, child.GetMaxX());
}

return maxX;
}

private float GetMinY()
{
float minY = MinY;

foreach (var child in children)
{
minY = Mathf.Min(minY, child.GetMinY());
}

return minY;
}

private float GetMaxY()
{
float maxY = MaxY;
foreach (var child in children)
{
maxY = Mathf.Max(maxY, child.GetMaxY());
}

return maxY;
}
}
}

节点线程

节点线程维护两个列表,左轮廓列表和右轮廓列表。

节点线程提供两个关键方法:

  1. 检查新节点是否需要移动。
  2. 合并轮廓。

检查新节点是否需要移动的时候,我们需要检查新节点的左轮廓是否与旧节点的右轮廓有重叠。我们需要移动的就是轮廓重叠的部分。

以竖向布局为例,左轮廓节点特指新节点,右轮廓节点特指旧节点(在竖向布局中,左轮廓可以看作是上轮廓,右轮廓可以看做下轮廓):

graph TD;

  A["左右轮廓索引是否超出范围"]
  B["返回移动距离"]
  C["当前右轮廓节点最小X值是否小于当前左轮廓节点最大X值
并且当前左右轮廓节点有重叠"] D["计算轮廓重叠深度"] E["判断当前左右轮廓节点最大X值的关系"] F["左轮廓节点进位"] G["右轮廓节点进位"] H["左右轮廓节点都进位"] G["继续循环"] A--"是"-->B A--"否"-->C C--"是"-->D C--"否"-->E D-->E E--"两节点最大X相等"-->H E--"右轮廓节点最大X大于左轮廓节点"-->F E--"左轮廓节点最大X大于右轮廓节点"-->G H-->G F-->G G-->A

当前右轮廓节点最小 X 值是否小于当前左轮廓节点最大 X 值的判断是为了防止两个 X 坐标投影不重叠的节点进行比较,造成布局错误。

检查移动的示例如下(横向布局案例,视觉上更直观,共有两个子树,左子树为旧节点,右子树为新节点):

  1. 检查新旧节点对应轮廓的第一个节点。

  1. 由于第一个节点两者最大 Y 值一样,所以同时进位比较。

  1. 由于旧节点的最大 Y 值比新节点的大,因此新节点进位,旧节点不变。

  1. 旧节点的最大 Y 值比新节点的小,因此旧节点进位,后面依次按照规则检测。

合并轮廓的时候,我们需要对轮廓进行裁剪。

依然以竖向布局左轮廓为例:

graph TD;

  A["反向遍历新节点左轮廓"]
  B["判断新节点左轮廓节点最大X值小于等于旧节点左轮廓节点总的最大值X"]
  C["裁剪新节点中接下来的所有节点"]
  D["剩下的节点添加到旧节点左轮廓列表"]

  A-->B
  B--"是"-->C
  B--"否"-->A
  C-->D

对于右轮廓来说也是一样的流程,只不过新旧节点交换:

graph TD;

  A["反向遍历旧节点右轮廓"]
  B["判断旧节点右轮廓节点最大X值小于等于新节点右轮廓节点总的最大值X"]
  C["裁剪旧节点中接下来的所有节点"]
  D["剩下的节点添加到新节点左轮廓列表"]
  E["新节点右轮廓赋值给旧节点左轮廓"]

  A-->B
  B--"是"-->C
  B--"否"-->A
  C-->D
  D-->E

为什么要裁剪接下来的所有节点?

因为剩下的所有节点都会被覆盖看不到,因此直接裁去。

为什么都是赋值给旧节点?

因为最终合并的线程属于旧节点,属于已经参与完一次计算的节点。

蓝色是旧节点,绿色是新节点,计算完后的新节点就会合并到旧节点中去。

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
using System;
using System.Collections.Generic;

namespace AutoLayout
{
public class NodeThread
{
public List<NodeData> left = new List<NodeData>();

public List<NodeData> right = new List<NodeData>();

public AutoLayoutDirection direction;

public float MaxX(List<NodeData> self, List<NodeData> other)
{
float max = self.Count == 0 ? 0 : self[0].MaxX;
foreach (var node in other)
{
if (node.MaxX > max)
{
max = node.MaxX;
}
}

return max;
}

public float MaxY(List<NodeData> self, List<NodeData> other)
{
float max = self.Count == 0 ? 0 : self[0].MaxY;
foreach (var node in other)
{
if (node.MaxY > max)
{
max = node.MaxY;
}
}

return max;
}

public float CheckMove(NodeThread other)
{
float move = 0;

int i = 0;
int j = 0;


while (i < right.Count && j < other.left.Count)
{
var leftNode = right[i];
var rightNode = other.left[j];

if (direction == AutoLayoutDirection.Horizontal)
{
if (leftNode.MinY < rightNode.MaxY && leftNode.CheckOverlap(rightNode))
{
float dis = leftNode.MaxX - rightNode.x;
if (dis > move)
{
move = dis;
}
}

if (Math.Abs(leftNode.MaxY - rightNode.MaxY) < 0.001)
{
++i;
++j;
}
else if (leftNode.MaxY > rightNode.MaxY)
{
++j;
}
else
{
++i;
}
}
else
{
if (leftNode.MinX < rightNode.MaxX && leftNode.CheckOverlap(rightNode))
{
float dis = leftNode.MaxY - rightNode.y;
if (dis > move)
{
move = dis;
}
}

if (Math.Abs(leftNode.MaxX - rightNode.MaxX) < 0.001)
{
++i;
++j;
}
else if (leftNode.MaxX > rightNode.MaxX)
{
++j;
}
else
{
++i;
}
}
}

return move;
}

public void SetLeftRight(NodeThread other)
{
if (direction == AutoLayoutDirection.Horizontal)
{
//合并左轮廓
float maxLeft = MaxY(left, left);
for (int i = other.left.Count - 1; i >= 0; i--)
{
var node = other.left[i];
if (node.MaxY <= maxLeft)
{
other.left.RemoveRange(0, i + 1);
break;
}
}

left.AddRange(other.left);

//合并右轮廓
float maxRight = other.MaxY(other.right, other.right);
for (int i = right.Count - 1; i >= 0; i--)
{
var node = right[i];
if (node.MaxY <= maxRight)
{
right.RemoveRange(0, i + 1);
break;
}
}

other.right.AddRange(right);
right = other.right;
}
else
{
//合并左轮廓
float maxLeft = MaxX(left, left);
for (int i = other.left.Count - 1; i >= 0; i--)
{
var node = other.left[i];
if (node.MaxX <= maxLeft)
{
other.left.RemoveRange(0, i + 1);
break;
}
}

left.AddRange(other.left);

//合并右轮廓
float maxRight = other.MaxX(other.right, other.right);
for (int i = right.Count - 1; i >= 0; i--)
{
var node = right[i];
if (node.MaxX <= maxRight)
{
right.RemoveRange(0, i + 1);
break;
}
}

other.right.AddRange(right);
right = other.right;
}
}
}
}

第一步

第 1 步很简单,只需要普通遍历就行。每一级的 x 或 y 坐标只需要增加上一级的宽度或高度就行。

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
/// <summary>
/// 布局方向坐标初始化
/// </summary>
/// <param name="node"></param>
/// <param name="dirDis"></param>
private static void FirstStep(NodeData node, float dirDis)
{
if (direction == AutoLayoutDirection.Horizontal)
{
node.y = dirDis;

foreach (var child in node.children)
{
FirstStep(child, dirDis + node.height);
}
}
else
{
node.x = dirDis;

foreach (var child in node.children)
{
FirstStep(child, dirDis + node.width);
}
}
}

第二步

第二步是重点,这一步负责具体的布局。

第二步总体的逻辑如下(以纵向布局为例):

graph TD;

A["是否叶子结点"]
B["设置y坐标"]
C["返回一个左线程"]
D["遍历子节点"]

subgraph AA["循环体内部"];
E["是否不存在左线程"]
F["传入当前子节点递归获取左线程"]
G["传入当前子节点递归获取右线程"]
H["左右线程检测移动距离"]
I["移动子节点"]
J["平滑分布"]
K["合并轮廓到左线程"]
end;

L["移动当前节点到合适的位置"]
M["左线程左右轮廓插入当前节点到列表头部"]

A--"是"-->B
B-->C
A--"否"-->D
D-->AA
E--"是"-->F

E--"否"-->G
G-->H
H-->I
I-->J
J-->K
AA-->L
L-->M
M-->C

平滑分布需要三个额外参数:

  1. last,上一个子节点。
  2. index,上一次平滑分布的起始索引。
  3. i,当前子节点索引。

关于平滑分布的逻辑如下:

graph TD;

A["判断当前子节点和上一子节点的间距"]
B["计算中间节点的平均额外间距"]
C["中间节点移动 平均额外间距*循环索引 的距离"]
D["更新index"]
E["更新last和i"]

A--"大于0"-->B
A--"等于0"-->E
D-->E
B-->C
C-->D
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
private static NodeThread SecondStep(NodeData node)
{
if (node.children.Count == 0)
{
if (direction == AutoLayoutDirection.Horizontal)
{
node.x = 0;
}
else
{
node.y = 0;
}

return new NodeThread()
{
direction = direction,
left = new List<NodeData>() { node },
right = new List<NodeData>() { node }
};
}

NodeThread left = null;

//子节点索引
int i = 0;
int index = 0;

//上一节点
NodeData last = node.children[0];

foreach (var child in node.children)
{
if (left == null)
{
left = SecondStep(child);
}
else
{
var right = SecondStep(child);
float moveDis = left.CheckMove(right);
float d = 0;
if (direction == AutoLayoutDirection.Horizontal)
{
child.MoveRight(moveDis);
//平均间距
d = child.MinX - last.MaxX;
if (d > 0)
{
float bonus = d / (i - index);

for (int j = index + 1; j < i; j++)
{
node.children[j].MoveRight(bonus * (j - index));
}

index = i;
}
}
else
{
child.MoveBottom(moveDis);
//平均间距
d = child.MinY - last.MaxY;
if (d > 0)
{
float bonus = d / (i - index);

for (int j = index + 1; j < i; j++)
{
node.children[j].MoveBottom(bonus * (j - index));
}

index = i;
}
}

left.SetLeftRight(right);
}

last = child;
i++;
}


if (direction == AutoLayoutDirection.Horizontal)
{

node.x += (node.children[0].MinX + node.children[^1].MaxX) * 0.5f - node.width * 0.5f;
}
else
{
node.y += (node.children[0].MinY + node.children[^1].MaxY) * 0.5f - node.height * 0.5f;
}

left.left.Insert(0, node);
left.right.Insert(0, node);
return left;
}

代码

节点数据
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
using System.Collections.Generic;
using GraphViewExtension;
using UnityEngine;

namespace AutoLayout
{
public class NodeData
{
public RootNode data;

public List<NodeData> children = new List<NodeData>();

public float x
{
get { return _x; }
set
{
_x = value;
data.style.left = value + hSpace;
}
}

private float _x;

public float y
{
get { return _y; }
set
{
_y = value;
data.style.top = value + vSpace;
}
}

private float _y;

public float MinX => x;

public float MaxX => x + width;

public float MinY => y;

public float MaxY => y + height;

public float width;

public float height;

public float hSpace;

public float vSpace;

/// <summary>
/// 检测是否重合
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public bool CheckOverlap(NodeData other)
{
if (MaxX < other.MinX || MaxY < other.MinY)
{
return false;
}
else
{
return true;
}
}

public void MoveRight(float dis)
{
x += dis;
foreach (var child in children)
{
child.MoveRight(dis);
}
}

public void MoveBottom(float dis)
{
y += dis;
foreach (var child in children)
{
child.MoveBottom(dis);
}
}

public float GetTotalWidth()
{
float minX = GetMinX();
float maxX = GetMaxX();
return maxX - minX;
}

public float GetTotalHeight()
{
float minY = GetMinY();
float maxY = GetMaxY();
return maxY - minY;
}

private float GetMinX()
{
float minX = MinX;
foreach (var child in children)
{
minX = Mathf.Min(minX, child.GetMinX());
}

return minX;
}

private float GetMaxX()
{
float maxX = MaxX;
foreach (var child in children)
{
maxX = Mathf.Max(maxX, child.GetMaxX());
}

return maxX;
}

private float GetMinY()
{
float minY = MinY;

foreach (var child in children)
{
minY = Mathf.Min(minY, child.GetMinY());
}

return minY;
}

private float GetMaxY()
{
float maxY = MaxY;
foreach (var child in children)
{
maxY = Mathf.Max(maxY, child.GetMaxY());
}

return maxY;
}
}
}
节点线程
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
using System;
using System.Collections.Generic;

namespace AutoLayout
{
public class NodeThread
{
public List<NodeData> left = new List<NodeData>();

public List<NodeData> right = new List<NodeData>();

public AutoLayoutDirection direction;

public float MaxX(List<NodeData> self, List<NodeData> other)
{
float max = self.Count == 0 ? 0 : self[0].MaxX;
foreach (var node in other)
{
if (node.MaxX > max)
{
max = node.MaxX;
}
}

return max;
}

public float MaxY(List<NodeData> self, List<NodeData> other)
{
float max = self.Count == 0 ? 0 : self[0].MaxY;
foreach (var node in other)
{
if (node.MaxY > max)
{
max = node.MaxY;
}
}

return max;
}

public float CheckMove(NodeThread other)
{
float move = 0;

int i = 0;
int j = 0;


while (i < right.Count && j < other.left.Count)
{
var leftNode = right[i];
var rightNode = other.left[j];

if (direction == AutoLayoutDirection.Horizontal)
{
if (leftNode.MinY < rightNode.MaxY && leftNode.CheckOverlap(rightNode))
{
float dis = leftNode.MaxX - rightNode.x;
if (dis > move)
{
move = dis;
}
}

if (Math.Abs(leftNode.MaxY - rightNode.MaxY) < 0.001)
{
++i;
++j;
}
else if (leftNode.MaxY > rightNode.MaxY)
{
++j;
}
else
{
++i;
}
}
else
{
if (leftNode.MinX < rightNode.MaxX && leftNode.CheckOverlap(rightNode))
{
float dis = leftNode.MaxY - rightNode.y;
if (dis > move)
{
move = dis;
}
}

if (Math.Abs(leftNode.MaxX - rightNode.MaxX) < 0.001)
{
++i;
++j;
}
else if (leftNode.MaxX > rightNode.MaxX)
{
++j;
}
else
{
++i;
}
}
}

return move;
}

public void SetLeftRight(NodeThread other)
{
if (direction == AutoLayoutDirection.Horizontal)
{
//合并左轮廓
float maxLeft = MaxY(left, left);
for (int i = other.left.Count - 1; i >= 0; i--)
{
var node = other.left[i];
if (node.MaxY <= maxLeft)
{
other.left.RemoveRange(0, i + 1);
break;
}
}

left.AddRange(other.left);

//合并右轮廓
float maxRight = other.MaxY(other.right, other.right);
for (int i = right.Count - 1; i >= 0; i--)
{
var node = right[i];
if (node.MaxY <= maxRight)
{
right.RemoveRange(0, i + 1);
break;
}
}

other.right.AddRange(right);
right = other.right;
}
else
{
//合并左轮廓
float maxLeft = MaxX(left, left);
for (int i = other.left.Count - 1; i >= 0; i--)
{
var node = other.left[i];
if (node.MaxX <= maxLeft)
{
other.left.RemoveRange(0, i + 1);
break;
}
}

left.AddRange(other.left);

//合并右轮廓
float maxRight = other.MaxX(other.right, other.right);
for (int i = right.Count - 1; i >= 0; i--)
{
var node = right[i];
if (node.MaxX <= maxRight)
{
right.RemoveRange(0, i + 1);
break;
}
}

other.right.AddRange(right);
right = other.right;
}
}
}
}
自动布局工具类
枚举
1
2
3
4
5
6
7
8
namespace AutoLayout
{
public enum AutoLayoutDirection
{
Horizontal,
Verticle
}
}
自动布局工具
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
using System.Collections.Generic;
using GraphViewExtension;

namespace AutoLayout
{
public class AutoLayoutUtils
{
public static float hSpace;

public static float vSpace;

public static AutoLayoutDirection direction = AutoLayoutDirection.Verticle;

public static float Layout(RootNode data,float bonus = 0)
{
//创建根节点
NodeData root = GenerateNode(data);
FirstStep(root, 0);
SecondStep(root);

if (direction == AutoLayoutDirection.Horizontal)
{
root.MoveRight(bonus);
return root.GetTotalWidth();
}

root.MoveBottom(bonus);
return root.GetTotalHeight();
}

private static NodeData GenerateNode(RootNode data)
{
NodeData root = new NodeData()
{
data = data,
width = data.resolvedStyle.width + hSpace,
height = data.resolvedStyle.height + vSpace,
hSpace = hSpace,
vSpace = vSpace
};

foreach (var edge in data.GetOutput().connections)
{
root.children.Add(GenerateNode(edge.input.node as RootNode));
}

return root;
}

/// <summary>
/// 布局方向坐标初始化
/// </summary>
/// <param name="node"></param>
/// <param name="dirDis"></param>
private static void FirstStep(NodeData node, float dirDis)
{
if (direction == AutoLayoutDirection.Horizontal)
{
node.y = dirDis;

foreach (var child in node.children)
{
FirstStep(child, dirDis + node.height);
}
}
else
{
node.x = dirDis;

foreach (var child in node.children)
{
FirstStep(child, dirDis + node.width);
}
}
}

private static NodeThread SecondStep(NodeData node)
{
if (node.children.Count == 0)
{
if (direction == AutoLayoutDirection.Horizontal)
{
node.x = 0;
}
else
{
node.y = 0;
}

return new NodeThread()
{
direction = direction, left = new List<NodeData>() { node }, right = new List<NodeData>() { node }
};
}

NodeThread left = null;

//子节点索引
int i = 0;
int index = 0;

//上一节点
NodeData last = node.children[0];

foreach (var child in node.children)
{
if (left == null)
{
left = SecondStep(child);
}
else
{
var right = SecondStep(child);
float moveDis = left.CheckMove(right);
float d = 0;
if (direction == AutoLayoutDirection.Horizontal)
{
child.MoveRight(moveDis);
//平均间距
d = child.MinX - last.MaxX;
if (d > 0)
{
float bonus = d / (i - index);

for (int j = index + 1; j < i; j++)
{
node.children[j].MoveRight(bonus * (j - index));
}

index = i;
}
}
else
{
child.MoveBottom(moveDis);
//平均间距
d = child.MinY - last.MaxY;
if (d > 0)
{
float bonus = d / (i - index);

for (int j = index + 1; j < i; j++)
{
node.children[j].MoveBottom(bonus * (j - index));
}

index = i;
}
}

left.SetLeftRight(right);
}

last = child;
i++;
}


if (direction == AutoLayoutDirection.Horizontal)
{

node.x += (node.children[0].MinX + node.children[^1].MaxX) * 0.5f - node.width * 0.5f;
}
else
{
node.y += (node.children[0].MinY + node.children[^1].MaxY) * 0.5f - node.height * 0.5f;
}

left.left.Insert(0, node);
left.right.Insert(0, node);
return left;
}
}
}

项目

更新日志

2024-06-21

  1. 更新基本内容。
  2. 新增获取整个树宽高的方法。
  3. 自动布局新增返回宽高(根据布局方向)。

评论