在混编开发中,我们经常遇到要全局替换当前字体的需求,在Native开发中,我们通常会加载Asset或者下载的字体文件,那么在Flutter中,如何直接使用Native的字体文件呢?

毕竟大部分的字体文件都毕竟大,特别是一些字体还有加密策略,如果在Flutter中再创建一份字体文件,既浪费空间,而且也是一种重复代码,所以,我们需要在Flutter端,获取Native的字体文件。

在Flutter中,系统给我们提供了FontLoader,来动态加载字体,与前面的做法一样,我们创建一个Native接口,来获取Native传来的Byte数据流,并借助FontLoader来加载字体。

FontLoader加载字体数据

为了提高传输的效率,我们使用BasicMessageChannel来作为Channel的实现,这些在我们讲解Flutter与Native的通信机制中,都已经演示过了,我们直接拿来Google的Demo代码,修改下需要的内容,将FontLoader引入,代码如下所示。

  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'package:flutter/services.dart';
  4. class NativeFontApi {
  5. NativeFontApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger;
  6. final BinaryMessenger? _binaryMessenger;
  7. Future<void> loadNativeFont(String fontFamily) async {
  8. final BasicMessageChannel<ByteData> channel = BasicMessageChannel<ByteData>(
  9. 'dev.flutter.pigeon.NativeFontApi.loadNativeFont',
  10. const BinaryCodec(),
  11. binaryMessenger: _binaryMessenger,
  12. );
  13. try {
  14. final uInt8List = utf8.encoder.convert(fontFamily);
  15. final Future<ByteData> fontData = _loadFontFileByteData(uInt8List.buffer.asByteData(), channel);
  16. if (await fontData != ByteData(0)) {
  17. final FontLoader fontLoader = FontLoader(fontFamily);
  18. fontLoader.addFont(fontData);
  19. fontLoader.load();
  20. }
  21. } catch (e) {
  22. return;
  23. }
  24. }
  25. Future<ByteData> _loadFontFileByteData(ByteData data, BasicMessageChannel channel) async {
  26. final ByteData? fontData = await channel.send(data);
  27. if (fontData != null) {
  28. return fontData;
  29. } else {
  30. return ByteData(0);
  31. }
  32. }
  33. }

在加载好字体数据之后,我们在代码中就可以直接使用看,这和在配置文件中新增字体后的使用方式一样,直接指定fontFamily即可,代码如下所示。

  1. Text(
  2. model[index].bookName ?? "",
  3. style: const TextStyle(
  4. fontSize: 16,
  5. fontFamily: 'xxx_Medium_60',
  6. ),
  7. )

唯一需要注意的是,我们需要在程序启动时,初始化我们的字体文件,代码如下所示,通过loadNativeFont调用Channel来加载字体文件。

  1. NativeFontApi().loadNativeFont('xxx_Medium_60');

Native实现

我们仿照pigeon的实现方式,来创建自己的FontBridgeApi,之所以没通过pigeon直接生成,那是因为pigeon还不支持生成Byte数组的方式,所以我们只能自己来写,代码如下所示。

  1. // Autogenerated from Pigeon (v1.0.15), do not edit directly.
  2. // See also: https://pub.dev/packages/pigeon
  3. package com.qidian.QDReader.flutter;
  4. import java.nio.ByteBuffer;
  5. import io.flutter.plugin.common.BasicMessageChannel;
  6. import io.flutter.plugin.common.BinaryCodec;
  7. import io.flutter.plugin.common.BinaryMessenger;
  8. @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})
  9. public class FontBridgeApi {
  10. public interface NativeFontApi {
  11. ByteBuffer loadNativeFont(String familyName);
  12. static void setup(BinaryMessenger binaryMessenger, NativeFontApi api) {
  13. BasicMessageChannel<ByteBuffer> channel =
  14. new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NativeFontApi.loadNativeFont", BinaryCodec.INSTANCE);
  15. if (api != null) {
  16. channel.setMessageHandler((message, reply) -> {
  17. try {
  18. String param = new String(message != null ? message.array() : new byte[0]);
  19. ByteBuffer output = api.loadNativeFont(param);
  20. reply.reply(output);
  21. } catch (Error | RuntimeException ignored) {
  22. }
  23. });
  24. } else {
  25. channel.setMessageHandler(null);
  26. }
  27. }
  28. }
  29. }

