如何计算字符串节点树的布局并绘制到 CGContext?
可以先计算树形布局,再交给 CGContext 绘制
可以先根据树结构和文字尺寸计算节点位置,再用 CGContext 绘制连线、节点和文字。CGContext 负责绘图,树的层级排列、节点间距和重叠处理则需要布局算法或布局库来完成。
如果要绘制一棵从上到下排列的树,可以先实现基于子树宽度的布局。这种方法容易理解,也能处理字符串长度不同的节点。
如何计算节点的位置
以下方案假设数据是一棵没有环的树,每个非根节点只有一个父节点。所有节点高度相同,文字保持单行,宽度随字符串长度变化。
先设置几个参数:
paddingX:文字左右留白。nodeHeight:节点统一高度,需要容纳字体和上下留白。siblingGap:相邻子树之间的水平间距。levelGap:相邻层节点之间的垂直间距。
布局分两遍进行。
第一遍:从叶子向根节点计算子树宽度
先用实际绘制时使用的字体测量字符串:
nodeWidth = textWidth + 2 * paddingX
叶子节点的子树宽度等于自身宽度。有子节点时,先把各棵子树的宽度和间距相加:
childrenWidth = sum(child.subtreeWidth)
+ siblingGap * (childCount - 1)
subtreeWidth = max(nodeWidth, childrenWidth)
这样可以为整棵子树预留水平空间。如果只根据当前节点的文字宽度排列兄弟节点,更下层的后代可能互相覆盖。
第二遍:从根节点向下分配坐标
将父节点放在其子树区域的水平中央,再把各棵子树依次放入下方区域。以下是布局伪代码,measureText、Rect 等名称用于说明算法,并非可直接调用的 Swift API:
function measure(node):
node.width = measureText(node.text) + 2 * paddingX
for child in node.children:
measure(child)
if node.children is empty:
node.childrenWidth = 0
else:
node.childrenWidth = sum(child.subtreeWidth)
node.childrenWidth += siblingGap * (count(node.children) - 1)
node.subtreeWidth = max(node.width, node.childrenWidth)
function place(node, left, depth):
centerX = left + node.subtreeWidth / 2
top = depth * (nodeHeight + levelGap)
node.frame = Rect(
x = centerX - node.width / 2,
y = top,
width = node.width,
height = nodeHeight
)
childLeft = left + (node.subtreeWidth - node.childrenWidth) / 2
for child in node.children:
place(child, childLeft, depth + 1)
childLeft += child.subtreeWidth + siblingGap
measure(root)
place(root, 0, 0)
这套算法为每棵子树保留独立的水平区域,在上述假设下可以避免节点重叠。代价是树较深或分支较多时,图形可能比较宽。
在 CGContext 中绘制
得到节点的矩形后,建议先画全部连线,再画节点和文字,避免连线盖住文字。
如果布局采用左上角为原点、Y 轴向下的逻辑坐标,可以将父节点的底边中心连接到子节点的顶边中心。假设 parentFrame、childFrame 均为已经计算好的 CGRect,连线代码如下:
import CoreGraphics
func drawEdge(
in context: CGContext,
parentFrame: CGRect,
childFrame: CGRect
) {
context.beginPath()
context.move(to: CGPoint(
x: parentFrame.midX,
y: parentFrame.maxY
))
context.addLine(to: CGPoint(
x: childFrame.midX,
y: childFrame.minY
))
context.strokePath()
}
这段代码要求绘图上下文的坐标方向与布局一致。不同来源的 CGContext 初始坐标系可能不同,不能直接假设 Y 轴总是向下。
文字可以通过 Core Text 绘制到 CGContext。测量和绘制时必须使用一致的字体、字号及换行规则。文字基线和坐标变换也需要单独处理,否则容易出现位置偏移或文字倒置。
查找算法或布局库时关注什么
可以从这些关键词入手:tree layout、tidy tree layout、variable node size tree layout、Swift tree layout。
评估库时,先确认它能否接收节点尺寸并输出布局坐标。如果库只负责输出坐标,可以自行将结果绘制到 CGContext;如果库绑定了自己的视图或画布,还需要确认是否方便导出位置和连线信息。
对于静态、规模较小的树,上面的子树宽度方案可以作为起点。如果需要更紧凑的排列,或者支持折叠、缩放、拖拽,可以再根据需求选择更完整的布局和交互方案。
备注:内容仅供参考。
