动态下发字体技术方案

一、关于字体下载、缓存以及加载

这一块主要参考google_fonts代码,由于它里面有好几个地方都会校验是不是注册的Google字体,所以没办法直接使用。所以我这边是cooy一份出来,稍微做了一些修改。

主要流程

  • 从判断有没有加载到内存,如果有就直接返回。
  • 再从系统的字体注册文件文件中查找,如果有就load。
  • 再从本地文件查询,如果有也直接读取到内存,然后load
  • 最后是从网络层下载,下载后直接load,并加入到内存数字里面

二、关于字体方案

  • 直接使用整个字体包,包含各种不同粗细的字体 这种方案比较简单,缺点是字体包太大,用户等待时间比较久
  • 使用分包策略,把不同粗细的分别拆开单独是一个字体 优点是包比较小,但是需要自己制定规则,例如我们w100-w300使用一个字体,w400-w600使用一个字体,w700-w900使用一个字体

三、代码

AssetManifest

dart 复制代码
// Copyright 2019 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:convert' as convert;

import 'package:flutter/foundation.dart';
// TODO(andrewkolos): remove this after flutter adds its own AssetManifest API
// (see https://github.com/flutter/flutter/pull/119277) which will replace the
// one defined here.
// ignore: undefined_hidden_name
import 'package:flutter/services.dart' hide AssetManifest;

/// A class to obtain and memoize the app's asset manifest.
///
/// Used to check whether a font is provided as an asset.
class AssetManifest {
  AssetManifest({this.enableCache = true});

  static Future<Map<String, List<String>>?>? _jsonFuture;

  /// Whether the rootBundle should cache AssetManifest.json.
  ///
  /// Enabled by default. Should only be disabled during tests.
  final bool enableCache;

  Future<Map<String, List<String>>?>? json() {
    _jsonFuture ??= _loadAssetManifestJson();
    return _jsonFuture;
  }

  Future<Map<String, List<String>>?> _loadAssetManifestJson() async {
    try {
      final jsonString = await rootBundle.loadString(
        'AssetManifest.json',
        cache: enableCache,
      );
      return _manifestParser(jsonString);
    } catch (e) {
      rootBundle.evict('AssetManifest.json');
      rethrow;
    }
  }

  static Future<Map<String, List<String>>?> _manifestParser(String? jsonData) {
    if (jsonData == null) {
      return SynchronousFuture(null);
    }
    final parsedJson = convert.json.decode(jsonData) as Map<String, dynamic>;
    final parsedManifest = <String, List<String>>{
      for (final entry in parsedJson.entries)
        entry.key: (entry.value as List<dynamic>).cast<String>(),
    };
    return SynchronousFuture(parsedManifest);
  }

  @visibleForTesting
  static void reset() => _jsonFuture = null;
}

file_io_desktop_and_mobile.dart

dart 复制代码
import 'dart:io';
import 'dart:typed_data';

import 'package:path_provider/path_provider.dart';

bool get isMacOS => Platform.isMacOS;
bool get isAndroid => Platform.isAndroid;
bool get isTest => Platform.environment.containsKey('FLUTTER_TEST');

Future<void> saveFontToDeviceFileSystem({
  required String name,
  required List<int> bytes,
}) async {
  final file = await _localFile(name);
  await file.writeAsBytes(bytes);
}

Future<ByteData?> loadFontFromDeviceFileSystem({
  required String name,
}) async {
  try {
    final file = await _localFile(name);
    final fileExists = file.existsSync();
    if (fileExists) {
      List<int> contents = await file.readAsBytes();
      if (contents.isNotEmpty) {
        return ByteData.view(Uint8List.fromList(contents).buffer);
      }
    }
  } catch (e) {
    return null;
  }
  return null;
}

Future<String> get _localPath async {
  final directory = await getApplicationSupportDirectory();
  return directory.path;
}

Future<File> _localFile(String name) async {
  final path = await _localPath;
  // We expect only ttf files to be provided to us by the Google Fonts API.
  // That's why we can be sure a previously saved Google Font is in the ttf
  // format instead of, for example, otf.
  return File('$path/$name.ttf');
}

p_font_manager.dart

typescript 复制代码
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' hide AssetManifest;
import 'package:http/http.dart' as http;
import 'package:ppt_engine/widget/impl/internal/font/asset_manifest.dart';
import 'package:ppt_engine/widget/impl/internal/font/file_io_desktop_and_mobile.dart';

