Add support for shared fonts in packages. (#12142)

This commit is contained in:
Sarah Zakarias 2017-09-19 14:04:51 +02:00 committed by GitHub
parent 2859a563ce
commit ec2d1c9116
8 changed files with 491 additions and 31 deletions

View file

@ -122,6 +122,76 @@ import 'basic_types.dart';
/// )
/// ```
///
/// ### Custom Fonts
///
/// Custom fonts can be declared in the `pubspec.yaml` file as shown below:
///
///```yaml
/// flutter:
/// fonts:
/// - family: Raleway
/// fonts:
/// - asset: fonts/Raleway-Regular.ttf
/// - asset: fonts/Raleway-Medium.ttf
/// weight: 500
/// - asset: assets/fonts/Raleway-SemiBold.ttf
/// weight: 600
/// - family: Schyler
/// fonts:
/// - asset: fonts/Schyler-Regular.ttf
/// - asset: fonts/Schyler-Italic.ttf
/// style: italic
///```
///
/// The `family` property determines the name of the font, which you can use in
/// the [fontFamily] argument. The `asset` property is a path to the font file,
/// relative to the [pubspec.yaml] file. The `weight` property specifies the
/// weight of the glyph outlines in the file as an integer multiple of 100
/// between 100 and 900. This corresponds to the [FontWeight] class and can be
/// used in the [fontWeight] argument. The `style` property specfies whether the
/// outlines in the file are `italic` or `normal`. These values correspond to
/// the [FontStyle] class and can be used in the [fontStyle] argument.
///
/// To select a custom font, create [TextStyle] using the [fontFamily]
/// argument as shown in the example below:
///
/// ```dart
/// const TextStyle(fontFamily: 'Raleway')
/// ```
///
/// To use a font family defined in a package, the [package] argument must be
/// provided. For instance, suppose the font declaration above is in the
/// `pubspec.yaml` of a package named `my_package` which the app depends on.
/// Then creating the TextStyle is done as follows:
///
/// ```dart
/// const TextStyle(fontFamily: 'Raleway', package: 'my_package')
/// ```
///
/// This is also how the package itself should create the style.
///
/// A package can also provide font files in its `lib/` folder which will not
/// automatically be included in the app. Instead the app can use these
/// selectively when declaring a font. Suppose a package named `my_package` has:
///
/// ```
/// lib/fonts/Raleway-Medium.ttf
/// ```
///
/// Then the app can declare a font like in the example below:
///
///```yaml
/// flutter:
/// fonts:
/// - family: Raleway
/// fonts:
/// - asset: assets/fonts/Raleway-Regular.ttf
/// - asset: packages/my_package/fonts/Raleway-Medium.ttf
/// weight: 500
///```
///
/// The `lib/` is implied, so it should not be included in the asset path.
///
/// See also:
///
/// * [Text], the widget for showing text in a single style.
@ -136,7 +206,7 @@ class TextStyle extends Diagnosticable {
const TextStyle({
this.inherit: true,
this.color,
this.fontFamily,
String fontFamily,
this.fontSize,
this.fontWeight,
this.fontStyle,
@ -147,7 +217,10 @@ class TextStyle extends Diagnosticable {
this.decoration,
this.decorationColor,
this.decorationStyle,
}) : assert(inherit != null);
this.package,
}) : fontFamily = package == null ? fontFamily : 'packages/$package/$fontFamily',
assert(inherit != null);
/// Whether null values are replaced with their value in an ancestor text
/// style (e.g., in a [TextSpan] tree).
@ -163,6 +236,10 @@ class TextStyle extends Diagnosticable {
/// The name of the font to use when painting the text (e.g., Roboto).
final String fontFamily;
/// The name of the package from which the font is included. See the
/// documentation for the [TextStyle] class itself for details.
final String package;
/// The size of glyphs (in logical pixels) to use when painting the text.
///
/// During painting, the [fontSize] is multiplied by the current

View file

@ -627,12 +627,12 @@ class MemoryImage extends ImageProvider<MemoryImage> {
/// Assets used by the package itself should also be fetched using the [package]
/// argument as above.
///
/// If the desired asset is specified in the [pubspec.yaml] of the package, it
/// If the desired asset is specified in the `pubspec.yaml` of the package, it
/// is bundled automatically with the app. In particular, assets used by the
/// package itself must be specified in its [pubspec.yaml].
/// package itself must be specified in its `pubspec.yaml`.
///
/// A package can also choose to have assets in its 'lib/' folder that are not
/// specified in its [pubspec.yaml]. In this case for those images to be
/// specified in its `pubspec.yaml`. In this case for those images to be
/// bundled, the app has to specify which ones to include. For instance a
/// package named `fancy_backgrounds` could have:
///
@ -642,7 +642,7 @@ class MemoryImage extends ImageProvider<MemoryImage> {
/// lib/backgrounds/background3.png
///```
///
/// To include, say the first image, the [pubspec.yaml] of the app should specify
/// To include, say the first image, the `pubspec.yaml` of the app should specify
/// it in the `assets` section:
///
/// ```yaml

View file

@ -60,7 +60,7 @@ const String _kAssetManifestFileName = 'AssetManifest.json';
///
/// When fetching an image provided by the app itself, use the [assetName]
/// argument to name the asset to choose. For instance, consider the structure
/// above. First, the [pubspec.yaml] of the project should specify its assets in
/// above. First, the `pubspec.yaml` of the project should specify its assets in
/// the `flutter` section:
///
/// ```yaml
@ -87,12 +87,12 @@ const String _kAssetManifestFileName = 'AssetManifest.json';
/// Assets used by the package itself should also be fetched using the [package]
/// argument as above.
///
/// If the desired asset is specified in the [pubspec.yaml] of the package, it
/// If the desired asset is specified in the `pubspec.yaml` of the package, it
/// is bundled automatically with the app. In particular, assets used by the
/// package itself must be specified in its [pubspec.yaml].
/// package itself must be specified in its `pubspec.yaml`.
///
/// A package can also choose to have assets in its 'lib/' folder that are not
/// specified in its [pubspec.yaml]. In this case for those images to be
/// specified in its `pubspec.yaml`. In this case for those images to be
/// bundled, the app has to specify which ones to include. For instance a
/// package named `fancy_backgrounds` could have:
///
@ -102,7 +102,7 @@ const String _kAssetManifestFileName = 'AssetManifest.json';
/// lib/backgrounds/background3.png
///```
///
/// To include, say the first image, the [pubspec.yaml] of the app should specify
/// To include, say the first image, the `pubspec.yaml` of the app should specify
/// it in the `assets` section:
///
/// ```yaml

View file

@ -233,12 +233,12 @@ class Image extends StatefulWidget {
/// Assets used by the package itself should also be displayed using the
/// [package] argument as above.
///
/// If the desired asset is specified in the [pubspec.yaml] of the package, it
/// If the desired asset is specified in the `pubspec.yaml` of the package, it
/// is bundled automatically with the app. In particular, assets used by the
/// package itself must be specified in its [pubspec.yaml].
/// package itself must be specified in its `pubspec.yaml`.
///
/// A package can also choose to have assets in its 'lib/' folder that are not
/// specified in its [pubspec.yaml]. In this case for those images to be
/// specified in its `pubspec.yaml`. In this case for those images to be
/// bundled, the app has to specify which ones to include. For instance a
/// package named `fancy_backgrounds` could have:
///
@ -248,7 +248,7 @@ class Image extends StatefulWidget {
/// lib/backgrounds/background3.png
///```
///
/// To include, say the first image, the [pubspec.yaml] of the app should
/// To include, say the first image, the `pubspec.yaml` of the app should
/// specify it in the assets section:
///
/// ```yaml

View file

@ -121,7 +121,9 @@ void main() {
final ui.ParagraphStyle ps5 = s5.getParagraphStyle();
expect(ps5, equals(new ui.ParagraphStyle(fontWeight: FontWeight.w700, fontSize: 12.0, lineHeight: 123.0)));
expect(ps5.toString(), 'ParagraphStyle(textAlign: unspecified, textDirection: unspecified, fontWeight: FontWeight.w700, fontStyle: unspecified, maxLines: unspecified, fontFamily: unspecified, fontSize: 12.0, lineHeight: 123.0x, ellipsis: unspecified)');
});
test('TextStyle with text direction', () {
final ui.ParagraphStyle ps6 = const TextStyle().getParagraphStyle(textDirection: TextDirection.ltr);
expect(ps6, equals(new ui.ParagraphStyle(textDirection: TextDirection.ltr)));
expect(ps6.toString(), 'ParagraphStyle(textAlign: unspecified, textDirection: TextDirection.ltr, fontWeight: unspecified, fontStyle: unspecified, maxLines: unspecified, fontFamily: unspecified, fontSize: unspecified, lineHeight: unspecified, ellipsis: unspecified)');
@ -130,4 +132,16 @@ void main() {
expect(ps7, equals(new ui.ParagraphStyle(textDirection: TextDirection.rtl)));
expect(ps7.toString(), 'ParagraphStyle(textAlign: unspecified, textDirection: TextDirection.rtl, fontWeight: unspecified, fontStyle: unspecified, maxLines: unspecified, fontFamily: unspecified, fontSize: unspecified, lineHeight: unspecified, ellipsis: unspecified)');
});
test('TextStyle using package font', () {
final TextStyle s6 = const TextStyle(fontFamily: 'test');
expect(s6.fontFamily, 'test');
expect(s6.package, isNull);
expect(s6.getTextStyle().toString(), 'TextStyle(color: unspecified, decoration: unspecified, decorationColor: unspecified, decorationStyle: unspecified, fontWeight: unspecified, fontStyle: unspecified, textBaseline: unspecified, fontFamily: test, fontSize: unspecified, letterSpacing: unspecified, wordSpacing: unspecified, height: unspecified)');
final TextStyle s7 = const TextStyle(fontFamily: 'test', package: 'p');
expect(s7.fontFamily, 'packages/p/test');
expect(s7.package, 'p');
expect(s7.getTextStyle().toString(), 'TextStyle(color: unspecified, decoration: unspecified, decorationColor: unspecified, decorationStyle: unspecified, fontWeight: unspecified, fontStyle: unspecified, textBaseline: unspecified, fontFamily: packages/p/test, fontSize: unspecified, letterSpacing: unspecified, wordSpacing: unspecified, height: unspecified)');
});
}

View file

@ -118,6 +118,7 @@ class AssetBundle {
manifestDescriptor['uses-material-design'];
// Add assets from packages.
final List<Map<String, dynamic>> fonts = <Map<String, dynamic>>[];
for (String packageName in packageMap.map.keys) {
final Uri package = packageMap.map[packageName];
if (package != null && package.scheme == 'file') {
@ -127,18 +128,30 @@ class AssetBundle {
continue;
final int result = await _validateFlutterManifest(packageManifest);
if (result == 0) {
final Map<String, dynamic> packageManifestDescriptor = packageManifest;
Map<String, dynamic> packageManifestDescriptor = packageManifest;
// Skip the app itself.
if (packageManifestDescriptor['name'] == appName)
continue;
if (packageManifestDescriptor.containsKey('flutter')) {
packageManifestDescriptor = packageManifestDescriptor['flutter'];
final String packageBasePath = fs.path.dirname(packageManifestPath);
assetVariants.addAll(_parseAssets(
packageMap,
packageManifestDescriptor['flutter'],
packageManifestDescriptor,
packageBasePath,
packageName: packageName,
));
final bool packageUsesMaterialDesign =
packageManifestDescriptor.containsKey('uses-material-design') &&
packageManifestDescriptor['uses-material-design'];
fonts.addAll(_parseFonts(
packageManifestDescriptor,
packageUsesMaterialDesign,
includeDefaultFonts,
packageMap,
packageName: packageName,
));
}
}
}
@ -179,10 +192,15 @@ class AssetBundle {
entries[_kAssetManifestJson] = _createAssetManifest(assetVariants);
final DevFSContent fontManifest =
_createFontManifest(manifestDescriptor, usesMaterialDesign, includeDefaultFonts);
if (fontManifest != null)
entries[_kFontManifestJson] = fontManifest;
fonts.addAll(_parseFonts(
manifestDescriptor,
usesMaterialDesign,
includeDefaultFonts,
packageMap,
));
if (fonts.isNotEmpty)
entries[_kFontManifestJson] = new DevFSStringContent(JSON.encode(fonts));
// TODO(ianh): Only do the following line if we've changed packages or if our LICENSE file changed
entries[_kLICENSE] = await _obtainLicenses(packageMap, assetBasePath, reportPackages: reportLicensedPackages);
@ -367,18 +385,73 @@ DevFSContent _createAssetManifest(Map<_Asset, List<_Asset>> assetVariants) {
return new DevFSStringContent(JSON.encode(json));
}
DevFSContent _createFontManifest(Map<String, dynamic> manifestDescriptor,
bool usesMaterialDesign,
bool includeDefaultFonts) {
List<Map<String, dynamic>> _parseFonts(
Map<String, dynamic> manifestDescriptor,
bool usesMaterialDesign,
bool includeDefaultFonts,
PackageMap packageMap, {
String packageName
}) {
final List<Map<String, dynamic>> fonts = <Map<String, dynamic>>[];
if (usesMaterialDesign && includeDefaultFonts) {
fonts.addAll(_getMaterialFonts(AssetBundle._kFontSetMaterial));
}
if (manifestDescriptor != null && manifestDescriptor.containsKey('fonts'))
fonts.addAll(manifestDescriptor['fonts']);
if (fonts.isEmpty)
return null;
return new DevFSStringContent(JSON.encode(fonts));
if (packageName == null) {
fonts.addAll(manifestDescriptor['fonts']);
} else {
fonts.addAll(_parsePackageFonts(
manifestDescriptor['fonts'],
packageName,
packageMap,
));
}
return fonts;
}
/// Prefixes family names and asset paths of fonts included from packages with
/// 'packages/<package_name>
///
/// (e.g., replace {"fonts":[{"asset":"a/bar"}],"family":"bar"} by
/// {"fonts":[{"asset":"packages/foo/a/bar"}],"family":"packages/foo/bar"})
List<Map<String, dynamic>> _parsePackageFonts(
List<Map<String, dynamic>> manifestDescriptor,
String packageName,
PackageMap packageMap,
) {
final List<Map<String, dynamic>> parsedFonts = <Map<String, dynamic>>[];
for (Map<String, dynamic> fontFamily in manifestDescriptor) {
final Map<String, dynamic> parsedFontFamily = <String, dynamic>{};
for (String key in fontFamily.keys) {
if (key == 'family') {
parsedFontFamily[key] = 'packages/$packageName/${fontFamily[key]}';
} else {
assert(key == 'fonts');
final List<Map<String, dynamic>> parsedAssets = <Map<String, dynamic>>[];
for (Map<String, dynamic> assetProperties in fontFamily[key]) {
final Map<String, dynamic> parsedAssetProperties = <String, dynamic>{};
for (String property in assetProperties.keys) {
if (property == 'asset') {
final String assetPath = assetProperties[property];
if (assetPath.startsWith('packages') &&
!fs.isFileSync(packageMap.map[packageName].resolve('../$assetPath').path))
parsedAssetProperties[property] = assetProperties[property];
else
parsedAssetProperties[property] = 'packages/$packageName/${assetProperties[property]}';
} else {
assert(property == 'style' || property == 'weight');
parsedAssetProperties[property] = assetProperties[property];
}
}
parsedAssets.add(parsedAssetProperties);
}
parsedFontFamily[key] = parsedAssets;
}
}
parsedFonts.add(parsedFontFamily);
}
return parsedFonts;
}
// Given an assets directory like this:

View file

@ -0,0 +1,296 @@
// Copyright 2017 The Chromium Authors. 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:async';
import 'dart:convert';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/asset.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:test/test.dart';
import 'src/common.dart';
import 'src/context.dart';
void main() {
void writePubspecFile(String path, String name, {String fontsSection}) {
if (fontsSection == null) {
fontsSection = '';
} else {
fontsSection = '''
flutter:
fonts:
$fontsSection
''';
}
fs.file(path)
..createSync(recursive: true)
..writeAsStringSync('''
name: $name
dependencies:
flutter:
sdk: flutter
$fontsSection
''');
}
void establishFlutterRoot() {
// Setting flutterRoot here so that it picks up the MemoryFileSystem's
// path separator.
Cache.flutterRoot = getFlutterRoot();
}
void writePackagesFile(String packages) {
fs.file(".packages")
..createSync()
..writeAsStringSync(packages);
}
Future<Null> buildAndVerifyFonts(
List<String> localFonts,
List<String> packageFonts,
List<String> packages,
String expectedAssetManifest,
) async {
final AssetBundle bundle = new AssetBundle();
await bundle.build(manifestPath: 'pubspec.yaml');
for (String packageName in packages) {
for (String packageFont in packageFonts) {
final String entryKey = 'packages/$packageName/$packageFont';
expect(bundle.entries.containsKey(entryKey), true);
expect(
UTF8.decode(await bundle.entries[entryKey].contentsAsBytes()),
packageFont,
);
}
for (String localFont in localFonts) {
expect(bundle.entries.containsKey(localFont), true);
expect(
UTF8.decode(await bundle.entries[localFont].contentsAsBytes()),
localFont,
);
}
}
expect(
UTF8.decode(await bundle.entries['FontManifest.json'].contentsAsBytes()),
expectedAssetManifest,
);
}
void writeFontAsset(String path, String font) {
fs.file('$path$font')
..createSync(recursive: true)
..writeAsStringSync(font);
}
group('AssetBundle fonts from packages', () {
testUsingContext('App includes neither font manifest nor fonts when no defines fonts', () async {
establishFlutterRoot();
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
writePubspecFile('p/p/pubspec.yaml', 'test_package');
final AssetBundle bundle = new AssetBundle();
await bundle.build(manifestPath: 'pubspec.yaml');
expect(bundle.entries.length, 2); // LICENSE, AssetManifest
expect(bundle.entries.containsKey('FontManifest.json'), false);
}, overrides: contextOverrides);
testUsingContext('App font uses font file from package', () async {
establishFlutterRoot();
final String fontsSection = '''
- family: foo
fonts:
- asset: packages/test_package/bar
''';
writePubspecFile('pubspec.yaml', 'test', fontsSection: fontsSection);
writePackagesFile('test_package:p/p/lib/');
writePubspecFile('p/p/pubspec.yaml', 'test_package');
final String font = 'bar';
writeFontAsset('p/p/lib/', font);
final String expectedFontManifest =
'[{"fonts":[{"asset":"packages/test_package/bar"}],"family":"foo"}]';
await buildAndVerifyFonts(
<String>[],
<String>[font],
<String>['test_package'],
expectedFontManifest,
);
}, overrides: contextOverrides);
testUsingContext('App font uses local font file and package font file', () async {
establishFlutterRoot();
final String fontsSection = '''
- family: foo
fonts:
- asset: packages/test_package/bar
- asset: a/bar
''';
writePubspecFile('pubspec.yaml', 'test', fontsSection: fontsSection);
writePackagesFile('test_package:p/p/lib/');
writePubspecFile('p/p/pubspec.yaml', 'test_package');
final String packageFont = 'bar';
writeFontAsset('p/p/lib/', packageFont);
final String localFont = 'a/bar';
writeFontAsset('', localFont);
final String expectedFontManifest =
'[{"fonts":[{"asset":"packages/test_package/bar"},{"asset":"a/bar"}],'
'"family":"foo"}]';
await buildAndVerifyFonts(
<String>[localFont],
<String>[packageFont],
<String>['test_package'],
expectedFontManifest,
);
}, overrides: contextOverrides);
testUsingContext('App uses package font with own font file', () async {
establishFlutterRoot();
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final String fontsSection = '''
- family: foo
fonts:
- asset: a/bar
''';
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
fontsSection: fontsSection,
);
final String font = 'a/bar';
writeFontAsset('p/p/', font);
final String expectedFontManifest =
'[{"fonts":[{"asset":"packages/test_package/a/bar"}],'
'"family":"packages/test_package/foo"}]';
await buildAndVerifyFonts(
<String>[],
<String>[font],
<String>['test_package'],
expectedFontManifest,
);
}, overrides: contextOverrides);
testUsingContext('App uses package font with font file from another package', () async {
establishFlutterRoot();
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/\ntest_package2:p2/p/lib/');
final String fontsSection = '''
- family: foo
fonts:
- asset: packages/test_package2/bar
''';
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
fontsSection: fontsSection,
);
writePubspecFile('p2/p/pubspec.yaml', 'test_package2');
final String font = 'bar';
writeFontAsset('p2/p/lib/', font);
final String expectedFontManifest =
'[{"fonts":[{"asset":"packages/test_package2/bar"}],'
'"family":"packages/test_package/foo"}]';
await buildAndVerifyFonts(
<String>[],
<String>[font],
<String>['test_package2'],
expectedFontManifest,
);
}, overrides: contextOverrides);
testUsingContext('App uses package font with properties and own font file', () async {
establishFlutterRoot();
writePubspecFile('pubspec.yaml', 'test');
writePackagesFile('test_package:p/p/lib/');
final String pubspec = '''
- family: foo
fonts:
- style: italic
weight: 400
asset: a/bar
''';
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
fontsSection: pubspec,
);
final String font = 'a/bar';
writeFontAsset('p/p/', font);
final String expectedFontManifest =
'[{"fonts":[{"weight":400,"style":"italic","asset":"packages/test_package/a/bar"}],'
'"family":"packages/test_package/foo"}]';
await buildAndVerifyFonts(
<String>[],
<String>[font],
<String>['test_package'],
expectedFontManifest,
);
}, overrides: contextOverrides);
testUsingContext('App uses local font and package font with own font file.', () async {
establishFlutterRoot();
final String fontsSection = '''
- family: foo
fonts:
- asset: a/bar
''';
writePubspecFile(
'pubspec.yaml',
'test',
fontsSection: fontsSection,
);
writePackagesFile('test_package:p/p/lib/');
writePubspecFile(
'p/p/pubspec.yaml',
'test_package',
fontsSection: fontsSection,
);
final String font = 'a/bar';
writeFontAsset('', font);
writeFontAsset('p/p/', font);
final String expectedFontManifest =
'[{"fonts":[{"asset":"packages/test_package/a/bar"}],'
'"family":"packages/test_package/foo"},'
'{"fonts":[{"asset":"a/bar"}],"family":"foo"}]';
await buildAndVerifyFonts(
<String>[font],
<String>[font],
<String>['test_package'],
expectedFontManifest,
);
}, overrides: contextOverrides);
});
}
Map<Type, Generator> get contextOverrides {
return <Type, Generator>{FileSystem: () => new MemoryFileSystem()};
}

View file

@ -396,6 +396,6 @@ $assetsSection
});
}
Map<Type, Generator> get contextOverrides => <Type, Generator>{
FileSystem: () => new MemoryFileSystem()
};
Map<Type, Generator> get contextOverrides {
return <Type, Generator>{FileSystem: () => new MemoryFileSystem()};
}