2.6 KiB
2.6 KiB
第五章:换行算法为什么能变成数值循环
5.1 用一组虚构宽度手算
下面只是教学数据,不代表某种真实字体:
分段:hello 空格 world 空格 again
宽度: 40 4 45 4 42
可用宽度:90
从左到右累计:
| 操作 | 当前宽度 | 决定 |
|---|---|---|
| 放入 hello | 40 | 留在第一行 |
| 放入空格 | 44 | 继续 |
| 放入 world | 89 | 继续 |
| 尝试后面的空格 | 93 | 第一行结束,行尾空格按规则处理 |
| 放入 again | 42 | 第二行 |
最终两行;若行高是 24px,高度就是 48px。
5.2 核心思路简单,实现处理边界
line-break.ts 的 countPreparedLines()为普通文本专门实现数值循环。以下是教学版,不是可替代源码的完整算法:
for (const segment of segments) {
if (currentWidth + segment.width <= availableWidth) {
currentWidth += segment.width
} else {
finishCurrentLine()
startNextLineWith(segment)
}
}
真实代码还处理行首空白、行尾修正、零宽空格、放不下一整个分段时的字素断行。关键是这些判断都基于提前准备的数据,不再重新问 Canvas 这个词有多宽。
5.3 为什么需要简单路径和完整路径
准备阶段设置两个相关标志:
simpleLineWalkFastPath:适合简单逐行遍历。simpleLineCountFastPath:适合简单行数计算;某些边界仍可能回退处理。
普通文本、普通空格等可以走简单路径。保留空白、制表符和软连字符等内容需要更完整的规则,进入 walkPreparedComplexLines() 等路径。
源码在 countPreparedLines() 前的注释记录:完整 walker 每个分段的成本约为简单路径的 3~5 倍。这是项目记录的局部测量依据,不是所有设备上的固定倍数。 分开实现的目的,是让常见输入少支付复杂状态管理的成本。
5.4 高度计算为何不经过“每行对象”
仅求高度时,简单计数循环使用局部数值变量,直接累加行数,没有每行游标对象和逐行回调。layout() 最后仍会返回一个结果对象,所以准确说法是“避免每行分配”,而不是“零分配”。
这也是为什么一个看似更通用、更加抽象的 walker,未必适合最高频的行数路径。
本章检查:为什么文本支持更多规则,不一定让普通英文同样变慢?因为准备阶段能判断适用路径,复杂处理不必全部进入普通文本的热循环。