@charset "UTF-8";.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:15px;overflow-x:hidden;color:#333}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{line-height:1.5;margin-top:35px;margin-bottom:10px;padding-bottom:5px}.markdown-body h1:first-child,.markdown-body h2:first-child,.markdown-body h3:first-child,.markdown-body h4:first-child,.markdown-body h5:first-child,.markdown-body h6:first-child{margin-top:-1.5rem;margin-bottom:1rem}.markdown-body h1:before,.markdown-body h2:before,.markdown-body h3:before,.markdown-body h4:before,.markdown-body h5:before,.markdown-body h6:before{content:"#";display:inline-block;color:#3eaf7c;padding-right:.23em}.markdown-body h1{position:relative;font-size:2.5rem;margin-bottom:5px}.markdown-body h1:before{font-size:2.5rem}.markdown-body h2{padding-bottom:.5rem;font-size:2.2rem;border-bottom:1px solid #ececec}.markdown-body h3{font-size:1.5rem;padding-bottom:0}.markdown-body h4{font-size:1.25rem}.markdown-body h5{font-size:1rem}.markdown-body h6{margin-top:5px}.markdown-body p{line-height:inherit;margin-top:22px;margin-bottom:22px}.markdown-body strong{color:#3eaf7c}.markdown-body img{max-width:100%;border-radius:2px;display:block;margin:auto;border:3px solid rgba(62,175,124,.2)}.markdown-body hr{border:none;border-top:1px solid #3eaf7c;margin-top:32px;margin-bottom:32px}.markdown-body code{word-break:break-word;overflow-x:auto;padding:.2rem .5rem;margin:0;color:#3eaf7c;font-weight:700;font-size:.85em;background-color:rgba(27,31,35,.05);border-radius:3px}.markdown-body code,.markdown-body pre{font-family:Menlo,Monaco,Consolas,Courier New,monospace}.markdown-body pre{overflow:auto;position:relative;line-height:1.75;border-radius:6px;border:2px solid #3eaf7c}.markdown-body pre>code{font-size:12px;padding:15px 12px;margin:0;word-break:normal;display:block;overflow-x:auto;color:#333;background:#f8f8f8}.markdown-body a{font-weight:500;text-decoration:none;color:#3eaf7c}.markdown-body a:active,.markdown-body a:hover{border-bottom:1.5px solid #3eaf7c}.markdown-body a:before{content:"⇲"}.markdown-body table{display:inline-block!important;font-size:12px;width:auto;max-width:100%;overflow:auto;border:1px solid #3eaf7c}.markdown-body thead{background:#3eaf7c;color:#fff;text-align:left}.markdown-body tr:nth-child(2n){background-color:rgba(62,175,124,.2)}.markdown-body td,.markdown-body th{padding:12px 7px;line-height:24px}.markdown-body td{min-width:120px}.markdown-body blockquote{color:#666;padding:1px 23px;margin:22px 0;border-left:.5rem solid;border-color:#42b983;background-color:#f8f8f8}.markdown-body blockquote:after{display:block;content:""}.markdown-body blockquote>p{margin:10px 0}.markdown-body details{outline:none;border:none;border-left:4px solid #3eaf7c;padding-left:10px;margin-left:4px}.markdown-body details summary{cursor:pointer;border:none;outline:none;background:#fff;margin:0 -17px}.markdown-body details summary::-webkit-details-marker{color:#3eaf7c}.markdown-body ol,.markdown-body ul{padding-left:28px}.markdown-body ol li,.markdown-body ul li{margin-bottom:0;list-style:inherit}.markdown-body ol li .task-list-item,.markdown-body ul li .task-list-item{list-style:none}.markdown-body ol li .task-list-item ol,.markdown-body ol li .task-list-item ul,.markdown-body ul li .task-list-item ol,.markdown-body ul li .task-list-item ul{margin-top:0}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:3px}.markdown-body ol li{padding-left:6px}.markdown-body ol li::marker{color:#3eaf7c}.markdown-body ul li{list-style:none}.markdown-body ul li:before{content:"•";margin-right:4px;color:#3eaf7c}@media (max-width:720px){.markdown-body h1{font-size:24px}.markdown-body h2{font-size:20px}.markdown-body h3{font-size:18px}}
零、前言
1. 前情简介
上一节写了一个小工具,通过 icon_builder.dart
来自动生成对应图标相关的 dart
文件。这样我们从引用自定义的图标只需要: 下载 -> 拷贝-> 生成
。
现在为止,功能还是比较单薄的,比如字体还需要自己在 pubspec.yaml
中配置,其实作为一个脚本而言,最好的就是一键 OK,所以 pubspec.yaml
中配置也可以通过代码自动完成。再比如说,多个字体图标文件怎么办,如何能更方便地支持多图标字体。
2.本系列其他文章
- 《Flutter 文本解读 1 | 从源码认识 Text 组件》
- 《Flutter 文本解读 2 | Text 是如何画出来的》
- 《Flutter 文本解读 3 | Text 组件使用介绍 》
- 《Flutter 文本解读 4 | TextStyle 文字样式解读 》
- 《Flutter 文本解读 5 | RichText 富文本的使用 (上)》
- 《Flutter 文本解读 6 | RichText 富文本的使用 (中)》
- 《Flutter 文本解读 7 | RichText 写个代码高亮组件》
- 《Flutter 文本解读 8 | Icon 与 RichText 的渊源》
一、 pubspec.yaml 中配置自动生成
1.需求分析
代码语言:javascript复制1. 如果没有 fonts: 节点,则创建 fonts: 节点
2. 在 [ pubspec.yaml ] 中自动对 fonts: 节点进行字体图标配置
3. 如果已存在 该字体图标配置 ,则不处理
2.分析 pubspec.yaml
首先说说思路,pubspec.yaml 是一行行配置的,所以我们可以读行。寻找到 fonts
行,看看有没有 该字体图标配置
,如果没有,则在 fonts
行的下一行添加对应节点,最后将字符串行列表写回 pubspec.yaml
即可。那么寻找 fonts 行
呢?也许你会想:用 contains 不就行了吗。但这样的匹配并不精确,可以会误判而出问题,匹配最好使用正则。通过 ^ fonts:
就可以匹配到以它开头的字符。
为了避免注释对匹配的干扰,在处理时,通过 RegExp(r'#.*')
将行中的注释临时去掉。fontLine
和 familyLine
分别记录 fonts
和 该字体图标配置
对应的行索引。
void handleYaml(
{String family = 'TolyIcon',
String asset = 'assets/iconfont/iconfont.ttf'}) async {
File yamlFile = File(path.join(Directory.current.path, 'pubspec.yaml'));
List<String> yamlLines = await yamlFile.readAsLines();
RegExp fontsReg = RegExp(r'^ fonts:');
RegExp familyReg = RegExp(r'- family:.*' family);
RegExp commentReg = RegExp(r'#.*');
int fontLine = -1;
int familyLine = -1;
for (int i = 0; i < yamlLines.length; i ) {
// 去除注释
String pureLine = yamlLines[i].replaceAll(commentReg, '');
if (fontsReg.hasMatch(pureLine)) {
fontLine = i;
}
if (familyReg.hasMatch(pureLine)) {
familyLine = i;
}
}
print('fontLine:$fontLine-----------familyLine:$familyLine---------',);
}
复制代码
如下处理,当 fontLine == -1
,则表示 fonts: 节点 不存在,则添加 fonts: 节点和配置。familyLine == -1
, 则表示 配置不存在,则添加配置
。否则,不处理。
String config =
"""
- family: $family
fonts:
- asset: $asset""";
if(fontLine == -1){
// fontLine 不存在,则添加 fonts: 节点和配置
yamlLines.add(' fonts: ');
yamlLines.add( config );
}else{
if(familyLine == -1){
// familyLine 不存在,则添加配置
yamlLines.insert(fontLine 1, config);
}else{
// 否则说明该图标字体已配置,无须处理
return;
}
}
await yamlFile.writeAsString(yamlLines.join('n'));
复制代码
这样,在 icon_builder.dart
运行后,pubspec.yaml
就会自动把图标字体节点配置好。
3.可配置参数
可以将 字体名
、字体资源文件夹
、产出位置
作为配置的参数。这样可以提取一个 buildAnIconFont
方法用于构建一个 字体图标
文件。
main() async {
String cssPath = 'assets/iconfont/iconfont.css'; // 样式路径
String fontPath = 'assets/iconfont/iconfont.ttf'; // 字体路径
String fontName = 'TolyIcon'; // 字体名称
String dist = 'generate/icon'; //输出文件地址
await buildAnIconFont(cssPath, fontPath, fontName, dist);
}
注意一点,.css
样式文件在生成 .dart
文件后,其使命就完成了,可以删除。
Future<void> buildAnIconFont(String fontDir, String fontName, String dist) async {
String asset = '$fontDir/$fontName.ttf'; //输出文件地址
File target = File(path.join(Directory.current.path, fontDir, '$fontName.css'));
if(!target.existsSync()) return; // 样式文件不存在,则直接返回
String str = await target.readAsString();
List<String> names = [];
List<String> unicodes = [];
StringScanner _scanner = StringScanner(str);
while (!_scanner.isDone) {
if (_scanner.scan(RegExp(r'.icon-(.*?):'))) {
String word = _scanner.lastMatch[1];
names.add(word);
}
if (_scanner.scan(RegExp(r'"\(.*?)"'))) {
String word = _scanner.lastMatch[1];
unicodes.add(word);
}
if (!_scanner.isDone) {
_scanner.position ;
}
}
assert(names.length == unicodes.length);
Map<String, String> iconMap = Map.fromIterables(names, unicodes);
String code = getCode(iconMap, fontName: fontName);
await save2File(code, filePath: dist, fontName: fontName);
await handleYaml(family: fontName, asset: asset);
await target.delete(); // 删除样式文件
}
复制代码
二、 多个字体图标文件处理
1.多图标字体分析
其实在图标网站可以通过项目
来管理图标,一般一个项目一个图标文件就够了。但如果真的有多个图标文件的需求,也可以将 icon_builder.dart
再优化一些。
就目前的小工具而言,再引入一个 Ruby
的字体文件,构建一下。也可以自动生成对应的 .dart
文件,以及自动配置 fonts
节点。
不过还需要手动修改些配置,有一丢丢的小麻烦。想要不麻烦,那就用规范来减少配置
。现在要求 .css 和 .ttf
的文件名相同,且文件名即为字体名。这样就可以遍历文件夹,解析文件名,从而减少配置。
2.代码处理
多字体文件放置如下,只需要配置资源目录
和 输出目录
即可。
main() {
String resDir = 'assets/iconfont'; // 字体位置
String dist = 'generate/icon'; //输出文件地址
parserResDir(resDir, dist);
}
void parserResDir(String resDir,String dist) async{
Directory dir = Directory(path.join(Directory.current.path, resDir));
List files = dir.listSync();
for(int i = 0; i < files.length ; i ){
File file = files[i];
if (file is File && file.path.endsWith('.css')) {
String fontName = path.basenameWithoutExtension(file.path);
await buildAnIconFont(resDir,fontName, dist);
}
}
}
这样运行 icon_builder.dart
过后,1. css
文件会被删除;2. 相应的.dart
文件会自动生成;3. pubspec.yaml
会自动配置。可以说已经很不错了。
3.字体类的融合
如果想要使用两种字体,但只想通过一个类进行调用,这样就不会生成过多的类,使用起来方便些。其实处理起来也很简单,设置两个标识,用于是否开启 mergeClass
以及融合后的类名。融合后效果如下,两个字体通过一个 .dart
文件管理。
bool mergeClass = true;
String className = 'TolyIcon';
这样就可以通过一个类,同时使用多个字体文件:
代码语言:javascript复制Wrap(
spacing: 20,
children: [
Icon(TolyIcon.icon_collect, size: 50,),
Icon(TolyIcon.icon_ruby, size: 50,)
]);
三、icon_builder.dart 完整代码
代码一共也就 170
行,但功能还不错。随便写写,代码结构上有待优化,其中包含了很多文件处理,字符串分析的知识,这些都挺好玩的。有什么更好的想法,也可以和我在群里交流。其实按照这个逻辑做成 AS 插件
或Gradle 插件
也未尝不可。不过通过一个小脚本也比较方便,运行一下就 OK 了 。谢谢观看 ~
import 'dart:io';
import 'package:string_scanner/string_scanner.dart';
import 'package:path/path.dart' as path;
/// create by 张风捷特烈 on 2021/1/22
/// contact me by email 1981462002@qq.com
/// 说明: iconfont 解析构造器
bool deleteCss = true; // 是否删除 css
bool mergeClass = true; // 多个字体文件时是否融合成一个类
String className = 'TolyIcon'; // 融合成一个类时类名
String resDir = 'assets/iconfont'; //资源文件地址
String dist = 'generate/icon'; //输出文件地址
main() async {
File target = File(path.join(Directory.current.path, 'lib', dist, '$className.dart'));
if(mergeClass&&target.existsSync()) {
await target.writeAsString('');
}
await parserResDir(resDir, dist);
if (mergeClass) {
String content = await target.readAsString();
String result = """import 'package:flutter/widgets.dart';
//Power By 张风捷特烈 --- Generated file. Do not edit.
class $className {
$className._();
""";
result = content;
result = "}";
await target.writeAsString(result);
}
}
Future<void> parserResDir(String resDir, String dist) async {
Directory dir = Directory(path.join(Directory.current.path, resDir));
List files = dir.listSync();
for (int i = 0; i < files.length; i ) {
File file = files[i];
if (file is File && file.path.endsWith('.css')) {
String fontName = path.basenameWithoutExtension(file.path);
await buildAnIconFont(resDir, fontName, dist);
}
}
}
Future<void> buildAnIconFont(
String resDir, String fontName, String dist) async {
String fontPath = '$resDir/$fontName.ttf';
String cssPath = '$resDir/$fontName.css';
File target = File(path.join(Directory.current.path, cssPath));
if (!target.existsSync()) return;
String str = await target.readAsString();
List<String> names = [];
List<String> unicodes = [];
StringScanner _scanner = StringScanner(str);
while (!_scanner.isDone) {
if (_scanner.scan(RegExp(r'.icon-(.*?):'))) {
String word = _scanner.lastMatch[1];
names.add(word);
}
if (_scanner.scan(RegExp(r'"\(.*?)"'))) {
String word = _scanner.lastMatch[1];
unicodes.add(word);
}
if (!_scanner.isDone) {
_scanner.position ;
}
}
assert(names.length == unicodes.length);
Map<String, String> iconMap = Map.fromIterables(names, unicodes);
String code = getCode(iconMap, fontName: fontName);
await save2File(code, filePath: dist, fontName: fontName);
await handleYaml(family: fontName, asset: fontPath);
if (deleteCss) await target.delete();
// 删除样式文件
print('创建 $fontName 完毕!');
}
Future<void> handleYaml({String family = 'TolyIcon',
String asset = 'assets/iconfont/iconfont.ttf'}) async {
File yamlFile = File(path.join(Directory.current.path, 'pubspec.yaml'));
List<String> yamlLines = await yamlFile.readAsLines();
RegExp fontsReg = RegExp(r'^ fonts:');
RegExp familyReg = RegExp(r'- family:.*' family);
RegExp commentReg = RegExp(r'#.*');
int fontLine = -1;
int familyLine = -1;
for (int i = 0; i < yamlLines.length; i ) {
// 去除注释
String pureLine = yamlLines[i].replaceAll(commentReg, '');
if (fontsReg.hasMatch(pureLine)) {
fontLine = i;
}
if (familyReg.hasMatch(pureLine)) {
familyLine = i;
}
}
String config = """
- family: $family
fonts:
- asset: $asset""";
if (fontLine == -1) {
// fontLine 不存在,则添加 fonts: 节点和配置
yamlLines.add(' fonts: ');
yamlLines.add(config);
} else {
if (familyLine == -1) {
// familyLine 不存在,则添加节点和配置
yamlLines.insert(fontLine 1, config);
} else {
// 否则说明该图标字体已配置,无须处理
return;
}
}
await yamlFile.writeAsString(yamlLines.join('n'));
}
Future<void> save2File(String content,
{String filePath: 'generate/icon', String fontName: 'TolyIcon'}) async {
if(mergeClass){
File target = File(
path.join(Directory.current.path, 'lib', filePath, '$className.dart'));
if (!target.existsSync()) {
await target.create(recursive: true);
}
await target.writeAsString(content,mode: FileMode.append);
}else{
File target = File(
path.join(Directory.current.path, 'lib', filePath, '$fontName.dart'));
if (!target.existsSync()) {
await target.create(recursive: true);
}
await target.writeAsString(content);
}
}
String getCode(Map<String, String> iconMap, {String fontName: 'TolyIcon'}) {
String content = '';
iconMap.forEach((key, value) {
content =
"""static const IconData $key = IconData( 0x$value, fontFamily: "$fontName");n""";
});
if (mergeClass) {
return content;
}
String result = """import 'package:flutter/widgets.dart';
//Power By 张风捷特烈 --- Generated file. Do not edit.
class $fontName {
$fontName._();
""";
result = content;
result = "}";
return result;
}
复制代码
@张风捷特烈 2021.01.24 未允禁转
我的公众号:编程之王
联系我--邮箱:1981462002@qq.com -- 微信:zdl1994328
~ END ~