🎨 基于路径绘制的无抗锯齿文本渲染库 - Flutter NoaaText
没有抗锯齿的字体渲染解决方案,通过直接绘制 TTF 字体的矢量路径实现精确控制。
- 🎯 路径绘制 - 解析 TTF 字体文件,通过矢量路径精确绘制文本,完全控制渲染过程
- 🚫 无抗锯齿 - 完全禁用抗锯齿,获得清晰锐利的边缘,适合像素艺术和特殊显示需求
- 💪 合成加粗 - 使用描边+填充技术模拟加粗效果,无需额外字体文件,支持动态调整加粗程度
- 📐 合成斜体 - 使用 Matrix4 剪切变换实现斜体效果,倾斜角度可调节(默认约 12 度)
- 📝 自动换行 - 支持根据容器宽度自动换行,智能处理中英文混排,基于字体的实际
advanceWidth精确计算 - 🔄 字体缓存 - 内置 LRU 缓存策略的字体管理器,自动缓存已加载字体,避免内存溢出
- 🎨 备用字体 - 支持备用字体列表,当主字体缺少某些字符时自动降级使用备用字体
- ⚡ 路径缓存 - 绘制器内置路径缓存机制,提高重绘性能,减少路径生成开销
- 🚀 Isolate 优化 - 批量加载字体时支持在独立线程解析,避免 UI 卡顿
- 📏 精确度量 - 基于 TTF 字体的
unitsPerEm和advanceWidth精确计算字符宽度和位置
本项目依赖独立的 TTF 字体解析库:
dependencies:
flutter_ttf_parser:
git:
url: https://gitea.cquni.com/yangjie/flutter_ttf_parser.git# pubspec.yaml
dependencies:
flutter_noaa_text:
git:
url: [你的仓库地址]# pubspec.yaml
flutter:
assets:
- assets/fonts/song_ti.ttf
- assets/fonts/source_han_sans.ttf # 备用字体(可选)import 'package:flutter_noaa_text/flutter_noaa_text.dart';
// 加载字体
final fontManager = NoaaFontManager();
final font = await fontManager.loadFont('assets/fonts/song_ti.ttf');
// 基础文本显示
NoaaText(
'你好世界 Hello World',
font: font,
style: NoaaTextStyle(
fontSize: 48.0,
color: Colors.black,
),
)NoaaText(
'加粗斜体文本',
font: font,
style: NoaaTextStyle(
fontSize: 48.0,
color: Colors.black,
fontWeight: FontWeight.bold, // 合成加粗
fontStyle: FontStyle.italic, // 合成斜体
boldStrokeWidthFactor: 0.03, // 加粗描边宽度系数(可选)
italicSkewFactor: -0.2, // 斜体剪切系数(可选)
disableAntiAlias: true, // 禁用抗锯齿
),
)当主字体缺少某些字符(如生僻字)时,自动使用备用字体:
// 加载主字体和备用字体
final mainFont = await fontManager.loadFont('assets/fonts/song_ti.ttf');
final fallbackFont = await fontManager.loadFont('assets/fonts/source_han_sans.ttf');
NoaaText(
'常用字和生僻字:焜燊',
font: mainFont,
style: NoaaTextStyle(
fontSize: 48.0,
fontFamilyFallback: [fallbackFont], // 备用字体列表
),
)NoaaText(
'这是一段很长的文本,会根据容器宽度自动换行。支持中英文混排,English and Chinese mixed layout.',
font: font,
style: NoaaTextStyle(
fontSize: 24.0,
height: 1.5, // 行高倍数(1.5 倍行距)
letterSpacing: 2.0, // 字符间距(像素)
),
maxLines: 3, // 最大行数
softWrap: true, // 启用自动换行
)提升应用启动后的渲染性能:
final fontManager = NoaaFontManager();
// 方式1: 主线程预加载(适合少量字体)
await fontManager.preloadFonts([
'assets/fonts/song_ti.ttf',
'assets/fonts/hei_ti.ttf',
]);
// 方式2: Isolate 预加载(适合大量字体,避免 UI 卡顿)
await fontManager.preloadFontsWithIsolate(
[
'assets/fonts/song_ti.ttf',
'assets/fonts/hei_ti.ttf',
'assets/fonts/kai_ti.ttf',
// ... 更多字体
],
onProgress: (loaded, total, path) {
print('加载进度: $loaded/$total - ${path ?? "完成"}');
},
);无抗锯齿文本渲染组件,支持自动换行、多行显示等。
主要参数:
| 参数 | 类型 | 说明 |
|---|---|---|
text |
String |
要显示的文本内容 |
font |
TtfFont |
TTF 字体对象(必需) |
style |
NoaaTextStyle? |
文本样式配置 |
softWrap |
bool? |
是否自动换行(默认 true) |
maxLines |
int? |
最大行数(null 表示无限制) |
textAlign |
TextAlign? |
文本对齐方式(暂未实现) |
overflow |
TextOverflow? |
溢出处理(暂未实现) |
文本样式配置类,参考 Flutter TextStyle API 设计。
主要属性:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
fontSize |
double? |
14.0 |
字体大小(像素) |
color |
Color? |
Color(0xFF000000) |
文本颜色 |
fontWeight |
FontWeight? |
FontWeight.normal |
字体粗细(bold 触发合成加粗) |
fontStyle |
FontStyle? |
FontStyle.normal |
字体样式(italic 触发合成斜体) |
letterSpacing |
double? |
0.0 |
字符间距(像素) |
height |
double? |
1.0 |
行高倍数 |
disableAntiAlias |
bool? |
true |
是否禁用抗锯齿 |
boldStrokeWidthFactor |
double? |
0.03 |
加粗描边宽度系数(相对字号) |
italicSkewFactor |
double? |
-0.2 |
斜体剪切系数(约 12 度倾斜) |
fontFamilyFallback |
List<TtfFont>? |
null |
备用字体列表 |
字体缓存管理器,采用单例模式,提供字体加载和缓存功能。
核心特性:
- ✅ LRU 缓存策略 - 最大缓存 20 个字体,自动淘汰最久未使用的字体
- ✅ 并发控制 - 防止同一字体被重复加载
- ✅ 内存优化 - 每个字体对象只创建一次,全局复用
- ✅ Isolate 支持 - 批量加载时在独立线程解析,避免 UI 卡顿
常用方法:
final fontManager = NoaaFontManager();
// 加载单个字体(如已缓存则直接返回)
final font = await fontManager.loadFont('assets/fonts/song_ti.ttf');
// 检查字体是否已缓存
bool isCached = fontManager.isCached('assets/fonts/song_ti.ttf');
// 获取已缓存的字体(不触发加载)
TtfFont? cachedFont = fontManager.getCachedFont('assets/fonts/song_ti.ttf');
// 清除缓存
fontManager.clearCache(); // 清除所有
fontManager.clearCache('assets/fonts/song_ti.ttf'); // 清除指定字体
// 查看缓存统计
Map<String, dynamic> stats = fontManager.getCacheStats();
print('当前缓存数量: ${stats['cacheSize']}/${stats['maxCacheSize']}');自定义文本绘制器,基于 CustomPainter 实现。
核心机制:
- ⚡ 路径缓存 - 缓存每行文本的矢量路径,避免重复生成
- 🎯 精确定位 - 基于字体的
ascender和advanceWidth精确定位字符 - 🎨 变换缓存 - 缓存斜体变换后的路径,提高重绘性能
- 🔄 智能重绘 - 通过
shouldRepaint判断是否需要重绘,优化性能
工具类,提供路径生成、变换和绘制等核心功能。
主要方法:
// 生成文本路径
Path path = NoaaTextUtils.generatePathForText(text, font, style);
// 应用斜体变换
Path italicPath = NoaaTextUtils.applyItalicTransform(path, skewFactor);
// 计算文本宽度
double width = NoaaTextUtils.calculateTextWidth(text, font, fontSize, letterSpacing);
// 文本自动换行
List<String> lines = NoaaTextUtils.wrapText(text, font, maxWidth, fontSize, letterSpacing);
// 绘制加粗路径
NoaaTextUtils.drawBoldPath(canvas, path, color, fontSize, strokeWidthFactor, disableAntiAlias);
// 绘制普通路径
NoaaTextUtils.drawNormalPath(canvas, path, color, disableAntiAlias);使用 flutter_ttf_parser 解析 TTF 字体文件:
- 读取字体表 - 解析
cmap(字符映射)、glyf(字形轮廓)、hmtx(水平度量)等表 - 字符到字形 - 通过
cmap表将 Unicode 字符映射到 Glyph ID - 提取轮廓 - 从
glyf表读取字形的贝塞尔曲线控制点 - 生成路径 - 将控制点转换为 Flutter
Path对象
基于 Canvas 绘制矢量路径:
// 1. 计算缩放比例(字体单位 → 像素单位)
final scale = fontSize / font.unitsPerEm;
// 2. 生成字符路径
final path = font.generatePathForCharacter(charCode);
// 3. 应用变换(缩放、平移)
final transformedPath = TtfTransform.moveAndScale(path, x, y, scale, scale);
// 4. 绘制到画布
canvas.drawPath(transformedPath, paint);使用 描边 + 填充 双重绘制技术:
// 1. 先绘制描边(增加笔画宽度)
paint.style = PaintingStyle.stroke;
paint.strokeWidth = fontSize * 0.03; // 约 3% 字号宽度
canvas.drawPath(path, paint);
// 2. 再填充中心
paint.style = PaintingStyle.fill;
canvas.drawPath(path, paint);效果: 两次绘制叠加产生加粗效果,无需 Bold 字体文件。
使用 Matrix4 水平剪切变换:
// 剪切矩阵(Skew Transform)
final matrix = Matrix4.identity()
..setEntry(0, 1, -0.2); // 水平剪切,向右倾斜约 12 度
// 应用变换
final italicPath = path.transform(matrix.storage);变换公式:
x' = x + y * skewFactor
y' = y
其中 skewFactor = -0.2 约等于 tan(12°)。
基于 TTF 水平度量 精确计算字符宽度:
// 1. 获取字形的 advanceWidth(字体单位)
final glyphId = font.getGlyphIdForCharacter(charCode);
final advanceWidth = font.getAdvanceWidthForGlyphId(glyphId);
// 2. 转换为像素单位
final charWidth = advanceWidth * (fontSize / font.unitsPerEm) + letterSpacing;
// 3. 累加宽度,超出则换行
if (currentWidth + charWidth > maxWidth && currentLine.isNotEmpty) {
lines.add(currentLine);
currentLine = char;
currentWidth = charWidth;
} else {
currentLine += char;
currentWidth += charWidth;
}当主字体缺少字符时自动降级:
// 1. 尝试主字体
Path charPath = font.generatePathForCharacter(charCode);
// 2. 检查路径是否为空
if (charPath.getBounds().isEmpty && fallbackFonts.isNotEmpty) {
// 3. 遍历备用字体
for (final fallbackFont in fallbackFonts) {
final fallbackPath = fallbackFont.generatePathForCharacter(charCode);
if (!fallbackPath.getBounds().isEmpty) {
charPath = fallbackPath;
effectiveFont = fallbackFont;
break;
}
}
}运行示例应用查看完整功能:
cd example
flutter run- 🎨 多种字体选择 - 宋体、楷体、黑体、方正魏碑等 11 种字体
- 📏 字体大小调整 - 12px ~ 120px 动态调节
- 💪 加粗/斜体切换 - 实时切换合成加粗和斜体效果
- 📊 对比视图 - 与 Flutter 原生 Text 对比显示
- 📸 导出图片 - 支持导出为 PNG 图片(Android 需权限)
- ℹ️ 字体信息 - 查看字体详细信息(unitsPerEm、ascender、descender 等)
- 🔍 字体比对页面 - 批量加载字体进行对比(使用 Isolate 优化)
example/
├── lib/
│ ├── main.dart # 主页面:基础功能演示
│ ├── font_comparison_page.dart # 字体比对页面
│ └── font_config.dart # 字体配置(中文名 ↔ 文件名映射)
├── assets/
│ └── fonts/ # 字体文件
│ ├── song_ti.ttf
│ ├── kai_ti.ttf
│ └── ...
└── pubspec.yaml
flutter pub getflutter analyzeflutter test✅ 像素艺术风格应用 - 需要硬边缘、无抗锯齿的像素风格文本
✅ 精确渲染控制 - 需要对文本渲染的每个细节进行精确控制
✅ 跨平台一致性 - 需要在不同平台保持完全一致的渲染效果
✅ 特殊显示需求 - LED 屏幕、单色显示器等特殊硬件
✅ 路径动画 - 需要对字形路径进行特殊处理(如动画、变形)
✅ 字体缺失场景 - 使用备用字体处理生僻字或特殊字符
❌ 常规文本显示 - 推荐使用 Flutter 原生 Text Widget(性能更优)
❌ 富文本编辑 - 不支持富文本、链接、内联图片等复杂功能
❌ 极大量文本 - 长文章、书籍等大量文本渲染性能较低
// ✅ 推荐:应用启动时预加载常用字体
class MyApp extends StatefulWidget {
@override
void initState() {
super.initState();
_preloadFonts();
}
Future<void> _preloadFonts() async {
final fontManager = NoaaFontManager();
await fontManager.preloadFontsWithIsolate([
'assets/fonts/song_ti.ttf',
'assets/fonts/source_han_sans.ttf',
]);
}
}
// ❌ 不推荐:每次使用时都加载
Widget build(BuildContext context) {
return FutureBuilder(
future: NoaaFontManager().loadFont('assets/fonts/song_ti.ttf'),
builder: (context, snapshot) { ... },
);
}// ✅ 推荐:使用覆盖范围广的字体作为备用
final mainFont = await fontManager.loadFont('assets/fonts/song_ti.ttf');
final fallbackFont = await fontManager.loadFont('assets/fonts/source_han_sans.ttf'); // 思源黑体覆盖更多字符
NoaaText(
'常用字和生僻字混排',
font: mainFont,
style: NoaaTextStyle(
fontFamilyFallback: [fallbackFont],
),
)// ✅ 推荐:复用字体对象
final font = await fontManager.loadFont('assets/fonts/song_ti.ttf');
// 多个地方使用同一个 font 对象
NoaaText('文本1', font: font, ...)
NoaaText('文本2', font: font, ...)
NoaaText('文本3', font: font, ...)
// ❌ 不推荐:重复加载
NoaaText('文本1', font: await fontManager.loadFont(...), ...)
NoaaText('文本2', font: await fontManager.loadFont(...), ...)// ✅ 推荐:定义全局样式常量
class AppTextStyles {
static const heading = NoaaTextStyle(
fontSize: 48.0,
fontWeight: FontWeight.bold,
height: 1.2,
);
static const body = NoaaTextStyle(
fontSize: 24.0,
height: 1.5,
);
}
// 使用
NoaaText('标题', font: font, style: AppTextStyles.heading)请查看 LICENSE 文件。
- flutter_ttf_parser - TTF 字体解析库
欢迎提交 Issue 和 Pull Request!
- 性能考虑 - 路径绘制比原生文本渲染更耗性能,不适合大量文本
- 字体许可 - 确保你有权使用项目中的字体文件
- 发布限制 - 本项目使用 Git 依赖,设置了
publish_to: none,暂不支持发布到 pub.dev - 平台兼容 - 理论上支持所有 Flutter 平台,但主要在 Windows、Android 上测试
Made with ❤️ by Flutter Community