什么是可变字体(Variable Font)

普通的字体可能有多个字重(比如 light、regular、bold)等,他们分别是一个个独立的静态的文件,在某些情况下,我们可能需要在应用中使用自定义的字体。可是,相对于西文字体来说,中文字体的体积相对偏高,单个字体文件的大小可能就有 15 MB 以上,并且在某些情况下,我们可能需要引入多个字重的字体,比如粗体和细体,无形中又增大了静态文件的大小。而可变字体,简单一点理解就是将多个字重的字体合并为一个字体。

可变字体的选择

虽然可变字体的好处众多,但可惜的是,对于中文字体来说,可选择的字体太少,到目前为止,较为优秀的中文可变字体只有 MiSans 的 VF 版本,以及 思源黑体 的 VF 版本,值得高兴的是,这两个字体均提供了免费的商用。接下来我以 MiSans 为例。

引入可变字体

首先在 MiSans 官网下载字体,下载出来后解压,其中有一个名为 MiSans VF.ttf 的文件,就是我们需要的可变字体。可以看到体积仅有 19 MB,如果还是觉得打包体积过大,可以采取应用下载后分发的方式加载。这样也可以减少打包的大小。

创建文件夹

在 Flutter 项目的根目录下创建一个 font 文件夹,注意不是 lib 目录下,而是和 lib 同级,之后将文件拷贝到目录下,完成后的项目结构看起来像这样。

引入字体文件

打开 pubspec.yamlflutter 块中加入如下代码。

fonts:
  - family: MiSans VF
    fonts:
      - asset: fonts/MiSans VF.ttf

修改后的文件看起来像这样。

这里通过静态方法进行引入,会将字体作为静态文件打包到安装包里,会带来一定的体积增加,如果想从网络分发字体,可以使用 FontLoader 加载,这里不展开讲。

引入后不要忘记运行一次 pub get

全局使用字体

找到应用入口的 MaterialApp() 块,通常在 main.dart 中,分别有两个属性 themedarkTheme ,用于控制浅色和深色模式的样式,需要对其修改,重新运行应用后,字体就被全局修改为了 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

如果想了解更多可变字体的特征,移步 Variable fonts guide

简单的说,可变字体引入了可变轴的概念,这也是为什么仅需要一个字体文件就可以代替多个字体的原因,目前常用的可变轴有五个,分别是字重,宽度,倾斜度,斜体和光学尺寸(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 曲线的变化看起来像这样。

将他应用到字体上会发生什么呢?

有趣。