Flutter 文本解读 8 | Icon 与 RichText 的渊源

3,638 阅读6分钟

零、前言

首先,掘金作者榜,请投我一票~,正文开始:

1、关于 Icon 组件

图标是每个应用都不可或缺的,我们通过 Icon 组件,指定一个图标就可以进行显示。可以随意指定其 颜色、大小。比起图片来说, Icon 不会因放大而失真。但你想过 Icon 组件是如何实现的吗?你有想过 Icons.android_rounded 到底代表什么吗?我跟你说它的本质就是文字你相信吗?接下来的文本解读系列,将花 2~3 篇来详细聊聊这个 Icon 组件。

2.本系列其他文章

一、认识 Icon 组件

1. Icon 组件的源码实现

Icon 是一个 StatelessWidget 组件,说明它是个打酱油的,内部 build 到底依赖其他 Widget 实现。构造方法中,向外暴露了几个属性以供用户使用,如 颜色、大小、图标数据等。

简单瞄一下源码中的 Icon#build 方法,可以看到其内部使用了 RichText,也就说明,Icon 组件的本质也是一种文本。至于更细的代码,现在先不看,先说写前置的知识。


2. IconData 到底是什么

我们一直用的 Icons.xxx 就可以获取到对应的图标数据,你有么有想过,这玩意到底是什么东西。其实不难看出,就是 Icons 类中的一个静态常量,类型为 IconData。

IconData 类如下,它主要需要 int 型的 codePoint 对象和 String 型的 fontFamily。也就是说,需要从一个字体文件中通过 一个 int 值 获取对应的 '文字'。


3. MaterialIcons 图标字体位置

你不禁会问,那 MaterialIcons 字体在哪里?如果你查看一下 应用的数据, 可以发现如下的图标字体文件。这就是图标数据的来源。

这时你也许会想,我可以用自己的 图标字体 ,默认的这些可以不用,毕竟两个加起来有 1 M 多,挺浪费空间的。可以将 pubspec.yaml 中的 uses-material-design 置为 false 即可,这样就不会将默认的图标字体 打包到应用中。这时你需要注意:Flutter 中一些组件会使用这些字体,在用的时候,换成自己的即可。


4. Icon 和 文字的联系

通过前面的描述,你应该了解 Icon 和 文字的联系了。下面通过两个小例子,加深一些了解。
图标字体 本身也是字体,如果不指定,会使用默认的字体。如下,随便写个 int 值,如果在默认字体中找到了,也是会显示出来的。


图标字体 本身也是字体,所以通过对应的 Unicode ,也可以通过 Text 显示出来字体图标。

甚至你可以通过 TextStyle 对图标进行样式处理,想一想第四篇花里胡哨 的那些东西,也可以用在字体图标中。因为它们的本质是一样的,都是基于 RichText 组件,通过 RenderParagraph 绘制的。这便是知识的联系与贯通。


二、如何自定义图标字体

1.寻找图标字体

我最喜欢的图标网站是 www.iconfont.cn/ 。在这里有海量的图标,提供下载。也可以将自己设计的图标上传进去。

通过搜索,将自己喜欢的图标加入小篮子。

你可以直接下载图标,但我建议最好建个项目 ,方便以后管理。

下面新建的 ruby 的项目,就可以将图标加入其中。


2.修改和下载图标字体

悬浮时可以看到修改的按钮,点击进入修改界面。


在这里可以设置 图标的名字 和 Unicode 。没错,就是 IconData 类中的那个 int 值。


修改完点击下载即可。

其中 iconfont.ttf 就是对于的图标字体文件。

另外 iconfont.css 中记录着 图标的信息。所以想要在 Flutter 中显示一个 图标字体 的两大要素都具备了,就差使用了。


3.在 Flutter 中使用图标字体

首先需要 引入资源 并在 pubspec.yaml 中进行配置。注:文件位置和文件名无所谓,只要对应即可。


这样,就可以将下载的图标字体用在 Flutter 中了。当然,我们也可以仿照 Icons 源码那样,提供一个 TolyIcon.XXX 来获取 IconData 数据。

class IconShow extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Icon(
      TolyIcon.ruby,
      size: 50,
    );
  }
}

class TolyIcon{
  static const IconData ruby = IconData(0xe64c,fontFamily: 'TolyIcon');
}

这时,你也许会说,哇,这 Unicode 一个个敲吗?好麻烦啊。你不知道有种力量叫做工具,程序就是为了解决这些无脑劳动而诞生的。下面来一起看看,如何 让代码生成代码。


三、动手写个小工具

1. 实现分析

其实原理很简单,我们只需要通过解析 iconfont.css 中的内容,获取到每个图标的 名称 和 Unicode 码 即可。然后通过代码生成 TolyIcon 类即可,也就是以后自定义字体图标的流程为: 下载 -> 拷贝-> 生成。


2.图标名和 Unicode 码 提取

如下,通过正则 \.icon-(.*?): 就可以匹配出所有的图标名字。


通过 "\\(.*?)" 可以匹配搭配到 Unicode 码 。


3.Dart 中正则的使用

通过 StringScanner 可以根据正则对字符串进行扫描匹配。如下图我们就获取了 图标名称。上面正则中,通过 () 对匹配的字符串进行分组,下面 _scanner.lastMatch[1] 说明是第一组,也就是括号里的。第 0 组默认是匹配的全字符。

main()async{
  
  File target = File('${Directory.current.path}/assets/iconfont/iconfont.css');
  
  String str = await target.readAsString();

  StringScanner _scanner = StringScanner(str);

  while (!_scanner.isDone) {
    if (_scanner.scan(RegExp(r'\.(.*?):'))) {
      String word = _scanner.lastMatch[1];
      print(word);
    }

    if (!_scanner.isDone) {
      _scanner.position++;
    }
  }
}

同理,可以获取 Unicode 码,通过两个 String 列表收集信息。这样就从 iconfont.css 中提取出了最关键的数据。

main() async {
  File target = File('${Directory.current.path}/assets/iconfont/iconfont.css');

  String str = await target.readAsString();

  Map<String, String> iconInfo = {};

  List<String> names = [];
  List<String> unicodes = [];
  StringScanner _scanner = StringScanner(str);
	// 根据正则扫描 
  while (!_scanner.isDone) {
    if (_scanner.scan(RegExp(r'\.(.*?):'))) {
      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
  Map<String,String> iconMap = Map.fromIterables(names, unicodes);
  iconMap.forEach((key, value) {
    print('----$key-----$value---');
  });
}

4.生成代码文件

信息在手,就可以自动生成代码了,操作如下:

String getCode(Map<String, String> iconMap, {String fontName: 'TolyIcon'}) {  
  String result = """import 'package:flutter/widgets.dart';
//Power By 张风捷特烈 --- Generated file. Do not edit.

class $fontName {
    $fontName._();
""";
  iconMap.forEach((key, value) {
    result +=
        """static const IconData $key = IconData( 0x$value, fontFamily: "$fontName");\n""";
  });
  result += "}";
  return result;
}

然后将其写入文件即可,运行 icon_builder.dart 就会自动生成对应的 dart 文件。

void save2File(String content,
               {String filePath: 'generate/icon',
                String fontName: 'TolyIcon'}) async{
  
  File target = File(path.join(Directory.current.path,'lib',filePath,'$fontName.dart'));
  if(!target.existsSync()){
   await target.create(recursive: true);
  }
  await target.writeAsString(content);
}

到这里,最基本的和核心的处理就结束了,但这也仅仅是个简单的工具。在下一篇中,将对这个工具进行改进,让它更方便使用。毕竟...