什么是可变字体(Variable Font)
普通的字体可能有多个字重(比如 light、regular、bold)等,他们分别是一个个独立的静态的文件,在某些情况下,我们可能需要在应用中使用自定义的字体。可是,相对于西文字体来说,中文字体的体积相对偏高,单个字体文件的大小可能就有 15 MB 以上,并且在某些情况下,我们可能需要引入多个字重的字体,比如粗体和细体,无形中又增大了静态文件的大小。而可变字体,简单一点理解就是将多个字重的字体合并为一个字体。
可变字体的选择
虽然可变字体的好处众多,但可惜的是,对于中文字体来说,可选择的字体太少,到目前为止,较为优秀的中文可变字体只有 MiSans 的 VF 版本,以及 思源黑体 的 VF 版本,值得高兴的是,这两个字体均提供了免费的商用。接下来我以 MiSans 为例。
引入可变字体
首先在 MiSans 官网下载字体,下载出来后解压,其中有一个名为 MiSans VF.ttf
的文件,就是我们需要的可变字体。可以看到体积仅有 19 MB,如果还是觉得打包体积过大,可以采取应用下载后分发的方式加载。这样也可以减少打包的大小。
创建文件夹
在 Flutter 项目的根目录下创建一个 font
文件夹,注意不是 lib 目录下,而是和 lib 同级,之后将文件拷贝到目录下,完成后的项目结构看起来像这样。
引入字体文件
打开 pubspec.yaml
在 flutter
块中加入如下代码。
fonts:
- family: MiSans VF
fonts:
- asset: fonts/MiSans VF.ttf
修改后的文件看起来像这样。
这里通过静态方法进行引入,会将字体作为静态文件打包到安装包里,会带来一定的体积增加,如果想从网络分发字体,可以使用 FontLoader
加载,这里不展开讲。
引入后不要忘记运行一次 pub get
。
全局使用字体
找到应用入口的 MaterialApp()
块,通常在 main.dart
中,分别有两个属性 theme
和 darkTheme
,用于控制浅色和深色模式的样式,需要对其修改,重新运行应用后,字体就被全局修改为了 MiSans。
theme: ThemeData(
fontFamily: 'MiSans VF',
fontFamilyFallback: const ['MiSans VF'],
),
darkTheme: ThemeData(
fontFamily: 'MiSans VF',
fontFamilyFallback: const ['MiSans VF'],
),
使用可变字体
可变字体最大的特征就是可变,在 Flutter 中,文字样式属性 TextStyle
的定义如下。
const TextStyle({
this.inherit = true,
this.color,
this.backgroundColor,
this.fontSize,
this.fontWeight,
this.fontStyle,
this.letterSpacing,
this.wordSpacing,
this.textBaseline,
this.height,
this.leadingDistribution,
this.locale,
this.foreground,
this.background,
this.shadows,
this.fontFeatures,
this.fontVariations,
this.decoration,
this.decorationColor,
this.decorationStyle,
this.decorationThickness,
this.debugLabel,
String? fontFamily,
List<String>? fontFamilyFallback,
String? package,
this.overflow,
}) : fontFamily = package == null ? fontFamily : 'packages/$package/$fontFamily',
_fontFamilyFallback = fontFamilyFallback,
_package = package,
assert(color == null || foreground == null, _kColorForegroundWarning),
assert(backgroundColor == null || background == null, _kColorBackgroundWarning);
可以看到其中有几个关键的属性,就拿一般定义字重用的 fontWeight
属性来说,查看源码会发现,Flutter 对于 FontWeight 的定义是 100-900 之间的整数值,就像下面这样。
/// Thin, the least thick.
static const FontWeight w100 = FontWeight._(0, 100);
/// Extra-light.
static const FontWeight w200 = FontWeight._(1, 200);
/// Light.
static const FontWeight w300 = FontWeight._(2, 300);
/// Normal / regular / plain.
static const FontWeight w400 = FontWeight._(3, 400);
/// Medium.
static const FontWeight w500 = FontWeight._(4, 500);
/// Semi-bold.
static const FontWeight w600 = FontWeight._(5, 600);
/// Bold.
static const FontWeight w700 = FontWeight._(6, 700);
/// Extra-bold.
static const FontWeight w800 = FontWeight._(7, 800);
/// Black, the most thick.
static const FontWeight w900 = FontWeight._(8, 900);
也就是说,你不能通过 FontWeight 取一个非整数的值,比如 325。对于普通的字体,基本都遵循这样的规范,比如 300 表示 Light 字重,而 700 表示 Bold 字重,但是对于可变字体来说,不同字重的映射关系可能并不是这样的整数,就拿 MiSans VF 来说,不同字重对应的 FontWeight 是这样的。
thin = FontVariation('wght', 150.0);
extraLight = FontVariation('wght', 200.0);
light = FontVariation('wght', 250.0);
normal = FontVariation('wght', 305.0);
regular = FontVariation('wght', 330.0);
medium = FontVariation('wght', 380.0);
demiBold = FontVariation('wght', 450.0);
semiBold = FontVariation('wght', 520.0);
bold = FontVariation('wght', 630.0);
heavy = FontVariation('wght', 700.0);
你可能注意到了这里的 FontVariation ,没错,这正是为可变字体准备的特征。
FontVariation
简单的说,可变字体引入了可变轴的概念,这也是为什么仅需要一个字体文件就可以代替多个字体的原因,目前常用的可变轴有五个,分别是字重,宽度,倾斜度,斜体和光学尺寸(MiSans 只支持字重一个可变轴)。
对于字重来说,对应的可变轴标签为 wght
,需要注意的是,wght
的值是可以自定义的,不同于传统 FontWeight 100-900 之间整数的表示形式,只要在有效范围内的 wght
值,都能直接反应到字体上,对于 MiSans ,wght
的有效范围是 150-700。
在定义好上面的 FontVariation 后,就可以在 TextStyle
中使用了,TextStyle
中的 fontVariation
是一个 List<FontVariation?>
,这样做的原因是,你可以同时定义多个可变轴。
Text(
'可变字体',
style: TextStyle(fontVariations: [VFFontWeight.thin], fontSize: 52),
),
Text(
'可变字体',
style: TextStyle(fontVariations: [VFFontWeight.extraLight], fontSize: 52),
),
Text(
'可变字体',
style: TextStyle(fontVariations: [VFFontWeight.light], fontSize: 52),
),
Text(
'可变字体',
style: TextStyle(fontVariations: [VFFontWeight.normal], fontSize: 52),
),
Text(
'可变字体',
style: TextStyle(fontVariations: [VFFontWeight.regular], fontSize: 52),
),
Text(
'可变字体',
style: TextStyle(fontVariations: [VFFontWeight.medium], fontSize: 52),
),
Text(
'可变字体',
style: TextStyle(fontVariations: [VFFontWeight.demiBold], fontSize: 52),
),
Text(
'可变字体',
style: TextStyle(fontVariations: [VFFontWeight.semiBold], fontSize: 52),
),
Text(
'可变字体',
style: TextStyle(fontVariations: [VFFontWeight.bold], fontSize: 52),
),
Text(
'可变字体',
style: TextStyle(fontVariations: [VFFontWeight.heavy], fontSize: 52),
),
上面的代码定义了 10 个标准字重的字体,如果成功运行,在设备上是这样的。
FontFeature
font-feature
并不是可变字体的专利,其他的字体如果遵循 Opentype 规范也可以使用。MiSans 包含了多种特征,具体的特征可以在官网查看,FontFeature 的使用和上面的 FontVariation 类似,参数类型为 List<FontFeature?>
,假如想启用等宽数字,可以使用 FontFeature.enable('thum')
。
有什么用
前面所提到的一些特征,好像传统字体也能做到,代价只是更大的体积而已,但其实可变字体有更多玩法可以使用。就拿字重来说,前面提到,可变字体的 wght
在有效范围内都是可以体现在字体上的,也就是说,你甚至可以将 wght
的值定义在一个动画曲线上,使得字体的字重可以动态改变,就想一个动画一样。这里我使用 Flutter 提供的 IntTween
将字重的范围 150-700 进行插值,同时使用了一个非线性的动画函数。
animationController =
AnimationController(vsync: this, duration: const Duration(seconds: 5), lowerBound: 0, upperBound: 1.0);
var value =
IntTween(begin: 150, end: 700).animate(CurvedAnimation(parent: animationController, curve: Curves.bounceInOut));
调用动画之后,会得到一个在 5 秒钟之内,从 150-700 变化的一个非线性的值,Curves.bounceInOut
曲线的变化看起来像这样。
将他应用到字体上会发生什么呢?
有趣。