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

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

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

FontLoader加载字体数据

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

import 'dart:async';
import 'dart:convert';

import 'package:flutter/services.dart';

class NativeFontApi {
NativeFontApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger;

final BinaryMessenger? _binaryMessenger;

Future<void> loadNativeFont(String fontFamily) async {
final BasicMessageChannel<ByteData> channel = BasicMessageChannel<ByteData>(
'dev.flutter.pigeon.NativeFontApi.loadNativeFont',
const BinaryCodec(),
binaryMessenger: _binaryMessenger,
);
try {
final uInt8List = utf8.encoder.convert(fontFamily);
final Future<ByteData> fontData = _loadFontFileByteData(uInt8List.buffer.asByteData(), channel);
if (await fontData != ByteData(0)) {
final FontLoader fontLoader = FontLoader(fontFamily);
fontLoader.addFont(fontData);
fontLoader.load();
}
} catch (e) {
return;
}
}

Future<ByteData> _loadFontFileByteData(ByteData data, BasicMessageChannel channel) async {
final ByteData? fontData = await channel.send(data);
if (fontData != null) {
return fontData;
} else {
return ByteData(0);
}
}
}

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

Text(
model[index].bookName ?? "",
style: const TextStyle(
fontSize: 16,
fontFamily: 'xxx_Medium_60',
),
)

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

NativeFontApi().loadNativeFont('xxx_Medium_60');

Native实现

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

// Autogenerated from Pigeon (v1.0.15), do not edit directly.
// See also: https://pub.dev/packages/pigeon

import java.nio.ByteBuffer;

import io.flutter.plugin.common.BasicMessageChannel;
import io.flutter.plugin.common.BinaryCodec;
import io.flutter.plugin.common.BinaryMessenger;

@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})
public class FontBridgeApi {

public interface NativeFontApi {
ByteBuffer loadNativeFont(String familyName);

static void setup(BinaryMessenger binaryMessenger, NativeFontApi api) {
BasicMessageChannel<ByteBuffer> channel =
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NativeFontApi.loadNativeFont", BinaryCodec.INSTANCE);
if (api != null) {
channel.setMessageHandler((message, reply) -> {
try {
String param = new String(message != null ? message.array() : new byte[0]);
ByteBuffer output = api.loadNativeFont(param);
reply.reply(output);
} catch (Error | RuntimeException ignored) {
}
});
} else {
channel.setMessageHandler(null);
}
}
}
}

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

private class NativeFontApiImp : FontBridgeApi.NativeFontApi {
private val externalAppFontIndexes = intArrayOf(
ETConverter.FONT_TYPE_INDEX_XXX_LIGHT,
ETConverter.FONT_TYPE_INDEX_XXX_MEDIUM,
ETConverter.FONT_TYPE_INDEX_XXXXX
)

override fun loadNativeFont(familyName: String?): ByteBuffer {
val trueTypeFolder = File(ApplicationContext.getInstance().filesDir, ETConverter.FOLDER_TRUE_TYPE_FONTS)
if (!trueTypeFolder.exists()) trueTypeFolder.mkdirs()
val familyNameIndex = when (familyName) {
"XXX_Light" -> 0
"XXX_Medium_60" -> 1
"XXXXSerif_Bold" -> 2
else -> 0
}
val ttf = File(trueTypeFolder, ETConverter.getFontTypeName(externalAppFontIndexes[familyNameIndex]) + ETConverter.POSTFIX_NEW_TTF)
val inputStream: InputStream = FileInputStream(ttf)
val output = ByteArrayOutputStream()
val buffer = ByteArray(4096)
var n = 0
while (true) {
try {
if (-1 == inputStream.read(buffer).also { n = it }) {
inputStream.close()
output.close()
break
}
} catch (e: IOException) {
e.printStackTrace()
}
output.write(buffer, 0, n)
}
return ByteBuffer.allocateDirect(output.toByteArray().size).put(output.toByteArray())
}
}

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

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

优化

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

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

全局字体

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

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

theme: ThemeData(
fontFamily: xxxx,
),

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

< END >

作者:徐宜生

更文不易,点个“三连”支持一下????