动态下发字体技术方案

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

这一块主要参考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)
相关推荐
火柴就是我1 小时前
flutter 之真手势冲突处理
android·flutter
Speed1231 小时前
`mockito` 的核心“打桩”规则
flutter·dart
法的空间1 小时前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
恋猫de小郭1 小时前
Android 将强制应用使用主题图标,你怎么看?
android·前端·flutter
玲珑Felone2 小时前
从flutter源码看其渲染机制
android·flutter
ALLIN1 天前
Flutter 三种方式实现页面切换后保持原页面状态
flutter
Dabei1 天前
Flutter 国际化
flutter
Dabei1 天前
Flutter MQTT 通信文档
flutter
Dabei1 天前
Flutter 中实现 TCP 通信
flutter
孤鸿玉1 天前
ios flutter_echarts 不在当前屏幕 白屏修复
flutter