Flutter 文本解读 9 | 打造 Icon 图标字体创建工具
零、前言
1. 前情简介
上一节写了一个小工具,通过 icon_builder.dart
来自动生成对应图标相关的 dart
文件。这样我们从引用自定义的图标只需要: 下载 -> 拷贝-> 生成
。

现在为止,功能还是比较单薄的,比如字体还需要自己在 pubspec.yaml
中配置,其实作为一个脚本而言,最好的就是一键 OK,所以 pubspec.yaml
中配置也可以通过代码自动完成。再比如说,多个字体图标文件怎么办,如何能更方便地支持多图标字体。
2.本系列其他文章
-
《Flutter 文本解读 7 | RichText 写个代码高亮组件》
-
《Flutter 文本解读 8 | Icon 与 RichText 的渊源》
一、 pubspec.yaml 中配置自动生成
1.需求分析
11. 如果没有 fonts: 节点,则创建 fonts: 节点
22. 在 [ pubspec.yaml ] 中自动对 fonts: 节点进行字体图标配置
33. 如果已存在 该字体图标配置 ,则不处理

2.分析 pubspec.yaml
首先说说思路,pubspec.yaml 是一行行配置的,所以我们可以读行。寻找到 fonts
行,看看有没有 该字体图标配置
,如果没有,则在 fonts
行的下一行添加对应节点,最后将字符串行列表写回 pubspec.yaml
即可。那么寻找 fonts 行
呢?也许你会想:用 contains 不就行了吗。但这样的匹配并不精确,可以会误判而出问题,匹配最好使用正则。通过 ^ fonts\:
就可以匹配到以它开头的字符。

为了避免注释对匹配的干扰,在处理时,通过 RegExp(r'#.*')
将行中的注释临时去掉。 fontLine
和 familyLine
分别记录 fonts
和 该字体图标配置
对应的行索引。
1void handleYaml(
2 {String family = 'TolyIcon',
3 String asset = 'assets/iconfont/iconfont.ttf'}) async {
4 File yamlFile = File(path.join(Directory.current.path, 'pubspec.yaml'));
5 List<String> yamlLines = await yamlFile.readAsLines();
6 RegExp fontsReg = RegExp(r'^ fonts\:');
7 RegExp familyReg = RegExp(r'\- family:.*' + family);
8 RegExp commentReg = RegExp(r'#.*');
9
10 int fontLine = -1;
11 int familyLine = -1;
12 for (int i = 0; i < yamlLines.length; i++) {
13 // 去除注释
14 String pureLine = yamlLines[i].replaceAll(commentReg, '');
15 if (fontsReg.hasMatch(pureLine)) {
16 fontLine = i;
17 }
18 if (familyReg.hasMatch(pureLine)) {
19 familyLine = i;
20 }
21 }
22 print('fontLine:$fontLine-----------familyLine:$familyLine---------',);
23}
如下处理,当 fontLine == -1
,则表示 fonts: 节点 不存在,则添加 fonts: 节点和配置。 familyLine == -1
, 则表示 配置不存在,则添加配置
。否则,不处理。
1 String config =
2"""
3 - family: $family
4 fonts:
5 - asset: $asset""";
6
7 if(fontLine == -1){
8 // fontLine 不存在,则添加 fonts: 节点和配置
9 yamlLines.add(' fonts: ');
10 yamlLines.add( config );
11 }else{
12 if(familyLine == -1){
13 // familyLine 不存在,则添加配置
14 yamlLines.insert(fontLine + 1, config);
15 }else{
16 // 否则说明该图标字体已配置,无须处理
17 return;
18 }
19 }
20 await yamlFile.writeAsString(yamlLines.join('\n'));
这样,在 icon_builder.dart
运行后, pubspec.yaml
就会自动把图标字体节点配置好。
3.可配置参数
可以将 字体名
、 字体资源文件夹
、 产出位置
作为配置的参数。这样可以提取一个 buildAnIconFont
方法用于构建一个 字体图标
文件。