下面就是实现接口,代码如下所示。

  1. private class NativeFontApiImp : FontBridgeApi.NativeFontApi {
  2. private val externalAppFontIndexes = intArrayOf(
  3. ETConverter.FONT_TYPE_INDEX_XXX_LIGHT,
  4. ETConverter.FONT_TYPE_INDEX_XXX_MEDIUM,
  5. ETConverter.FONT_TYPE_INDEX_XXXXX
  6. )
  7. override fun loadNativeFont(familyName: String?): ByteBuffer {
  8. val trueTypeFolder = File(ApplicationContext.getInstance().filesDir, ETConverter.FOLDER_TRUE_TYPE_FONTS)
  9. if (!trueTypeFolder.exists()) trueTypeFolder.mkdirs()
  10. val familyNameIndex = when (familyName) {
  11. "XXX_Light" -> 0
  12. "XXX_Medium_60" -> 1
  13. "XXXXSerif_Bold" -> 2
  14. else -> 0
  15. }
  16. val ttf = File(trueTypeFolder, ETConverter.getFontTypeName(externalAppFontIndexes[familyNameIndex]) + ETConverter.POSTFIX_NEW_TTF)
  17. val inputStream: InputStream = FileInputStream(ttf)
  18. val output = ByteArrayOutputStream()
  19. val buffer = ByteArray(4096)
  20. var n = 0
  21. while (true) {
  22. try {
  23. if (-1 == inputStream.read(buffer).also { n = it }) {
  24. inputStream.close()
  25. output.close()
  26. break
  27. }
  28. } catch (e: IOException) {
  29. e.printStackTrace()
  30. }
  31. output.write(buffer, 0, n)
  32. }
  33. return ByteBuffer.allocateDirect(output.toByteArray().size).put(output.toByteArray())
  34. }
  35. }

我们放到前面pigeon统一实现的地方进行初始化,代码如下。

  1. FontBridgeApi.NativeFontApi.setup(flutterEngine.dartExecutor, NativeFontApiImp())

优化

通过上面的方式,我们很轻松就实现了Flutter端加载Native的字体文件,但是在代码实现过程中,实际上有些地方是可以进行优化的,例如在Flutter中加载字体的异步方法中,我们可以构建一个枚举,根据不同的状态值,来修改代码的执行逻辑,例如增加:「加载中」、「加载失败」等状态,这样在程序异常的时候,可以判断是否需要跳过后面的加载流程、或者是重新执行加载流程,可以增加代码的鲁棒性。

  1. // 状态
  2. enum NativeFontLoadState { loading, failed, complete, notFound }
  3. // 返回枚举状态
  4. Future<NativeFontLoadState> loadFontIfNeeded(String fontFamily)

全局字体

在Flutter中,我们通常会根据自己项目的特点,封装一些Text组件,那么在这些组件中,就可以直接指定fontFamily,这样在业务开发时,就不需要重复指定fontFamily了,直接使用XXXText即可。

除了这种方式以外,还可以在APP的themeData中,直接指定fontFamily,代码如下:

  1. theme: ThemeData(
  2. fontFamily: xxxx,
  3. ),

这样可以为子组件提供默认的字体支持。如果在某些场景下需要修改默认字体,那么重新给Text设置不同的fontFamily覆盖即可。

向大家推荐下我的网站 https://xuyisheng.top/ 专注 Android-Kotlin-Flutter 欢迎大家访问

Flutter混编工程之Font桥接 - 图1