/// Used to determine whether to load a font or not.
final Set<String> _loadedFonts = {};

@visibleForTesting
void clearCache() => _loadedFonts.clear();

/// Set of [Future]s corresponding to fonts that are loading.
///
/// When a font is loading, a future is added to this set. When it is loaded in
/// the [FontLoader], that future is removed from this set.
final Set<Future<void>> pendingFontFutures = {};

@visibleForTesting
AssetManifest assetManifest = AssetManifest();
@visibleForTesting
http.Client httpClient = http.Client();

///通过fontFamily加载字体,并获取样式
TextStyle getFontTextStyle(String? url, {String? fontFamily, FontWeight? fontWeight, TextStyle? textStyle}) {
  textStyle ??= const TextStyle();
  if (url == null) return textStyle;
  fontFamily ??= url.substring(url.lastIndexOf("/") + 1, url.lastIndexOf("."));
  final loadingFuture = loadFontIfNecessary(url, fontFamily);
  pendingFontFutures.add(loadingFuture);
  loadingFuture.then((_) => pendingFontFutures.remove(loadingFuture));

  return textStyle.copyWith(
    fontFamily: fontFamily,
    fontFamilyFallback: [fontFamily],
  );
}
///通过appUrl 和FontWeight 确定字体
TextStyle getFontTextStyle2(Map<String,String>? appUrl, {String? fontFamily, @required FontWeight? fontWeight, TextStyle? textStyle}) {
  textStyle ??=  TextStyle(fontFamily: fontFamily,fontWeight: fontWeight);
  if (appUrl == null || appUrl.isEmpty) return textStyle;
  String? url = getUrlFormWeight(appUrl,fontWeight);
  if (url == null) return textStyle;
  fontFamily ??= url.substring(url.lastIndexOf("/") + 1, url.lastIndexOf("."));
  final loadingFuture = loadFontIfNecessary(url, fontFamily);
  pendingFontFutures.add(loadingFuture);
  loadingFuture.then((_) => pendingFontFutures.remove(loadingFuture));

  return textStyle.copyWith(
    fontFamily: fontFamily,
    fontFamilyFallback: [fontFamily],
  );
}

String? getUrlFormWeight(Map<String,String>? appUrl, FontWeight? fontWeight){
  if(fontWeight == null ) return null;

  switch(fontWeight){
    case FontWeight.w100:
    case FontWeight.w200:
    case FontWeight.w300:
      return appUrl!['light'];
    case FontWeight.w400://Regular
    case FontWeight.w500:
    case FontWeight.w600:
    return appUrl!['regular'];
    case FontWeight.w700:
    case FontWeight.w800:
    case FontWeight.w900:
      return appUrl!['bold'];
  }
  return null;
}

String? findUrl(List<String?> urls ,String name){
  for (var url in urls) {
    if(url == null  || url.isEmpty) return null;
    if(url.toLowerCase().contains(name)){
      return url;
    }
  }
  return null;
}



/// Loads a font into the [FontLoader] with [googleFontsFamilyName] for the
/// matching [expectedFileHash].
///
/// If a font with the [fontName] has already been loaded into memory, then
/// this method does nothing as there is no need to load it a second time.
///
/// Otherwise, this method will first check to see if the font is available
/// as an asset, then on the device file system. If it isn't, it is fetched via
/// the [fontUrl] and stored on device. In all cases, the returned future
/// completes once the font is loaded into the [FontLoader].
Future<void> loadFontIfNecessary(String url, String fontFamily) async {
  // If this font has already already loaded or is loading, then there is no
  // need to attempt to load it again, unless the attempted load results in an
  // error.
  if (_loadedFonts.contains(fontFamily)) {
    return;
  } else {
    _loadedFonts.add(fontFamily);
  }

  try {
    Future<ByteData?>? byteData;

    // Check if this font can be loaded by the pre-bundled assets.
    final assetManifestJson = await assetManifest.json();
    final assetPath = _findFamilyWithVariantAssetPath(
      fontFamily,
      assetManifestJson,
    );
    if (assetPath != null) {
      byteData = rootBundle.load(assetPath);
    }
    if (await byteData != null) {
      return loadFontByteData(fontFamily, byteData);
    }

    // Check if this font can be loaded from the device file system.
    byteData = loadFontFromDeviceFileSystem(
      name: fontFamily,
    );

    if (await byteData != null) {
      return loadFontByteData(fontFamily, byteData);
    }

    // Attempt to load this font via http, unless disallowed.
    byteData = _httpFetchFontAndSaveToDevice(
      fontFamily,
      url,
    );
    if (await byteData != null) {
      return loadFontByteData(fontFamily, byteData);
    }
  } catch (e) {
    _loadedFonts.remove(fontFamily);
    // print('Error: google_fonts was unable to load font $fontName because the '
    //     'following exception occurred:\n$e');
    // if (file_io.isTest) {
    //   print('\nThere is likely something wrong with your test. Please see '
    //       'https://github.com/material-foundation/flutter-packages/blob/main/packages/google_fonts/example/test '
    //       'for examples of how to test with google_fonts.');
    // } else if (file_io.isMacOS || file_io.isAndroid) {
    //   print(
    //     '\nSee https://docs.flutter.dev/development/data-and-backend/networking#platform-notes.',
    //   );
    // }
    print('If troubleshooting doesn\'t solve the problem, please file an issue '
        'at https://github.com/material-foundation/flutter-packages/issues/new/choose.\n');
    rethrow;
  }
}

