-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathimage_resolution.dart
293 lines (271 loc) · 11 KB
/
image_resolution.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
// Copyright 2014 The Flutter 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:collection';
import 'dart:convert';
import 'dart:ui' show hashValues;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'image_provider.dart';
const String _kAssetManifestFileName = 'AssetManifest.json';
/// Fetches an image from an [AssetBundle], having determined the exact image to
/// use based on the context.
///
/// Given a main asset and a set of variants, AssetImage chooses the most
/// appropriate asset for the current context, based on the device pixel ratio
/// and size given in the configuration passed to [resolve].
///
/// To show a specific image from a bundle without any asset resolution, use an
/// [AssetBundleImageProvider].
///
/// ## Naming assets for matching with different pixel densities
///
/// Main assets are presumed to match a nominal pixel ratio of 1.0. To specify
/// assets targeting different pixel ratios, place the variant assets in
/// the application bundle under subdirectories named in the form "Nx", where
/// N is the nominal device pixel ratio for that asset.
///
/// For example, suppose an application wants to use an icon named
/// "heart.png". This icon has representations at 1.0 (the main icon), as well
/// as 1.5 and 2.0 pixel ratios (variants). The asset bundle should then contain
/// the following assets:
///
/// ```
/// heart.png
/// 1.5x/heart.png
/// 2.0x/heart.png
/// ```
///
/// On a device with a 1.0 device pixel ratio, the image chosen would be
/// heart.png; on a device with a 1.3 device pixel ratio, the image chosen
/// would be 1.5x/heart.png.
///
/// The directory level of the asset does not matter as long as the variants are
/// at the equivalent level; that is, the following is also a valid bundle
/// structure:
///
/// ```
/// icons/heart.png
/// icons/1.5x/heart.png
/// icons/2.0x/heart.png
/// ```
///
/// assets/icons/3.0x/heart.png would be a valid variant of
/// assets/icons/heart.png.
///
///
/// ## Fetching assets
///
/// 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
/// the `flutter` section:
///
/// ```yaml
/// flutter:
/// assets:
/// - icons/heart.png
/// ```
///
/// Then, to fetch the image, use
/// ```dart
/// AssetImage('icons/heart.png')
/// ```
///
/// ## Assets in packages
///
/// To fetch an asset from a package, the [package] argument must be provided.
/// For instance, suppose the structure above is inside a package called
/// `my_icons`. Then to fetch the image, use:
///
/// ```dart
/// AssetImage('icons/heart.png', package: 'my_icons')
/// ```
///
/// 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
/// is bundled automatically with the app. In particular, assets used by the
/// 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
/// bundled, the app has to specify which ones to include. For instance a
/// package named `fancy_backgrounds` could have:
///
/// ```
/// lib/backgrounds/background1.png
/// lib/backgrounds/background2.png
/// lib/backgrounds/background3.png
/// ```
///
/// To include, say the first image, the `pubspec.yaml` of the app should specify
/// it in the `assets` section:
///
/// ```yaml
/// assets:
/// - packages/fancy_backgrounds/backgrounds/background1.png
/// ```
///
/// The `lib/` is implied, so it should not be included in the asset path.
///
/// See also:
///
/// * [Image.asset] for a shorthand of an [Image] widget backed by [AssetImage]
/// when used without a scale.
class AssetImage extends AssetBundleImageProvider {
/// Creates an object that fetches an image from an asset bundle.
///
/// The [assetName] argument must not be null. It should name the main asset
/// from the set of images to choose from. The [package] argument must be
/// non-null when fetching an asset that is included in package. See the
/// documentation for the [AssetImage] class itself for details.
const AssetImage(
this.assetName, {
this.bundle,
this.package,
}) : assert(assetName != null);
/// The name of the main asset from the set of images to choose from. See the
/// documentation for the [AssetImage] class itself for details.
final String assetName;
/// The name used to generate the key to obtain the asset. For local assets
/// this is [assetName], and for assets from packages the [assetName] is
/// prefixed 'packages/<package_name>/'.
String get keyName => package == null ? assetName : 'packages/$package/$assetName';
/// The bundle from which the image will be obtained.
///
/// If the provided [bundle] is null, the bundle provided in the
/// [ImageConfiguration] passed to the [resolve] call will be used instead. If
/// that is also null, the [rootBundle] is used.
///
/// The image is obtained by calling [AssetBundle.load] on the given [bundle]
/// using the key given by [keyName].
final AssetBundle bundle;
/// The name of the package from which the image is included. See the
/// documentation for the [AssetImage] class itself for details.
final String package;
// We assume the main asset is designed for a device pixel ratio of 1.0
static const double _naturalResolution = 1.0;
@override
Future<AssetBundleImageKey> obtainKey(ImageConfiguration configuration) {
// This function tries to return a SynchronousFuture if possible. We do this
// because otherwise showing an image would always take at least one frame,
// which would be sad. (This code is called from inside build/layout/paint,
// which all happens in one call frame; using native Futures would guarantee
// that we resolve each future in a new call frame, and thus not in this
// build/layout/paint sequence.)
final AssetBundle chosenBundle = bundle ?? configuration.bundle ?? rootBundle;
Completer<AssetBundleImageKey> completer;
Future<AssetBundleImageKey> result;
chosenBundle.loadStructuredData<Map<String, List<String>>>(_kAssetManifestFileName, _manifestParser).then<void>(
(Map<String, List<String>> manifest) {
final String chosenName = _chooseVariant(
keyName,
configuration,
manifest == null ? null : manifest[keyName],
);
final double chosenScale = _parseScale(chosenName);
final AssetBundleImageKey key = AssetBundleImageKey(
bundle: chosenBundle,
name: chosenName,
scale: chosenScale,
);
if (completer != null) {
// We already returned from this function, which means we are in the
// asynchronous mode. Pass the value to the completer. The completer's
// future is what we returned.
completer.complete(key);
} else {
// We haven't yet returned, so we must have been called synchronously
// just after loadStructuredData returned (which means it provided us
// with a SynchronousFuture). Let's return a SynchronousFuture
// ourselves.
result = SynchronousFuture<AssetBundleImageKey>(key);
}
}
).catchError((dynamic error, StackTrace stack) {
// We had an error. (This guarantees we weren't called synchronously.)
// Forward the error to the caller.
assert(completer != null);
assert(result == null);
completer.completeError(error, stack);
});
if (result != null) {
// The code above ran synchronously, and came up with an answer.
// Return the SynchronousFuture that we created above.
return result;
}
// The code above hasn't yet run its "then" handler yet. Let's prepare a
// completer for it to use when it does run.
completer = Completer<AssetBundleImageKey>();
return completer.future;
}
static Future<Map<String, List<String>>> _manifestParser(String jsonData) {
if (jsonData == null)
return SynchronousFuture<Map<String, List<String>>>(null);
// TODO(ianh): JSON decoding really shouldn't be on the main thread.
final Map<String, dynamic> parsedJson = json.decode(jsonData) as Map<String, dynamic>;
final Iterable<String> keys = parsedJson.keys;
final Map<String, List<String>> parsedManifest =
Map<String, List<String>>.fromIterables(keys,
keys.map<List<String>>((String key) => List<String>.from(parsedJson[key] as List<dynamic>)));
// TODO(ianh): convert that data structure to the right types.
return SynchronousFuture<Map<String, List<String>>>(parsedManifest);
}
String _chooseVariant(String main, ImageConfiguration config, List<String> candidates) {
if (config.devicePixelRatio == null || candidates == null || candidates.isEmpty)
return main;
// TODO(ianh): Consider moving this parsing logic into _manifestParser.
final SplayTreeMap<double, String> mapping = SplayTreeMap<double, String>();
for (final String candidate in candidates)
mapping[_parseScale(candidate)] = candidate;
// TODO(ianh): implement support for config.locale, config.textDirection,
// config.size, config.platform (then document this over in the Image.asset
// docs)
return _findNearest(mapping, config.devicePixelRatio);
}
// Return the value for the key in a [SplayTreeMap] nearest the provided key.
String _findNearest(SplayTreeMap<double, String> candidates, double value) {
if (candidates.containsKey(value))
return candidates[value];
final double lower = candidates.lastKeyBefore(value);
final double upper = candidates.firstKeyAfter(value);
if (lower == null)
return candidates[upper];
if (upper == null)
return candidates[lower];
if (value > (lower + upper) / 2)
return candidates[upper];
else
return candidates[lower];
}
static final RegExp _extractRatioRegExp = RegExp(r'/?(\d+(\.\d*)?)x$');
double _parseScale(String key) {
if (key == assetName) {
return _naturalResolution;
}
final Uri assetUri = Uri.parse(key);
String directoryPath = '';
if (assetUri.pathSegments.length > 1) {
directoryPath = assetUri.pathSegments[assetUri.pathSegments.length - 2];
}
final Match match = _extractRatioRegExp.firstMatch(directoryPath);
if (match != null && match.groupCount > 0)
return double.parse(match.group(1));
return _naturalResolution; // i.e. default to 1.0x
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is AssetImage
&& other.keyName == keyName
&& other.bundle == bundle;
}
@override
int get hashCode => hashValues(keyName, bundle);
@override
String toString() => '${objectRuntimeType(this, 'AssetImage')}(bundle: $bundle, name: "$keyName")';
}