1main() async {
2 String cssPath = 'assets/iconfont/iconfont.css'; // 样式路径
3 String fontPath = 'assets/iconfont/iconfont.ttf'; // 字体路径
4 String fontName = 'TolyIcon'; // 字体名称
5 String dist = 'generate/icon'; //输出文件地址
6
7 await buildAnIconFont(cssPath, fontPath, fontName, dist);
8}
注意一点, .css
样式文件在生成 .dart
文件后,其使命就完成了,可以删除。
1Future<void> buildAnIconFont(String fontDir, String fontName, String dist) async {
2 String asset = '$fontDir/$fontName.ttf'; //输出文件地址
3 File target = File(path.join(Directory.current.path, fontDir, '$fontName.css'));
4 if(!target.existsSync()) return; // 样式文件不存在,则直接返回
5
6 String str = await target.readAsString();
7 List<String> names = [];
8 List<String> unicodes = [];
9 StringScanner _scanner = StringScanner(str);
10
11 while (!_scanner.isDone) {
12 if (_scanner.scan(RegExp(r'\.icon-(.*?):'))) {
13 String word = _scanner.lastMatch[1];
14 names.add(word);
15 }
16
17 if (_scanner.scan(RegExp(r'"\\(.*?)"'))) {
18 String word = _scanner.lastMatch[1];
19 unicodes.add(word);
20 }
21
22 if (!_scanner.isDone) {
23 _scanner.position++;
24 }
25 }
26
27 assert(names.length == unicodes.length);
28
29 Map<String, String> iconMap = Map.fromIterables(names, unicodes);
30 String code = getCode(iconMap, fontName: fontName);
31 await save2File(code, filePath: dist, fontName: fontName);
32 await handleYaml(family: fontName, asset: asset);
33 await target.delete(); // 删除样式文件
34}
二、 多个字体图标文件处理
1.多图标字体分析
其实在图标网站可以通过 项目
来管理图标,一般一个项目一个图标文件就够了。但如果真的有多个图标文件的需求,也可以将 icon_builder.dart
再优化一些。

就目前的小工具而言,再引入一个 Ruby
的字体文件,构建一下。也可以自动生成对应的 .dart
文件,以及自动配置 fonts
节点。

不过还需要手动修改些配置,有一丢丢的小麻烦。想要不麻烦,那就 用规范来减少配置
。现在要求 .css 和 .ttf
的文件名相同,且文件名即为字体名。这样就可以遍历文件夹,解析文件名,从而减少配置。
2.代码处理
多字体文件放置如下,只需要配置 资源目录
和 输出目录
即可。

1main() {
2 String resDir = 'assets/iconfont'; // 字体位置
3 String dist = 'generate/icon'; //输出文件地址
4 parserResDir(resDir, dist);
5}
6
7void parserResDir(String resDir,String dist) async{
8 Directory dir = Directory(path.join(Directory.current.path, resDir));
9 List files = dir.listSync();
10
11 for(int i = 0; i < files.length ; i++){
12 File file = files[i];
13 if (file is File && file.path.endsWith('.css')) {
14 String fontName = path.basenameWithoutExtension(file.path);
15 await buildAnIconFont(resDir,fontName, dist);
16 }
17 }
18}
这样运行 icon_builder.dart
过后,1. css
文件会被删除;2. 相应的 .dart
文件会自动生成;3. pubspec.yaml
会自动配置。可以说已经很不错了。

3.字体类的融合
如果想要使用两种字体,但只想通过一个类进行调用,这样就不会生成过多的类,使用起来方便些。其实处理起来也很简单,设置两个标识,用于是否开启 mergeClass
以及融合后的类名。融合后效果如下,两个字体通过一个 .dart
文件管理。
1bool mergeClass = true;
2String className = 'TolyIcon';

这样就可以通过一个类,同时使用多个字体文件:

1Wrap(
2 spacing: 20,
3 children: [
4 Icon(TolyIcon.icon_collect, size: 50,),
5 Icon(TolyIcon.icon_ruby, size: 50,)
6]);
三、icon_builder.dart 完整代码
代码一共也就 170
行,但功能还不错。随便写写,代码结构上有待优化,其中包含了很多文件处理,字符串分析的知识,这些都挺好玩的。有什么更好的想法,也可以和我在群里交流。其实按照这个逻辑做成 AS 插件
或 Gradle 插件
也未尝不可。不过通过一个小脚本也比较方便,运行一下就 OK 了 。谢谢观看 ~
1import 'dart:io';
2import 'package:string_scanner/string_scanner.dart';
3import 'package:path/path.dart' as path;
4
5/// create by 张风捷特烈 on 2021/1/22
6/// contact me by email 1981462002@qq.com
7/// 说明: iconfont 解析构造器
8
9bool deleteCss = true; // 是否删除 css
10bool mergeClass = true; // 多个字体文件时是否融合成一个类
11String className = 'TolyIcon'; // 融合成一个类时类名
12String resDir = 'assets/iconfont'; //资源文件地址
13String dist = 'generate/icon'; //输出文件地址
14
15main() async {
16 File target = File(path.join(Directory.current.path, 'lib', dist, '$className.dart'));
17 if(mergeClass&&target.existsSync()) {
18 await target.writeAsString('');
19 }
20
21 await parserResDir(resDir, dist);
22
23 if (mergeClass) {
24 String content = await target.readAsString();
25 String result = """import 'package:flutter/widgets.dart';
26//Power By 张风捷特烈 --- Generated file. Do not edit.
27
28class $className {
29 $className._();
30""";
31 result += content;
32 result += "}";
33 await target.writeAsString(result);
34 }
35}
36
37Future<void> parserResDir(String resDir, String dist) async {
38 Directory dir = Directory(path.join(Directory.current.path, resDir));
39 List files = dir.listSync();
40
41 for (int i = 0; i < files.length; i++) {
42 File file = files[i];
43 if (file is File && file.path.endsWith('.css')) {
44 String fontName = path.basenameWithoutExtension(file.path);
45 await buildAnIconFont(resDir, fontName, dist);
46 }
47 }
48}
49
50Future<void> buildAnIconFont(
51 String resDir, String fontName, String dist) async {
52 String fontPath = '$resDir/$fontName.ttf';
53 String cssPath = '$resDir/$fontName.css';
54 File target = File(path.join(Directory.current.path, cssPath));
55 if (!target.existsSync()) return;
56
57 String str = await target.readAsString();
58
59 List<String> names = [];
60 List<String> unicodes = [];
61 StringScanner _scanner = StringScanner(str);
62
63 while (!_scanner.isDone) {
64 if (_scanner.scan(RegExp(r'\.icon-(.*?):'))) {
65 String word = _scanner.lastMatch[1];
66 names.add(word);
67 }
68
69 if (_scanner.scan(RegExp(r'"\\(.*?)"'))) {
70 String word = _scanner.lastMatch[1];
71 unicodes.add(word);
72 }
73
74 if (!_scanner.isDone) {
75 _scanner.position++;
76 }
77 }
78 assert(names.length == unicodes.length);
79
80 Map<String, String> iconMap = Map.fromIterables(names, unicodes);
81 String code = getCode(iconMap, fontName: fontName);
82 await save2File(code, filePath: dist, fontName: fontName);
83 await handleYaml(family: fontName, asset: fontPath);
84 if (deleteCss) await target.delete();
85 // 删除样式文件
86 print('创建 $fontName 完毕!');
87}
88
89Future<void> handleYaml({String family = 'TolyIcon',
90 String asset = 'assets/iconfont/iconfont.ttf'}) async {
91 File yamlFile = File(path.join(Directory.current.path, 'pubspec.yaml'));
92 List<String> yamlLines = await yamlFile.readAsLines();
93 RegExp fontsReg = RegExp(r'^ fonts\:');
94 RegExp familyReg = RegExp(r'\- family:.*' + family);
95 RegExp commentReg = RegExp(r'#.*');
96
97 int fontLine = -1;
98 int familyLine = -1;
99 for (int i = 0; i < yamlLines.length; i++) {
100 // 去除注释
101 String pureLine = yamlLines[i].replaceAll(commentReg, '');
102 if (fontsReg.hasMatch(pureLine)) {
103 fontLine = i;
104 }
105 if (familyReg.hasMatch(pureLine)) {
106 familyLine = i;
107 }
108 }
109
110 String config = """
111 - family: $family
112 fonts:
113 - asset: $asset""";
114
115 if (fontLine == -1) {
116 // fontLine 不存在,则添加 fonts: 节点和配置
117 yamlLines.add(' fonts: ');
118 yamlLines.add(config);
119 } else {
120 if (familyLine == -1) {
121 // familyLine 不存在,则添加节点和配置
122 yamlLines.insert(fontLine + 1, config);
123 } else {
124 // 否则说明该图标字体已配置,无须处理
125 return;
126 }
127 }
128 await yamlFile.writeAsString(yamlLines.join('\n'));
129}
130
131Future<void> save2File(String content,
132 {String filePath: 'generate/icon', String fontName: 'TolyIcon'}) async {
133
134 if(mergeClass){
135 File target = File(
136 path.join(Directory.current.path, 'lib', filePath, '$className.dart'));
137 if (!target.existsSync()) {
138 await target.create(recursive: true);
139 }
140 await target.writeAsString(content,mode: FileMode.append);
141 }else{
142 File target = File(
143 path.join(Directory.current.path, 'lib', filePath, '$fontName.dart'));
144 if (!target.existsSync()) {
145 await target.create(recursive: true);
146 }
147 await target.writeAsString(content);
148 }
149}
150
151String getCode(Map<String, String> iconMap, {String fontName: 'TolyIcon'}) {
152 String content = '';
153 iconMap.forEach((key, value) {
154 content +=
155 """static const IconData $key = IconData( 0x$value, fontFamily: "$fontName");\n""";
156 });
157
158 if (mergeClass) {
159 return content;
160 }
161 String result = """import 'package:flutter/widgets.dart';
162//Power By 张风捷特烈 --- Generated file. Do not edit.
163
164class $fontName {
165 $fontName._();
166""";
167 result += content;
168 result += "}";
169 return result;
170}
@张风捷特烈 2021.01.24 未允禁转
我的公众号:编程之王
联系我--邮箱:1981462002@qq.com -- 微信:zdl1994328
~ END ~