Flutter 文本解读 9 | 打造 Icon 图标字体创建工具

零、前言

1. 前情简介

上一节写了一个小工具,通过 icon_builder.dart 来自动生成对应图标相关的 dart 文件。这样我们从引用自定义的图标只需要: 下载 -> 拷贝-> 生成

现在为止,功能还是比较单薄的,比如字体还需要自己在 pubspec.yaml 中配置,其实作为一个脚本而言,最好的就是一键 OK,所以   pubspec.yaml 中配置也可以通过代码自动完成。再比如说,多个字体图标文件怎么办,如何能更方便地支持多图标字体。

2.本系列其他文章

一、 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'#.*') 将行中的注释临时去掉。 fontLinefamilyLine 分别记录 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<StringString> 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<StringString> 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<StringString> 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 ~