动态下发字体技术方案

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

这一块主要参考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)
相关推荐
AiFlutter1 分钟前
Flutter Web部署到子路径的打包指令
flutter
有趣的杰克6 分钟前
Flutter InkWell组件去掉灰色遮罩
开发语言·javascript·flutter
Python私教7 分钟前
Flutter动画容器
flutter
wills77713 小时前
Flutter 状态管理框架Get
flutter·react native
Rudy102116 小时前
分享我在flutter中使用的MVVM框架 - 2
前端·flutter
恋猫de小郭1 天前
什么?Flutter 又要凉了? Flock 是什么东西?
flutter
lqj_本人1 天前
<大厂实战场景> ~ Flutter&鸿蒙next 解析后端返回的 HTML 数据详解
flutter·华为·架构·harmonyos·1024程序员节
MavenTalk1 天前
前端跨平台开发常见的解决方案
前端·flutter·react native·reactjs·weex·大前端
WANKUN2 天前
Flutter踩坑 Packages get 失效
flutter