/// Looks for a matching [family] font, provided the asset manifest.
/// Returns the path of the font asset if found, otherwise an empty string.
String? _findFamilyWithVariantAssetPath(
  String family,
  Map<String, List<String>>? manifestJson,
) {
  if (manifestJson == null) return null;

  for (final assetList in manifestJson.values) {
    for (final String asset in assetList) {
      for (final matchingSuffix in ['.ttf', '.otf'].where(asset.endsWith)) {
        final assetWithoutExtension = asset.substring(asset.lastIndexOf("/") + 1, asset.length - matchingSuffix.length);
        if (assetWithoutExtension.endsWith(family)) {
          return asset;
        }
      }
    }
  }

  return null;
}

/// Loads a font with [FontLoader], given its name and byte-representation.
@visibleForTesting
Future<void> loadFontByteData(
  String familyWithVariantString,
  Future<ByteData?>? byteData,
) async {
  if (byteData == null) return;
  final fontData = await byteData;
  if (fontData == null) return;

  final fontLoader = FontLoader(familyWithVariantString);
  fontLoader.addFont(Future.value(fontData));
  await fontLoader.load();
}

/// Fetches a font with [fontName] from the [fontUrl] and saves it locally if
/// it is the first time it is being loaded.
///
/// This function can return `null` if the font fails to load from the URL.
Future<ByteData> _httpFetchFontAndSaveToDevice(String fontName, String url) async {
  final uri = Uri.tryParse(url);
  if (uri == null) {
    throw Exception('Invalid fontUrl: $url');
  }

  http.Response response;
  try {
    response = await httpClient.get(uri);
  } catch (e) {
    throw Exception('Failed to load font with url $url: $e');
  }
  if (response.statusCode == 200) {
    _unawaited(saveFontToDeviceFileSystem(
      name: fontName,
      bytes: response.bodyBytes,
    ));

    return ByteData.view(response.bodyBytes.buffer);
  } else {
    // If that call was not successful, throw an error.
    throw Exception('Failed to load font with url: $url');
  }
}

void _unawaited(Future<void> future) {}
  • 如果是整包方案下载可以直接使用getFontTextStyle方法 getFontTextStyle2(appUrl)
  • 如果是拆分包的方式下载可以参考getFontTextStyle2方法getFontTextStyle2(appUrl,fontWeight: titleTextWeight)
相关推荐
2601_949575861 小时前
JSON5 跨端三方库仓颉适配全流程
前端·javascript·flutter
法的空间1 小时前
AI 编程时代,为什么 Dart 分析会拖慢大型项目的开发
android·flutter·ios
恋猫de小郭3 小时前
Flutter 紧急发布 3.47.8 安全升级版本,建议都升级
android·前端·flutter
好的佩奇19 小时前
Dart 之模式
android·flutter·dart
淡淡的香烟21 小时前
Flutter实现多语言切换
android·flutter·1024程序员节
事圆则缓1 天前
从零搭建一套 Flutter 架构:先跑通数据流
flutter
程序员老刘2 天前
Flutter版本选择指南:3.44稳定了11周正式转正 | 2026年10月
flutter·ai编程·客户端
chicken_h2 天前
【无标题】
flutter·打包
事圆则缓2 天前
Flutter 底层渲染引擎怎么变了:从 Skia 到 Impeller,以及迁移时的坑
flutter
码林鼠3 天前
flutter状态管理入门教程-Riverpod
开发语言·javascript·flutter