本文的实现基于杰理SDK的字体引擎,相关基础知识见 杰理字体引擎:CJK判定、文字方向与自动换行。
整体设计可以分为三个阶段:解析 → 排版 → 绘制
1. 解析
输入一段带标签的字符串:
"[c=0xF800]错误[/c] 连接失败\n状态:[c=0x07E0]正常[/c] [img=IconOK]"
解析器从头到尾扫一遍,识别出三种东西:
- 颜色标签
[c=颜色]...[/c] - 图片标签
[img=名称] \n强制换行
2. 排版
把结构化的数据摆成行:
RichText
├─ RichLine (第1行) y=0, height=16
│ ├─ RichRun (文字"错误", 红色)
│ ├─ RichRun (文字" 连接失败")
│
├─ RichLine (第2行) y=18, height=16
│ ├─ RichRun (文字"状态:")
│ ├─ RichRun (文字"正常", 绿色)
│ ├─ RichRun (图片 IconOK)
...
3. 绘制
每帧绘制的时候,遍历所有Line和Run:
对每一行:
这行在屏幕可见区域内吗?
├─ 不在 → 跳过
└─ 在 → 对这行的每个 Run:
├─ 文字 Run → 把位图用 GPU 画到屏幕上,叠加 Run 的颜色
└─ 图片 Run → 把图片资源画到屏幕上
解析+排版的实现算法
上面是总览,下面展开前两个阶段的实现——解析和排版其实是在同一次扫描中完成的。
核心思路:一个指针从头扫到尾,边扫边完成解析和排版。
算法需要维护两个指针和一个状态:
*p当前扫描位置*segStart当前文字段起点curColor当前颜色
然后逐字符扫描,逻辑很简单:遇到特殊字符就处理,没遇到就继续扫。
if 扫描到 *p = '[', 就找到后面的 ]
{
// 不管是什么标签,先把 segStart 到 p 之间的文字吐出去
if (p > segStart)
{
把这段文字保存为 Run(用 curColor 着色)
}
if 是颜色开始标签
{
curColor = 解析出的颜色
}
if 是颜色结束标签
{
curColor = 默认颜色
}
if 是图片标签
{
追加一个图片 Run
// 遇到图片 Run,curColor 不会重置,所以 color 标签里可以嵌套 img 标签
}
p 移动到 ] 后面
segStart = p // 重新开始攒下一段文字
}
else
{
p++ // 普通字符,继续攒
}
那问题来了,这个逻辑,只有解析,没有排版。排版是怎么实现的呢?
答案是:排版就藏在"把文字保存为Run"的这个动作里。
这个动作内部会执行:
- 扫描文字,遇到
\n就切开,把文字分成若干段 - 测量这段文字的宽度和高度
- 放得下?→ 直接追加
- 放不下?→ 找断点,断点前放当前行,新建一行,后半继续循环
- 创建 RichRun,设置 x = 当前行前面元素的宽度
- 更新行宽、行高
这有点分治思想的意思:
"第一行文字\n很长的第二行需要自动断行的文字"
│
先按 \n 切成两段
│
┌────┴────┐
"第一行文字" "很长的第二行需要自动断行的文字"
│ │
直接追加 进入自动断行循环