一、关于字体下载、缓存以及加载
这一块主要参考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)