Commit 2dfdc840 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Refactor everything to do with images (#4583)

Overview
========

This patch refactors images to achieve the following goals:

* it allows references to unresolved assets to be passed
  around (previously, almost every layer of the system had to know about
  whether an image came from an asset bundle or the network or
  elsewhere, and had to manually interact with the image cache).

* it allows decorations to use the same API for declaring images as the
  widget tree.

It requires some minor changes to call sites that use images, as
discussed below.

Widgets
-------

Change this:

```dart
      child: new AssetImage(
        name: 'my_asset.png',
        ...
      )
```

...to this:

```dart
      child: new Image(
        image: new AssetImage('my_asset.png'),
        ...
      )
```

Decorations
-----------

Change this:

```dart
      child: new DecoratedBox(
        decoration: new BoxDecoration(
          backgroundImage: new BackgroundImage(
            image: DefaultAssetBundle.of(context).loadImage('my_asset.png'),
            ...
          ),
          ...
        ),
        child: ...
      )
```

...to this:

```dart
      child: new DecoratedBox(
        decoration: new BoxDecoration(
          backgroundImage: new BackgroundImage(
            image: new AssetImage('my_asset.png'),
            ...
          ),
          ...
        ),
        child: ...
      )
```

DETAILED CHANGE LOG
===================

The following APIs have been replaced in this patch:

* The `AssetImage` and `NetworkImage` widgets have been split in two,
  with identically-named `ImageProvider` subclasses providing the
  image-loading logic, and a single `Image` widget providing all the
  widget tree logic.

* `ImageResource` is now `ImageStream`. Rather than configuring it with
  a `Future<ImageInfo>`, you complete it with an `ImageStreamCompleter`.

* `ImageCache.load` and `ImageCache.loadProvider` are replaced by
  `ImageCache.putIfAbsent`.

The following APIs have changed in this patch:

* `ImageCache` works in terms of arbitrary keys and caches
  `ImageStreamCompleter` objects using those keys. With the new model,
  you should never need to interact with the cache directly.

* `Decoration` can now be `const`. The state has moved to the
  `BoxPainter` class. Instead of a list of listeners, there's now just a
  single callback and a `dispose()` method on the painter. The callback
  is passed in to the `createBoxPainter()` method. When invoked, you
  should repaint the painter.

The following new APIs are introduced:

* `AssetBundle.loadStructuredData`.

* `SynchronousFuture`, a variant of `Future` that calls the `then`
  callback synchronously. This enables the asynchronous and
  synchronous (in-the-cache) code paths to look identical yet for the
  latter to avoid returning to the event loop mid-paint.

* `ExactAssetImage`, a variant of `AssetImage` that doesn't do anything clever.

* `ImageConfiguration`, a class that describes parameters that configure
  the `AssetImage` resolver.

The following APIs are entirely removed by this patch:

* `AssetBundle.loadImage` is gone. Use an `AssetImage` instead.

* `AssetVendor` is gone. `AssetImage` handles everything `AssetVendor`
  used to handle.

* `RawImageResource` and `AsyncImage` are gone.

The following code-level changes are performed:

* `Image`, which replaces `AsyncImage`, `NetworkImage`, `AssetImage`,
  and `RawResourceImage`, lives in `image.dart`.

* `DecoratedBox` and `Container` live in their own file now,
  `container.dart` (they reference `image.dart`).

DIRECTIONS FOR FUTURE RESEARCH
==============================

* The `ImageConfiguration` fields are mostly aspirational. Right now
  only `devicePixelRatio` and `bundle` are implemented. `locale` isn't
  even plumbed through, it will require work on the localisation logic.

* We should go through and make `BoxDecoration`, `AssetImage`, and
  `NetworkImage` objects `const` where possible.

* This patch makes supporting animated GIFs much easier.

* This patch makes it possible to create an abstract concept of an
  "Icon" that could be either an image or a font-based glyph (using
  `IconData` or similar). (see
  https://github.com/flutter/flutter/issues/4494)

RELATED ISSUES
==============

Fixes https://github.com/flutter/flutter/issues/4500
Fixes https://github.com/flutter/flutter/issues/4495
Obsoletes https://github.com/flutter/flutter/issues/4496
parent 2ce57eb3
This diff is collapsed.
......@@ -429,7 +429,7 @@ class CardCollectionState extends State<CardCollection> {
if (_sunshine) {
cardCollection = new Stack(
children: <Widget>[
new Column(children: <Widget>[new NetworkImage(src: _sunshineURL)]),
new Column(children: <Widget>[new Image(image: new NetworkImage(_sunshineURL))]),
new ShaderMask(child: cardCollection, shaderCallback: _createShader)
]
);
......
......@@ -157,8 +157,8 @@ class WeatherButton extends StatelessWidget {
child: new InkWell(
onTap: onPressed,
child: new Center(
child: new AssetImage(
name: icon,
child: new Image(
image: new AssetImage(icon),
width: _kWeatherIconSize,
height: _kWeatherIconSize
)
......
......@@ -66,8 +66,8 @@ class TravelDestinationItem extends StatelessWidget {
top: 0.0,
bottom: 0.0,
right: 0.0,
child: new AssetImage(
name: destination.assetName,
child: new Image(
image: new AssetImage(destination.assetName),
fit: ImageFit.cover
)
),
......
......@@ -129,8 +129,8 @@ class ContactsDemoState extends State<ContactsDemo> {
title : new Text('Ali Connors'),
background: new Stack(
children: <Widget>[
new AssetImage(
name: 'packages/flutter_gallery_assets/ali_connors.png',
new Image(
image: new AssetImage('packages/flutter_gallery_assets/ali_connors.png'),
fit: ImageFit.cover,
height: _appBarHeight
),
......
......@@ -66,8 +66,8 @@ class GridDemoPhotoItem extends StatelessWidget {
body: new Material(
child: new Hero(
tag: photoHeroTag,
child: new AssetImage(
name: photo.assetName,
child: new Image(
image: new AssetImage(photo.assetName),
fit: ImageFit.cover
)
)
......@@ -84,8 +84,8 @@ class GridDemoPhotoItem extends StatelessWidget {
child: new Hero(
key: new Key(photo.assetName),
tag: photoHeroTag,
child: new AssetImage(
name: photo.assetName,
child: new Image(
image: new AssetImage(photo.assetName),
fit: ImageFit.cover
)
)
......
......@@ -105,8 +105,8 @@ class _PestoDemoState extends State<PestoDemo> {
bottom: extraPadding
),
child: new Center(
child: new AssetImage(
name: _kLogoImages[bestHeight],
child: new Image(
image: new AssetImage(_kLogoImages[bestHeight]),
fit: ImageFit.scaleDown
)
)
......@@ -133,8 +133,8 @@ class _PestoDemoState extends State<PestoDemo> {
padding: const EdgeInsets.all(2.0),
margin: const EdgeInsets.only(bottom: 8.0),
child: new ClipOval(
child: new AssetImage(
name: _kUserImage,
child: new Image(
image: new AssetImage(_kUserImage),
fit: ImageFit.contain
)
)
......@@ -237,8 +237,8 @@ class _RecipeCard extends StatelessWidget {
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new AssetImage(
name: recipe.imagePath,
new Image(
image: new AssetImage(recipe.imagePath),
fit: ImageFit.scaleDown
),
new Flexible(
......@@ -246,10 +246,10 @@ class _RecipeCard extends StatelessWidget {
children: <Widget>[
new Padding(
padding: const EdgeInsets.all(16.0),
child: new AssetImage(
child: new Image(
image: new AssetImage(recipe.ingredientsImagePath),
width: 48.0,
height: 48.0,
name: recipe.ingredientsImagePath
height: 48.0
)
),
new Column(
......@@ -341,7 +341,7 @@ class _RecipePageState extends State<_RecipePage> {
decoration: new BoxDecoration(
backgroundColor: Theme.of(context).canvasColor,
backgroundImage: new BackgroundImage(
image: DefaultAssetBundle.of(context).loadImage(config.recipe.imagePath),
image: new AssetImage(config.recipe.imagePath),
alignment: FractionalOffset.topCenter,
fit: fullWidth ? ImageFit.fitWidth : ImageFit.cover
)
......@@ -428,10 +428,10 @@ class _RecipeSheet extends StatelessWidget {
children: <Widget>[
new TableCell(
verticalAlignment: TableCellVerticalAlignment.middle,
child: new AssetImage(
child: new Image(
image: new AssetImage(recipe.ingredientsImagePath),
width: 32.0,
height: 32.0,
name: recipe.ingredientsImagePath,
alignment: FractionalOffset.centerLeft,
fit: ImageFit.scaleDown
)
......
......@@ -36,9 +36,9 @@ class VendorItem extends StatelessWidget {
child: new ClipRRect(
xRadius: 12.0,
yRadius: 12.0,
child: new AssetImage(
fit: ImageFit.cover,
name: vendor.avatarAsset
child: new Image(
image: new AssetImage(vendor.avatarAsset),
fit: ImageFit.cover
)
)
),
......@@ -165,9 +165,9 @@ class FeatureItem extends StatelessWidget {
minHeight: 340.0,
maxHeight: 340.0,
alignment: FractionalOffset.topRight,
child: new AssetImage(
fit: ImageFit.cover,
name: product.imageAsset
child: new Image(
image: new AssetImage(product.imageAsset),
fit: ImageFit.cover
)
)
)
......@@ -229,9 +229,9 @@ class ProductItem extends StatelessWidget {
new Hero(
tag: productHeroTag,
key: new ObjectKey(product),
child: new AssetImage(
fit: ImageFit.contain,
name: product.imageAsset
child: new Image(
image: new AssetImage(product.imageAsset),
fit: ImageFit.contain
)
),
new Material(
......
......@@ -41,9 +41,9 @@ class OrderItem extends StatelessWidget {
height: 248.0,
child: new Hero(
tag: productHeroTag,
child: new AssetImage(
fit: ImageFit.contain,
name: product.imageAsset
child: new Image(
image: new AssetImage(product.imageAsset),
fit: ImageFit.contain
)
)
)
......@@ -192,9 +192,9 @@ class _OrderPageState extends State<OrderPage> {
.map((Product product) {
return new Card(
elevation: 0,
child: new AssetImage(
fit: ImageFit.contain,
name: product.imageAsset
child: new Image(
image: new AssetImage(product.imageAsset),
fit: ImageFit.contain
)
);
}).toList()
......
......@@ -222,8 +222,8 @@ new ScrollableGrid(
footer: new GridTileBar(
title: new Text(url)
),
child: new NetworkImage(
src: url,
child: new Image(
image: new NetworkImage(url),
fit: ImageFit.cover
)
);
......
......@@ -66,8 +66,8 @@ class GalleryHomeState extends State<GalleryHome> {
appBar: new AppBar(
expandedHeight: _kFlexibleSpaceMaxHeight,
flexibleSpace: new FlexibleSpaceBar(
background: new AssetImage(
name: 'packages/flutter_gallery_assets/appbar_background.jpg',
background: new Image(
image: new AssetImage('packages/flutter_gallery_assets/appbar_background.jpg'),
fit: ImageFit.cover,
height: _kFlexibleSpaceMaxHeight
),
......
......@@ -36,17 +36,19 @@ test 1 1
class TestAssetBundle extends AssetBundle {
@override
ImageResource loadImage(String key) => null;
Future<core.MojoDataPipeConsumer> load(String key) => null;
@override
Future<String> loadString(String key) {
Future<String> loadString(String key, { bool cache: true }) {
if (key == 'lib/gallery/example_code.dart')
return new Future<String>.value(testCodeFile);
return null;
}
@override
Future<core.MojoDataPipeConsumer> load(String key) => null;
Future<dynamic> loadStructuredData(String key, Future<dynamic> parser(String value)) async {
return parser(await loadString(key));
}
@override
String toString() => '$runtimeType@$hashCode()';
......
......@@ -51,7 +51,9 @@ void attachWidgetTreeToRenderTree(RenderProxyBox container) {
new RaisedButton(
child: new Row(
children: <Widget>[
new NetworkImage(src: "http://flutter.io/favicon.ico"),
new Image(
image: new NetworkImage('http://flutter.io/favicon.ico')
),
new Text('PRESS ME'),
]
),
......
......@@ -14,3 +14,4 @@ export 'src/foundation/basic_types.dart';
export 'src/foundation/binding.dart';
export 'src/foundation/change_notifier.dart';
export 'src/foundation/print.dart';
export 'src/foundation/synchronous_future.dart';
......@@ -22,7 +22,9 @@ export 'src/services/haptic_feedback.dart';
export 'src/services/host_messages.dart';
export 'src/services/image_cache.dart';
export 'src/services/image_decoder.dart';
export 'src/services/image_resource.dart';
export 'src/services/image_provider.dart';
export 'src/services/image_resolution.dart';
export 'src/services/image_stream.dart';
export 'src/services/keyboard.dart';
export 'src/services/path_provider.dart';
export 'src/services/shell.dart';
......
// Copyright 2015 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';
/// A [Future] whose [then] implementation calls the callback immediately.
///
/// This is similar to [new Future.value], except that the value is available in
/// the same event-loop iteration.
///
/// ⚠ This class is useful in cases where you want to expose a single API, where
/// you normally want to have everything execute synchronously, but where on
/// rare occasions you want the ability to switch to an asynchronous model. **In
/// general use of this class should be avoided as it is very easy difficult to
/// debug such bimodal behavior.**
class SynchronousFuture<T> implements Future<T> {
/// Creates a synchronous future.
///
/// See also [new Future.value].
SynchronousFuture(this._value);
final T _value;
@override
Stream<T> asStream() {
final StreamController<T> controller = new StreamController<T>();
controller.add(_value);
controller.close();
return controller.stream;
}
@override
Future<T> catchError(Function onError, { bool test(dynamic error) }) => new Completer<T>().future;
@override
Future<dynamic/*=E*/> then/*<E>*/(dynamic f(T value), { Function onError }) {
dynamic result = f(_value);
if (result is Future<dynamic/*=E*/>)
return result;
return new SynchronousFuture<dynamic/*=E*/>(result);
}
@override
Future<T> timeout(Duration timeLimit, { Future<T> onTimeout() }) => new Completer<T>().future;
@override
Future<T> whenComplete(Future<T> action()) => action();
}
\ No newline at end of file
......@@ -34,6 +34,9 @@ class _DropDownMenuPainter extends CustomPainter {
elevation = elevation,
resize = resize,
_painter = new BoxDecoration(
// If you add a background image here, you must provide a real
// configuration in the paint() function and you must provide some sort
// of onChanged callback here.
backgroundColor: color,
borderRadius: 2.0,
boxShadow: kElevationToShadow[elevation]
......@@ -59,7 +62,9 @@ class _DropDownMenuPainter extends CustomPainter {
end: size.height
);
_painter.paint(canvas, new Rect.fromLTRB(0.0, top.evaluate(resize), size.width, bottom.evaluate(resize)));
final Rect rect = new Rect.fromLTRB(0.0, top.evaluate(resize), size.width, bottom.evaluate(resize));
_painter.paint(canvas, rect.topLeft.toOffset(), new ImageConfiguration(size: rect.size));
}
@override
......
......@@ -80,11 +80,11 @@ class Switch extends StatelessWidget {
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
ThemeData themeData = Theme.of(context);
final ThemeData themeData = Theme.of(context);
final bool isDark = themeData.brightness == Brightness.dark;
Color activeThumbColor = activeColor ?? themeData.accentColor;
Color activeTrackColor = activeThumbColor.withAlpha(0x80);
final Color activeThumbColor = activeColor ?? themeData.accentColor;
final Color activeTrackColor = activeThumbColor.withAlpha(0x80);
Color inactiveThumbColor;
Color inactiveTrackColor;
......@@ -104,9 +104,18 @@ class Switch extends StatelessWidget {
inactiveThumbDecoration: inactiveThumbDecoration,
activeTrackColor: activeTrackColor,
inactiveTrackColor: inactiveTrackColor,
configuration: createLocalImageConfiguration(context),
onChanged: onChanged
);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('value: ${value ? "on" : "off"}');
if (onChanged == null)
description.add('disabled');
}
}
class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
......@@ -119,6 +128,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
this.inactiveThumbDecoration,
this.activeTrackColor,
this.inactiveTrackColor,
this.configuration,
this.onChanged
}) : super(key: key);
......@@ -129,6 +139,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
final Decoration inactiveThumbDecoration;
final Color activeTrackColor;
final Color inactiveTrackColor;
final ImageConfiguration configuration;
final ValueChanged<bool> onChanged;
@override
......@@ -140,6 +151,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
inactiveThumbDecoration: inactiveThumbDecoration,
activeTrackColor: activeTrackColor,
inactiveTrackColor: inactiveTrackColor,
configuration: configuration,
onChanged: onChanged
);
......@@ -153,6 +165,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
..inactiveThumbDecoration = inactiveThumbDecoration
..activeTrackColor = activeTrackColor
..inactiveTrackColor = inactiveTrackColor
..configuration = configuration
..onChanged = onChanged;
}
}
......@@ -173,11 +186,13 @@ class _RenderSwitch extends RenderToggleable {
Decoration inactiveThumbDecoration,
Color activeTrackColor,
Color inactiveTrackColor,
ImageConfiguration configuration,
ValueChanged<bool> onChanged
}) : _activeThumbDecoration = activeThumbDecoration,
_inactiveThumbDecoration = inactiveThumbDecoration,
_activeTrackColor = activeTrackColor,
_inactiveTrackColor = inactiveTrackColor,
_configuration = configuration,
super(
value: value,
activeColor: activeColor,
......@@ -196,43 +211,19 @@ class _RenderSwitch extends RenderToggleable {
set activeThumbDecoration(Decoration value) {
if (value == _activeThumbDecoration)
return;
_removeActiveThumbListenerIfNeeded();
_activeThumbDecoration = value;
_addActiveThumbListenerIfNeeded();
markNeedsPaint();
}
void _addActiveThumbListenerIfNeeded() {
if (attached && _activeThumbDecoration != null && _activeThumbDecoration.needsListeners)
_activeThumbDecoration.addChangeListener(markNeedsPaint);
}
void _removeActiveThumbListenerIfNeeded() {
if (attached && _activeThumbDecoration != null && _activeThumbDecoration.needsListeners)
_activeThumbDecoration.removeChangeListener(markNeedsPaint);
}
Decoration get inactiveThumbDecoration => _inactiveThumbDecoration;
Decoration _inactiveThumbDecoration;
set inactiveThumbDecoration(Decoration value) {
if (value == _inactiveThumbDecoration)
return;
_removeInactiveThumbListenerIfNeeded();
_inactiveThumbDecoration = value;
_addInactiveThumbListenerIfNeeded();
markNeedsPaint();
}
void _addInactiveThumbListenerIfNeeded() {
if (attached && _inactiveThumbDecoration != null && _inactiveThumbDecoration.needsListeners)
_inactiveThumbDecoration.addChangeListener(markNeedsPaint);
}
void _removeInactiveThumbListenerIfNeeded() {
if (attached && _inactiveThumbDecoration != null && _inactiveThumbDecoration.needsListeners)
_inactiveThumbDecoration.removeChangeListener(markNeedsPaint);
}
Color get activeTrackColor => _activeTrackColor;
Color _activeTrackColor;
set activeTrackColor(Color value) {
......@@ -253,17 +244,20 @@ class _RenderSwitch extends RenderToggleable {
markNeedsPaint();
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_addInactiveThumbListenerIfNeeded();
_addActiveThumbListenerIfNeeded();
ImageConfiguration get configuration => _configuration;
ImageConfiguration _configuration;
set configuration (ImageConfiguration value) {
assert(value != null);
if (value == _configuration)
return;
_configuration = value;
markNeedsPaint();
}
@override
void detach() {
_removeActiveThumbListenerIfNeeded();
_removeInactiveThumbListenerIfNeeded();
_cachedThumbPainter?.dispose();
_cachedThumbPainter = null;
super.detach();
}
......@@ -318,22 +312,22 @@ class _RenderSwitch extends RenderToggleable {
final bool isActive = onChanged != null;
final double currentPosition = position.value;
Color trackColor = isActive ? Color.lerp(inactiveTrackColor, activeTrackColor, currentPosition) : inactiveTrackColor;
final Color trackColor = isActive ? Color.lerp(inactiveTrackColor, activeTrackColor, currentPosition) : inactiveTrackColor;
// Paint the track
Paint paint = new Paint()
final Paint paint = new Paint()
..color = trackColor;
double trackHorizontalPadding = kRadialReactionRadius - _kTrackRadius;
Rect trackRect = new Rect.fromLTWH(
final double trackHorizontalPadding = kRadialReactionRadius - _kTrackRadius;
final Rect trackRect = new Rect.fromLTWH(
offset.dx + trackHorizontalPadding,
offset.dy + (size.height - _kTrackHeight) / 2.0,
size.width - 2.0 * trackHorizontalPadding,
_kTrackHeight
);
RRect trackRRect = new RRect.fromRectXY(trackRect, _kTrackRadius, _kTrackRadius);
final RRect trackRRect = new RRect.fromRectXY(trackRect, _kTrackRadius, _kTrackRadius);
canvas.drawRRect(trackRRect, paint);
Point thumbPosition = new Point(
final Point thumbPosition = new Point(
kRadialReactionRadius + currentPosition * _trackInnerLength,
size.height / 2.0
);
......@@ -342,25 +336,25 @@ class _RenderSwitch extends RenderToggleable {
BoxPainter thumbPainter;
if (_inactiveThumbDecoration == null && _activeThumbDecoration == null) {
Color thumbColor = isActive ? Color.lerp(inactiveColor, activeColor, currentPosition) : inactiveColor;
final Color thumbColor = isActive ? Color.lerp(inactiveColor, activeColor, currentPosition) : inactiveColor;
if (thumbColor != _cachedThumbColor || _cachedThumbPainter == null) {
_cachedThumbColor = thumbColor;
_cachedThumbPainter = _createDefaultThumbDecoration(thumbColor).createBoxPainter();
_cachedThumbPainter = _createDefaultThumbDecoration(thumbColor).createBoxPainter(markNeedsPaint);
}
thumbPainter = _cachedThumbPainter;
} else {
Decoration startDecoration = _inactiveThumbDecoration ?? _createDefaultThumbDecoration(inactiveColor);
Decoration endDecoration = _activeThumbDecoration ?? _createDefaultThumbDecoration(isActive ? activeTrackColor : inactiveColor);
thumbPainter = Decoration.lerp(startDecoration, endDecoration, currentPosition).createBoxPainter();
final Decoration startDecoration = _inactiveThumbDecoration ?? _createDefaultThumbDecoration(inactiveColor);
final Decoration endDecoration = _activeThumbDecoration ?? _createDefaultThumbDecoration(isActive ? activeTrackColor : inactiveColor);
thumbPainter = Decoration.lerp(startDecoration, endDecoration, currentPosition).createBoxPainter(markNeedsPaint);
}
// The thumb contracts slightly during the animation
double inset = 1.0 - (currentPosition - 0.5).abs() * 2.0;
double radius = _kThumbRadius - inset;
Rect thumbRect = new Rect.fromLTRB(thumbPosition.x + offset.dx - radius,
thumbPosition.y + offset.dy - radius,
thumbPosition.x + offset.dx + radius,
thumbPosition.y + offset.dy + radius);
thumbPainter.paint(canvas, thumbRect);
final double inset = 1.0 - (currentPosition - 0.5).abs() * 2.0;
final double radius = _kThumbRadius - inset;
thumbPainter.paint(
canvas,
thumbPosition.toOffset() + offset,
configuration.copyWith(size: new Size.fromRadius(radius))
);
}
}
......@@ -291,4 +291,12 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic
@override
void handleSemanticScrollDown() { }
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('value: ${value ? "checked" : "unchecked"}');
if (!isInteractive)
description.add('disabled');
}
}
......@@ -1039,20 +1039,29 @@ class FractionalOffset {
}
/// A background image for a box.
///
/// The image is painted using [paintImage], which describes the meanings of the
/// various fields on this class in more detail.
class BackgroundImage {
/// Creates a background image.
///
/// The [image] argument must not be null.
BackgroundImage({
ImageResource image,
const BackgroundImage({
this.image,
this.fit,
this.repeat: ImageRepeat.noRepeat,
this.centerSlice,
this.colorFilter,
this.alignment
}) : _imageResource = image;
});
/// The image to be painted into the background.
final ImageProvider image;
/// How the background image should be inscribed into the box.
///
/// The default varies based on the other fields. See the discussion at
/// [paintImage].
final ImageFit fit;
/// How to paint any portions of the box not covered by the background image.
......@@ -1077,44 +1086,6 @@ class BackgroundImage {
/// of the right edge of its layout bounds.
final FractionalOffset alignment;
/// The image to be painted into the background.
ui.Image get image => _image;
ui.Image _image;
final ImageResource _imageResource;
final List<VoidCallback> _listeners = <VoidCallback>[];
/// Adds a listener for background-image changes (e.g., for when it arrives
/// from the network).
void _addChangeListener(VoidCallback listener) {
// We add the listener to the _imageResource first so that the first change
// listener doesn't get callback synchronously if the image resource is
// already resolved.
if (_listeners.isEmpty)
_imageResource.addListener(_handleImageChanged);
_listeners.add(listener);
}
/// Removes the listener for background-image changes.
void _removeChangeListener(VoidCallback listener) {
_listeners.remove(listener);
// We need to remove ourselves as listeners from the _imageResource so that
// we're not kept alive by the image_cache.
if (_listeners.isEmpty)
_imageResource.removeListener(_handleImageChanged);
}
void _handleImageChanged(ImageInfo resolvedImage) {
if (resolvedImage == null)
return;
_image = resolvedImage.image;
final List<VoidCallback> localListeners =
new List<VoidCallback>.from(_listeners);
for (VoidCallback listener in localListeners)
listener();
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
......@@ -1122,19 +1093,19 @@ class BackgroundImage {
if (other is! BackgroundImage)
return false;
final BackgroundImage typedOther = other;
return fit == typedOther.fit &&
return image == typedOther.image &&
fit == typedOther.fit &&
repeat == typedOther.repeat &&
centerSlice == typedOther.centerSlice &&
colorFilter == typedOther.colorFilter &&
alignment == typedOther.alignment &&
_imageResource == typedOther._imageResource;
alignment == typedOther.alignment;
}
@override
int get hashCode => hashValues(fit, repeat, centerSlice, colorFilter, alignment, _imageResource);
int get hashCode => hashValues(image, fit, repeat, centerSlice, colorFilter, alignment);
@override
String toString() => 'BackgroundImage($fit, $repeat)';
String toString() => 'BackgroundImage($image, $fit, $repeat)';
}
/// The shape to use when rendering a BoxDecoration.
......@@ -1318,24 +1289,6 @@ class BoxDecoration extends Decoration {
return result.join('\n');
}
/// Whether this [Decoration] subclass needs its painters to use
/// [addChangeListener] to listen for updates.
///
/// [BoxDecoration] objects only need a listener if they have a
/// background image.
@override
bool get needsListeners => backgroundImage != null;
@override
void addChangeListener(VoidCallback listener) {
backgroundImage?._addChangeListener(listener);
}
@override
void removeChangeListener(VoidCallback listener) {
backgroundImage?._removeChangeListener(listener);
}
@override
bool hitTest(Size size, Point position) {
assert(shape != null);
......@@ -1358,12 +1311,15 @@ class BoxDecoration extends Decoration {
}
@override
_BoxDecorationPainter createBoxPainter() => new _BoxDecorationPainter(this);
_BoxDecorationPainter createBoxPainter([VoidCallback onChanged]) {
assert(onChanged != null || backgroundImage == null);
return new _BoxDecorationPainter(this, onChanged);
}
}
/// An object that paints a [BoxDecoration] into a canvas.
class _BoxDecorationPainter extends BoxPainter {
_BoxDecorationPainter(this._decoration) {
_BoxDecorationPainter(this._decoration, VoidCallback onChange) : super(onChange) {
assert(_decoration != null);
}
......@@ -1430,11 +1386,20 @@ class _BoxDecorationPainter extends BoxPainter {
_paintBox(canvas, rect, _getBackgroundPaint(rect));
}
void _paintBackgroundImage(Canvas canvas, Rect rect) {
ImageStream _imageStream;
ImageInfo _image;
void _paintBackgroundImage(Canvas canvas, Rect rect, ImageConfiguration configuration) {
final BackgroundImage backgroundImage = _decoration.backgroundImage;
if (backgroundImage == null)
return;
ui.Image image = backgroundImage.image;
final ImageStream newImageStream = backgroundImage.image.resolve(configuration);
if (newImageStream.key != _imageStream?.key) {
_imageStream?.removeListener(_imageListener);
_imageStream = newImageStream;
_imageStream.addListener(_imageListener);
}
final ui.Image image = _image?.image;
if (image == null)
return;
paintImage(
......@@ -1448,12 +1413,31 @@ class _BoxDecorationPainter extends BoxPainter {
);
}
void _imageListener(ImageInfo value) {
if (_image == value)
return;
_image = value;
assert(onChanged != null);
onChanged();
}
@override
void dispose() {
_imageStream?.removeListener(_imageListener);
_imageStream = null;
_image = null;
super.dispose();
}
/// Paint the box decoration into the given location on the given canvas
@override
void paint(Canvas canvas, Rect rect) {
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
assert(configuration != null);
assert(configuration.size != null);
final Rect rect = offset & configuration.size;
_paintShadows(canvas, rect);
_paintBackgroundColor(canvas, rect);
_paintBackgroundImage(canvas, rect);
_paintBackgroundImage(canvas, rect, configuration);
_decoration.border?.paint(
canvas,
rect,
......
......@@ -2,10 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/services.dart';
import 'package:meta/meta.dart';
import 'basic_types.dart';
import 'edge_insets.dart';
export 'basic_types.dart' show Point, Offset, Size;
export 'edge_insets.dart' show EdgeInsets;
export 'package:flutter/services.dart' show ImageConfiguration;
// This group of classes is intended for painting in cartesian coordinates.
......@@ -64,26 +69,12 @@ abstract class Decoration {
/// otherwise.
bool hitTest(Size size, Point position) => true;
/// Whether this [Decoration] subclass needs its painters to use
/// [addChangeListener] to listen for updates. For example, if a
/// decoration draws a background image, owners would have to listen
/// for the image's load completing so that they could repaint
/// themselves when appropriate.
bool get needsListeners => false;
/// Register a listener. See [needsListeners].
///
/// Only call this if [needsListeners] is true.
void addChangeListener(VoidCallback listener) { assert(false); }
/// Unregisters a listener previous registered with
/// [addChangeListener]. See [needsListeners].
///
/// Only call this if [needsListeners] is true.
void removeChangeListener(VoidCallback listener) { assert(false); }
/// Returns a [BoxPainter] that will paint this decoration.
BoxPainter createBoxPainter();
///
/// The `onChanged` argument configures [BoxPainter.onChanged]. It can be
/// omitted if there is no chance that the painter will change (for example,
/// if it is a [BoxDecoration] with definitely no [BackgroundImage]).
BoxPainter createBoxPainter([VoidCallback onChanged]);
@override
String toString([String prefix = '']) => '$prefix$runtimeType';
......@@ -93,16 +84,26 @@ abstract class Decoration {
///
/// [BoxPainter] objects can cache resources so that they can be used
/// multiple times.
abstract class BoxPainter { // ignore: one_member_abstracts
///
/// Some resources used by [BoxPainter] may load asynchronously. When this
/// happens, the [onChanged] callback will be invoked. To stop this callback
/// from being called after the painter has been discarded, call [dispose].
abstract class BoxPainter {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
BoxPainter([this._onChanged]);
/// Paints the [Decoration] for which this object was created on the
/// given canvas using the given rectangle.
/// given canvas using the given configuration.
///
/// The [ImageConfiguration] object passed as the third argument must, at a
/// minimum, have a non-null [Size].
///
/// If this object caches resources for painting (e.g. [Paint]
/// objects), the cache may be flushed when [paint] is called with a
/// new [Rect]. For this reason, it may be more efficient to call
/// [Decoration.createBoxPainter] for each different rectangle that
/// is being painted in a particular frame.
/// If this object caches resources for painting (e.g. [Paint] objects), the
/// cache may be flushed when [paint] is called with a new configuration. For
/// this reason, it may be more efficient to call
/// [Decoration.createBoxPainter] for each different rectangle that is being
/// painted in a particular frame.
///
/// For example, if a decoration's owner wants to paint a particular
/// decoration once for its whole size, and once just in the bottom
......@@ -110,5 +111,21 @@ abstract class BoxPainter { // ignore: one_member_abstracts
/// However, when its size changes, it could continue using those
/// same instances, since the previous resources would no longer be
/// relevant and thus losing them would not be an issue.
void paint(Canvas canvas, Rect rect);
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration);
/// Callback that is invoked if an asynchronously-loading resource used by the
/// decoration finishes loading. For example, an image. When this is invoked,
/// the [paint] method should be called again.
///
/// Resources might not start to load until after [paint] has been called,
/// because they might depend on the configuration.
VoidCallback get onChanged => _onChanged;
VoidCallback _onChanged;
/// Discard any resources being held by the object. This also guarantees that
/// the [onChanged] callback will not be called again.
@mustCallSuper
void dispose() {
_onChanged = null;
}
}
......@@ -15,6 +15,9 @@ export 'package:flutter/painting.dart' show
///
/// The render image attempts to find a size for itself that fits in the given
/// constraints and preserves the image's intrinisc aspect ratio.
///
/// The image is painted using [paintImage], which describes the meanings of the
/// various fields on this class in more detail.
class RenderImage extends RenderBox {
/// Creates a render box that displays an image.
RenderImage({
......@@ -112,6 +115,9 @@ class RenderImage extends RenderBox {
}
/// How to inscribe the image into the place allocated during layout.
///
/// The default varies based on the other fields. See the discussion at
/// [paintImage].
ImageFit get fit => _fit;
ImageFit _fit;
set fit (ImageFit value) {
......
......@@ -6,6 +6,7 @@ import 'dart:ui' as ui show ImageFilter;
import 'package:flutter/animation.dart';
import 'package:flutter/gestures.dart';
import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart';
import 'box.dart';
......@@ -1093,17 +1094,23 @@ enum DecorationPosition {
class RenderDecoratedBox extends RenderProxyBox {
/// Creates a decorated box.
///
/// The [decoration] and [position] arguments must not be null. By default the
/// decoration paints behind the child.
/// The [decoration], [position], and [configuration] arguments must not be
/// null. By default the decoration paints behind the child.
///
/// The [ImageConfiguration] will be passed to the decoration (with the size
/// filled in) to let it resolve images.
RenderDecoratedBox({
Decoration decoration,
@required Decoration decoration,
DecorationPosition position: DecorationPosition.background,
ImageConfiguration configuration: ImageConfiguration.empty,
RenderBox child
}) : _decoration = decoration,
_position = position,
_configuration = configuration,
super(child) {
assert(decoration != null);
assert(position != null);
assert(configuration != null);
}
BoxPainter _painter;
......@@ -1117,10 +1124,9 @@ class RenderDecoratedBox extends RenderProxyBox {
assert(newDecoration != null);
if (newDecoration == _decoration)
return;
_removeListenerIfNeeded();
_painter?.dispose();
_painter = null;
_decoration = newDecoration;
_addListenerIfNeeded();
markNeedsPaint();
}
......@@ -1135,29 +1141,23 @@ class RenderDecoratedBox extends RenderProxyBox {
markNeedsPaint();
}
bool get _needsListeners {
return attached && _decoration.needsListeners;
}
void _addListenerIfNeeded() {
if (_needsListeners)
_decoration.addChangeListener(markNeedsPaint);
}
void _removeListenerIfNeeded() {
if (_needsListeners)
_decoration.removeChangeListener(markNeedsPaint);
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_addListenerIfNeeded();
/// The settings to pass to the decoration when painting, so that it can
/// resolve images appropriately. See [ImageProvider.resolve] and
/// [BoxPainter.paint].
ImageConfiguration get configuration => _configuration;
ImageConfiguration _configuration;
set configuration (ImageConfiguration newConfiguration) {
assert(newConfiguration != null);
if (newConfiguration == _configuration)
return;
_configuration = newConfiguration;
markNeedsPaint();
}
@override
void detach() {
_removeListenerIfNeeded();
_painter?.dispose();
_painter = null;
super.detach();
}
......@@ -1170,12 +1170,13 @@ class RenderDecoratedBox extends RenderProxyBox {
void paint(PaintingContext context, Offset offset) {
assert(size.width != null);
assert(size.height != null);
_painter ??= _decoration.createBoxPainter();
_painter ??= _decoration.createBoxPainter(markNeedsPaint);
final ImageConfiguration filledConfiguration = configuration.copyWith(size: size);
if (position == DecorationPosition.background)
_painter.paint(context.canvas, offset & size);
_painter.paint(context.canvas, offset, filledConfiguration);
super.paint(context, offset);
if (position == DecorationPosition.foreground)
_painter.paint(context.canvas, offset & size);
_painter.paint(context.canvas, offset, filledConfiguration);
}
@override
......@@ -1183,6 +1184,7 @@ class RenderDecoratedBox extends RenderProxyBox {
super.debugFillDescription(description);
description.add('decoration:');
description.addAll(_decoration.toString(" ").split('\n'));
description.add('configuration: $configuration');
}
}
......@@ -1466,11 +1468,11 @@ abstract class CustomPainter {
///
/// To paint an image on a [Canvas]:
///
/// 1. Obtain an [ImageResource], for example by using the [ImageCache.load]
/// method on the [imageCache] singleton.
/// 1. Obtain an [ImageStream], for example by calling [ImageProvider.resolve]
/// on an [AssetImage] or [NetworkImage] object.
///
/// 2. Whenever the [ImageResource]'s underlying [ImageInfo] object changes
/// (see [ImageResource.addListener]), create a new instance of your custom
/// 2. Whenever the [ImageStream]'s underlying [ImageInfo] object changes
/// (see [ImageStream.addListener]), create a new instance of your custom
/// paint delegate, giving it the new [ImageInfo] object.
///
/// 3. In your delegate's [paint] method, call the [Canvas.drawImage],
......
......@@ -465,6 +465,7 @@ class RenderTable extends RenderBox {
/// * `children` must either be null or contain lists of all the same length.
/// if `children` is not null, then `rows` must be null.
/// * [defaultColumnWidth] must not be null.
/// * [configuration] must not be null (but has a default value).
RenderTable({
int columns,
int rows,
......@@ -472,6 +473,7 @@ class RenderTable extends RenderBox {
TableColumnWidth defaultColumnWidth: const FlexColumnWidth(1.0),
TableBorder border,
List<Decoration> rowDecorations,
ImageConfiguration configuration: ImageConfiguration.empty,
Decoration defaultRowDecoration,
TableCellVerticalAlignment defaultVerticalAlignment: TableCellVerticalAlignment.top,
TextBaseline textBaseline,
......@@ -481,13 +483,15 @@ class RenderTable extends RenderBox {
assert(rows == null || rows >= 0);
assert(rows == null || children == null);
assert(defaultColumnWidth != null);
assert(configuration != null);
_columns = columns ?? (children != null && children.length > 0 ? children.first.length : 0);
_rows = rows ?? 0;
_children = new List<RenderBox>()..length = _columns * _rows;
_columnWidths = columnWidths ?? new HashMap<int, TableColumnWidth>();
_defaultColumnWidth = defaultColumnWidth;
_border = border;
this.rowDecorations = rowDecorations; // must use setter to initialize box painters
this.rowDecorations = rowDecorations; // must use setter to initialize box painters array
_configuration = configuration;
_defaultVerticalAlignment = defaultVerticalAlignment;
_textBaseline = textBaseline;
if (children != null) {
......@@ -619,30 +623,25 @@ class RenderTable extends RenderBox {
set rowDecorations(List<Decoration> value) {
if (_rowDecorations == value)
return;
_removeListenersIfNeeded();
_rowDecorations = value;
_rowDecorationPainters = _rowDecorations != null ? new List<BoxPainter>(_rowDecorations.length) : null;
_addListenersIfNeeded();
}
void _removeListenersIfNeeded() {
Set<Decoration> visitedDecorations = new Set<Decoration>();
if (_rowDecorations != null && attached) {
for (Decoration decoration in _rowDecorations) {
if (decoration != null && decoration.needsListeners && visitedDecorations.add(decoration))
decoration.removeChangeListener(markNeedsPaint);
}
if (_rowDecorationPainters != null) {
for (BoxPainter painter in _rowDecorationPainters)
painter?.dispose();
}
_rowDecorationPainters = _rowDecorations != null ? new List<BoxPainter>(_rowDecorations.length) : null;
}
void _addListenersIfNeeded() {
Set<Decoration> visitedDecorations = new Set<Decoration>();
if (_rowDecorations != null && attached) {
for (Decoration decoration in _rowDecorations) {
if (decoration != null && decoration.needsListeners && visitedDecorations.add(decoration))
decoration.addChangeListener(markNeedsPaint);
}
}
/// The settings to pass to the [rowDecorations] when painting, so that they
/// can resolve images appropriately. See [ImageProvider.resolve] and
/// [BoxPainter.paint].
ImageConfiguration get configuration => _configuration;
ImageConfiguration _configuration;
set configuration (ImageConfiguration value) {
assert(value != null);
if (value == _configuration)
return;
_configuration = value;
markNeedsPaint();
}
/// How cells that do not explicitly specify a vertical alignment are aligned vertically.
......@@ -798,12 +797,15 @@ class RenderTable extends RenderBox {
super.attach(owner);
for (RenderBox child in _children)
child?.attach(owner);
_addListenersIfNeeded();
}
@override
void detach() {
_removeListenersIfNeeded();
if (_rowDecorationPainters != null) {
for (BoxPainter painter in _rowDecorationPainters)
painter?.dispose();
_rowDecorationPainters = null;
}
for (RenderBox child in _children)
child?.detach();
super.detach();
......@@ -1214,13 +1216,12 @@ class RenderTable extends RenderBox {
if (_rowDecorations.length <= y)
break;
if (_rowDecorations[y] != null) {
_rowDecorationPainters[y] ??= _rowDecorations[y].createBoxPainter();
_rowDecorationPainters[y].paint(canvas, new Rect.fromLTRB(
offset.dx,
offset.dy + _rowTops[y],
offset.dx + size.width,
offset.dy + _rowTops[y+1]
));
_rowDecorationPainters[y] ??= _rowDecorations[y].createBoxPainter(markNeedsPaint);
_rowDecorationPainters[y].paint(
canvas,
new Offset(offset.dx, offset.dy + _rowTops[y]),
configuration.copyWith(size: new Size(size.width, _rowTops[y+1] - _rowTops[y]))
);
}
}
}
......
......@@ -3,16 +3,15 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:ui' as ui;
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/http.dart' as http;
import 'package:mojo/core.dart' as core;
import 'package:mojo_services/mojo/asset_bundle/asset_bundle.mojom.dart' as mojom;
import 'image_cache.dart';
import 'image_decoder.dart';
import 'image_resource.dart';
import 'shell.dart';
/// A collection of resources used by the application.
......@@ -43,20 +42,33 @@ import 'shell.dart';
/// * [NetworkAssetBundle]
/// * [rootBundle]
abstract class AssetBundle {
/// Retrieve an image from the asset bundle.
ImageResource loadImage(String key);
/// Retrieve string from the asset bundle.
Future<String> loadString(String key);
/// Retrieve a binary resource from the asset bundle as a data stream.
Future<core.MojoDataPipeConsumer> load(String key);
/// Retrieve a string from the asset bundle.
///
/// If the `cache` argument is set to `false`, then the data will not be
/// cached, and reading the data may bypass the cache. This is useful if the
/// caller is going to be doing its own caching. (It might not be cached if
/// it's set to `true` either, that depends on the asset bundle
/// implementation.)
Future<String> loadString(String key, { bool cache: true });
/// Retrieve a string from the asset bundle, parse it with the given function,
/// and return the function's result.
///
/// Implementations may cache the result, so a particular key should only be
/// used with one parser for the lifetime of the asset bundle.
Future<dynamic> loadStructuredData(String key, dynamic parser(String value));
@override
String toString() => '$runtimeType@$hashCode()';
}
/// An [AssetBundle] that loads resources over the network.
///
/// This asset bundle does not cache any resources, though the underlying
/// network stack may implement some level of caching itself.
class NetworkAssetBundle extends AssetBundle {
/// Creates an network asset bundle that resolves asset keys as URLs relative
/// to the given base URL.
......@@ -71,52 +83,76 @@ class NetworkAssetBundle extends AssetBundle {
return await http.readDataPipe(_urlFromKey(key));
}
/// Retrieve an image from the asset bundle.
///
/// Images are cached in the [imageCache].
@override
ImageResource loadImage(String key) => imageCache.load(_urlFromKey(key));
Future<String> loadString(String key, { bool cache: true }) async {
return (await http.get(_urlFromKey(key))).body;
}
/// Retrieve a string from the asset bundle, parse it with the given function,
/// and return the function's result.
///
/// The result is not cached. The parser is run each time the resource is
/// fetched.
@override
Future<String> loadString(String key) async {
return (await http.get(_urlFromKey(key))).body;
Future<dynamic> loadStructuredData(String key, Future<dynamic> parser(String value)) async {
assert(key != null);
assert(parser != null);
return parser(await loadString(key));
}
@override
String toString() => '$runtimeType@$hashCode($_baseUrl)';
}
/// An [AssetBundle] that adds a layer of caching to an asset bundle.
/// An [AssetBundle] that permanently caches string and structured resources
/// that have been fetched.
///
/// Strings (for [loadString] and [loadStructuredData]) are decoded as UTF-8.
/// Data that is cached is cached for the lifetime of the asset bundle
/// (typically the lifetime of the application).
///
/// Binary resources (from [load]) are not cached.
abstract class CachingAssetBundle extends AssetBundle {
final Map<String, ImageResource> _imageResourceCache =
<String, ImageResource>{};
final Map<String, Future<String>> _stringCache =
<String, Future<String>>{};
/// Override to alter how images are retrieved from the underlying [AssetBundle].
///
/// For example, the resolution-aware asset bundle created by [AssetVendor]
/// overrides this function to fetch an image with the appropriate resolution.
Future<ImageInfo> fetchImage(String key) async {
return new ImageInfo(image: await decodeImageFromDataPipe(await load(key)));
}
// TODO(ianh): Replace this with an intelligent cache, see https://github.com/flutter/flutter/issues/3568
final Map<String, Future<String>> _stringCache = <String, Future<String>>{};
final Map<String, Future<dynamic>> _structuredDataCache = <String, Future<dynamic>>{};
@override
ImageResource loadImage(String key) {
return _imageResourceCache.putIfAbsent(key, () {
return new ImageResource(fetchImage(key));
});
Future<String> loadString(String key, { bool cache: true }) {
if (cache)
return _stringCache.putIfAbsent(key, () => _fetchString(key));
return _fetchString(key);
}
Future<String> _fetchString(String key) async {
core.MojoDataPipeConsumer pipe = await load(key);
ByteData data = await core.DataPipeDrainer.drainHandle(pipe);
return new String.fromCharCodes(new Uint8List.view(data.buffer));
final core.MojoDataPipeConsumer pipe = await load(key);
final ByteData data = await core.DataPipeDrainer.drainHandle(pipe);
return UTF8.decode(new Uint8List.view(data.buffer));
}
/// Retrieve a string from the asset bundle, parse it with the given function,
/// and return the function's result.
///
/// The result of parsing the string is cached (the string itself is not,
/// unless you also fetch it with [loadString]). For any given `key`, the
/// `parser` is only run the first time.
///
/// Once the value has been parsed, the future returned by this function for
/// subsequent calls will be a [SynchronousFuture], which resolves its
/// callback synchronously.
@override
Future<String> loadString(String key) {
return _stringCache.putIfAbsent(key, () => _fetchString(key));
Future<dynamic> loadStructuredData(String key, Future<dynamic> parser(String value)) {
assert(key != null);
assert(parser != null);
if (_structuredDataCache.containsKey(key))
return _structuredDataCache[key];
final Completer<dynamic> completer = new Completer<dynamic>();
_structuredDataCache[key] = completer.future;
completer.complete(loadString(key, cache: false).then/*<dynamic>*/(parser));
completer.future.then((dynamic value) {
_structuredDataCache[key] = new SynchronousFuture<dynamic>(value);
});
return completer.future;
}
}
......@@ -127,15 +163,16 @@ class MojoAssetBundle extends CachingAssetBundle {
/// Retrieves the asset bundle located at the given URL, unpacks it, and provides it contents.
factory MojoAssetBundle.fromNetwork(String relativeUrl) {
mojom.AssetBundleProxy bundle = new mojom.AssetBundleProxy.unbound();
_fetchAndUnpackBundle(relativeUrl, bundle);
final mojom.AssetBundleProxy bundle = new mojom.AssetBundleProxy.unbound();
_fetchAndUnpackBundleAsychronously(relativeUrl, bundle);
return new MojoAssetBundle(bundle);
}
static Future<Null> _fetchAndUnpackBundle(String relativeUrl, mojom.AssetBundleProxy bundle) async {
core.MojoDataPipeConsumer bundleData = await http.readDataPipe(Uri.base.resolve(relativeUrl));
mojom.AssetUnpackerProxy unpacker = shell.connectToApplicationService(
'mojo:asset_bundle', mojom.AssetUnpacker.connectToService);
static Future<Null> _fetchAndUnpackBundleAsychronously(String relativeUrl, mojom.AssetBundleProxy bundle) async {
final core.MojoDataPipeConsumer bundleData = await http.readDataPipe(Uri.base.resolve(relativeUrl));
final mojom.AssetUnpackerProxy unpacker = shell.connectToApplicationService(
'mojo:asset_bundle', mojom.AssetUnpacker.connectToService
);
unpacker.unpackZipStream(bundleData, bundle);
unpacker.close();
}
......@@ -166,11 +203,15 @@ AssetBundle _initRootBundle() {
/// Rather than using [rootBundle] directly, consider obtaining the
/// [AssetBundle] for the current [BuildContext] using [DefaultAssetBundle.of].
/// This layer of indirection lets ancestor widgets substitute a different
/// [AssetBundle] (e.g., for testing or localization) at runtime rather than
/// [AssetBundle] at runtime (e.g., for testing or localization) rather than
/// directly replying upon the [rootBundle] created at build time. For
/// convenience, the [WidgetsApp] or [MaterialApp] widget at the top of the
/// widget hierarchy configures the [DefaultAssetBundle] to be the [rootBundle].
///
/// In normal operation, the [rootBundle] is a [MojoAssetBundle], though it can
/// also end up being a [NetworkAssetBundle] in some cases (e.g. if the
/// application's resources are being served from a local HTTP server).
///
/// See also:
///
/// * [DefaultAssetBundle]
......
......@@ -2,89 +2,9 @@
// 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:ui' show hashValues, Image;
import 'package:flutter/foundation.dart';
import 'package:flutter/http.dart' as http;
import 'package:mojo/core.dart' as mojo;
import 'image_decoder.dart';
import 'image_resource.dart';
/// Implements a way to retrieve an image, for example by fetching it from the
/// network. Also used as a key in the image cache.
///
/// This is the interface implemented by objects that can be used as the
/// argument to [ImageCache.loadProvider].
///
/// The [ImageCache.load] function uses an [ImageProvider] that fetches images
/// described by URLs. One could create an [ImageProvider] that used a custom
/// protocol, e.g. a direct TCP connection to a remote host, or using a
/// screenshot API from the host platform; such an image provider would then
/// share the same cache as all the other image loading codepaths that used the
/// [imageCache].
abstract class ImageProvider { // ignore: one_member_abstracts
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const ImageProvider();
/// Subclasses must implement this method by having it asynchronously return
/// an [ImageInfo] that represents the image provided by this [ImageProvider].
Future<ImageInfo> loadImage();
/// Subclasses must implement the `==` operator so that the image cache can
/// distinguish identical requests.
@override
bool operator ==(dynamic other);
/// Subclasses must implement the `hashCode` operator so that the image cache
/// can efficiently store the providers in a map.
@override
int get hashCode;
}
class _UrlFetcher implements ImageProvider {
_UrlFetcher(this._url, this._scale);
final String _url;
final double _scale;
@override
Future<ImageInfo> loadImage() async {
try {
final Uri resolvedUrl = Uri.base.resolve(_url);
final mojo.MojoDataPipeConsumer dataPipe = await http.readDataPipe(resolvedUrl);
if (dataPipe == null)
throw 'Unable to read data from: $resolvedUrl';
final Image image = await decodeImageFromDataPipe(dataPipe);
if (image == null)
throw 'Unable to decode image data from: $resolvedUrl';
return new ImageInfo(image: image, scale: _scale);
} catch (exception, stack) {
FlutterError.reportError(new FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: 'while fetching an image for the image cache',
silent: true
));
return null;
}
}
@override
bool operator ==(dynamic other) {
if (other is! _UrlFetcher)
return false;
final _UrlFetcher typedOther = other;
return _url == typedOther._url && _scale == typedOther._scale;
}
@override
int get hashCode => hashValues(_url, _scale);
}
import 'image_stream.dart';
const int _kDefaultSize = 1000;
......@@ -93,20 +13,21 @@ const int _kDefaultSize = 1000;
/// Implements a least-recently-used cache of up to 1000 images. The maximum
/// size can be adjusted using [maximumSize]. Images that are actively in use
/// (i.e. to which the application is holding references, either via
/// [ImageResource] objects, [ImageInfo] objects, or raw [ui.Image] objects) may
/// get evicted from the cache (and thus need to be refetched from the network
/// if they are referenced in the [load] method), but the raw bits are kept in
/// memory for as long as the application is using them.
/// [ImageStream] objects, [ImageStreamCompleter] objects, [ImageInfo] objects,
/// or raw [ui.Image] objects) may get evicted from the cache (and thus need to
/// be refetched from the network if they are referenced in the [putIfAbsent]
/// method), but the raw bits are kept in memory for as long as the application
/// is using them.
///
/// The [load] method fetches the image with the given URL and scale.
/// The [putIfAbsent] method is the main entry-point to the cache API. It
/// returns the previously cached [ImageStreamCompleter] for the given key, if
/// available; if not, it calls the given callback to obtain it first. In either
/// case, the key is moved to the "most recently used" position.
///
/// For more complicated use cases, the [loadProvider] method can be used with a
/// custom [ImageProvider].
/// Generally this class is not used directly. The [ImageProvider] class and its
/// subclasses automatically handle the caching of images.
class ImageCache {
ImageCache._();
final LinkedHashMap<ImageProvider, ImageResource> _cache =
new LinkedHashMap<ImageProvider, ImageResource>();
final LinkedHashMap<Object, ImageStreamCompleter> _cache = new LinkedHashMap<Object, ImageStreamCompleter>();
/// Maximum number of entries to store in the cache.
///
......@@ -134,53 +55,35 @@ class ImageCache {
}
}
/// Calls the [ImageProvider.loadImage] method on the given image provider, if
/// necessary, and returns an [ImageResource] that encapsulates a [Future] for
/// the given image.
/// Returns the previously cached [ImageStream] for the given key, if available;
/// if not, calls the given callback to obtain it first. In either case, the
/// key is moved to the "most recently used" position.
///
/// If the given [ImageProvider] has already been used and is still in the
/// cache, then the [ImageResource] object is immediately usable and the
/// provider is not called.
ImageResource loadProvider(ImageProvider provider) {
assert(provider != null);
ImageResource result = _cache[provider];
/// The arguments cannot be null. The `loader` cannot return null.
ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader()) {
assert(key != null);
assert(loader != null);
ImageStreamCompleter result = _cache[key];
if (result != null) {
_cache.remove(provider);
// Remove the provider from the list so that we can put it back in below
// and thus move it to the end of the list.
_cache.remove(key);
} else {
if (_cache.length == maximumSize && maximumSize > 0)
_cache.remove(_cache.keys.first);
result = new ImageResource(provider.loadImage());;
result = loader();
}
if (maximumSize > 0) {
assert(_cache.length < maximumSize);
_cache[provider] = result;
_cache[key] = result;
}
assert(_cache.length <= maximumSize);
return result;
}
/// Fetches the given URL, associating it with the given scale.
///
/// The return value is an [ImageResource], which encapsulates a [Future] for
/// the given image.
///
/// If the given URL has already been fetched for the given scale, and it is
/// still in the cache, then the [ImageResource] object is immediately usable.
ImageResource load(String url, { double scale: 1.0 }) {
assert(url != null);
assert(scale != null);
return loadProvider(new _UrlFetcher(url, scale));
}
}
/// The singleton that implements the Flutter framework's image cache.
///
/// The simplest use of this object is as follows:
///
/// ```dart
/// imageCache.load(myImageUrl).first.then(myImageHandler);
/// ```
///
/// ...where `myImageHandler` is a function with one argument, an [ImageInfo]
/// object.
final ImageCache imageCache = new ImageCache._();
/// The cache is used internally by [ImageProvider] and should generally not be
/// accessed directly.
final ImageCache imageCache = new ImageCache();
......@@ -14,6 +14,9 @@ import 'package:mojo/core.dart' show MojoDataPipeConsumer;
/// in the data pipe as an image. If successful, the returned [Future] resolves
/// to the decoded image. Otherwise, the [Future] resolves to [null].
Future<ui.Image> decodeImageFromDataPipe(MojoDataPipeConsumer consumerHandle) {
assert(consumerHandle != null);
assert(consumerHandle.handle != null);
assert(consumerHandle.handle.h != null);
Completer<ui.Image> completer = new Completer<ui.Image>();
ui.decodeImageFromDataPipe(consumerHandle.handle.h, (ui.Image image) {
completer.complete(image);
......
This diff is collapsed.
// Copyright 2016 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:collection';
import 'dart:convert';
import 'dart:ui' show hashValues;
import 'package:flutter/foundation.dart';
import 'asset_bundle.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
/// ```
class AssetImage extends AssetBundleImageProvider {
/// Creates an object that fetches an image from an asset bundle.
///
/// The [name] argument must not be null. It should name the main asset from
/// the set of images to chose from.
AssetImage(this.name, {
this.bundle
}) {
assert(name != null);
}
/// The name of the main asset from the set of images to chose from. See the
/// documentation for the [AssetImage] class itself for details.
final String name;
/// 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 [name].
final AssetBundle bundle;
// 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(_kAssetManifestFileName, _manifestParser).then(
(Map<String, List<String>> manifest) {
final String chosenName = _chooseVariant(
name,
configuration,
manifest == null ? null : manifest[name]
);
final double chosenScale = _parseScale(chosenName);
final AssetBundleImageKey key = new 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
// function 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 = new 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 = new Completer<AssetBundleImageKey>();
return completer.future;
}
static Future<Map<String, List<String>>> _manifestParser(String json) {
if (json == null)
return null;
// TODO(ianh): JSON decoding really shouldn't be on the main thread.
final Map<dynamic, dynamic> parsedManifest = JSON.decode(json);
// TODO(ianh): convert that data structure to the right types.
return new SynchronousFuture<Map<dynamic, dynamic>>(parsedManifest);
}
String _chooseVariant(String main, ImageConfiguration config, List<String> candidates) {
if (candidates == null || candidates.isEmpty)
return main;
// TODO(ianh): Consider moving this parsing logic into _manifestParser.
final SplayTreeMap<double, String> mapping = new SplayTreeMap<double, String>();
for (String candidate in candidates)
mapping[_parseScale(candidate)] = candidate;
mapping[_naturalResolution] = main;
// TODO(ianh): implement support for config.locale, config.size, config.platform
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];
double lower = candidates.lastKeyBefore(value);
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 = new RegExp(r"/?(\d+(\.\d*)?)x/");
double _parseScale(String key) {
Match match = _extractRatioRegExp.firstMatch(key);
if (match != null && match.groupCount > 0)
return double.parse(match.group(1));
return _naturalResolution;
}
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType)
return false;
final AssetImage typedOther = other;
return name == typedOther.name
&& bundle == typedOther.bundle;
}
@override
int get hashCode => hashValues(name, bundle);
@override
String toString() => '$runtimeType(bundle: $bundle, name: $name)';
}
......@@ -5,11 +5,13 @@
import 'dart:async';
import 'dart:ui' as ui show Image;
import 'package:meta/meta.dart';
import 'package:flutter/foundation.dart';
/// A [ui.Image] object with its corresponding scale.
///
/// ImageInfo objects are used by [ImageResource] objects to represent the
/// ImageInfo objects are used by [ImageStream] objects to represent the
/// actual data of the image once it has been obtained.
class ImageInfo {
/// Creates an [ImageInfo] object for the given image and scale.
......@@ -44,52 +46,119 @@ class ImageInfo {
/// Signature for callbacks reporting that an image is available.
///
/// Used by [ImageResource].
/// Used by [ImageStream].
typedef void ImageListener(ImageInfo image);
/// A handle to an image resource.
///
/// ImageResource represents a handle to a [ui.Image] object and its scale
/// ImageStream represents a handle to a [ui.Image] object and its scale
/// (together represented by an [ImageInfo] object). The underlying image object
/// might change over time, either because the image is animating or because the
/// underlying image resource was mutated.
///
/// ImageResource objects can also represent an image that hasn't finished
/// ImageStream objects can also represent an image that hasn't finished
/// loading.
class ImageResource {
/// Creates an image resource.
///
/// ImageStream objects are backed by [ImageStreamCompleter] objects.
class ImageStream {
/// Create an initially unbound image stream.
///
/// The image resource awaits the given [Future]. When the future resolves,
/// it notifies the [ImageListener]s that have been registered with
/// [addListener].
ImageResource(this._futureImage) {
_futureImage.then(
_handleImageLoaded,
onError: (dynamic exception, dynamic stack) {
_handleImageError('while loading an image', exception, stack);
/// Once an [ImageStreamCompleter] is available, call [setCompleter].
ImageStream();
/// The completer that has been assigned to this image stream.
///
/// Generally there is no need to deal with the completer directly.
ImageStreamCompleter get completer => _completer;
ImageStreamCompleter _completer;
List<ImageListener> _listeners;
/// Assigns a particular [ImageStreamCompleter] to this [ImageStream].
///
/// This is usually done automatically by the [ImageProvider] that created the
/// [ImageStream].
void setCompleter(ImageStreamCompleter value) {
assert(_completer == null);
_completer = value;
if (_listeners != null) {
final List<ImageListener> initialListeners = _listeners;
_listeners = null;
initialListeners.forEach(_completer.addListener);
}
);
}
bool _resolved = false;
Future<ImageInfo> _futureImage;
ImageInfo _image;
final List<ImageListener> _listeners = new List<ImageListener>();
/// Adds a listener callback that is called whenever a concrete [ImageInfo]
/// object is available. If a concrete image is already available, this object
/// will call the listener synchronously.
void addListener(ImageListener listener) {
if (_completer != null)
return _completer.addListener(listener);
_listeners ??= <ImageListener>[];
_listeners.add(listener);
}
/// Stop listening for new concrete [ImageInfo] objects.
void removeListener(ImageListener listener) {
if (_completer != null)
return _completer.removeListener(listener);
assert(_listeners != null);
_listeners.remove(listener);
}
/// The first concrete [ImageInfo] object represented by this handle.
/// Returns an object which can be used with `==` to determine if this
/// [ImageStream] shares the same listeners list as another [ImageStream].
///
/// This can be used to avoid unregistering and reregistering listeners after
/// calling [ImageProvider.resolve] on a new, but possibly equivalent,
/// [ImageProvider].
///
/// Instead of receiving only the first image, most clients will want to
/// [addListener] to be notified whenever a a concrete image is available.
Future<ImageInfo> get first => _futureImage;
/// The key may change once in the lifetime of the object. When it changes, it
/// will go from being different than other [ImageStream]'s keys to
/// potentially being the same as others'. No notification is sent when this
/// happens.
Object get key => _completer != null ? _completer : this;
@override
String toString() {
StringBuffer result = new StringBuffer();
result.write('$runtimeType(');
if (_completer == null) {
result.write('unresolved; ');
if (_listeners != null) {
result.write('${_listeners.length} listener${_listeners.length == 1 ? "" : "s" }');
} else {
result.write('no listeners');
}
} else {
result.write('${_completer.runtimeType}; ');
final List<String> description = <String>[];
_completer._debugFillDescription(description);
result.write(description.join('; '));
}
result.write(')');
return result.toString();
}
}
/// Base class for those that manage the loading of [ui.Image] objects for
/// [ImageStream]s.
///
/// This class is rarely used directly. Generally, an [ImageProvider] subclass
/// will return an [ImageStream] and automatically configure it with the right
/// [ImageStreamCompleter] when possible.
class ImageStreamCompleter {
final List<ImageListener> _listeners = <ImageListener>[];
ImageInfo _current;
/// Adds a listener callback that is called whenever a concrete [ImageInfo]
/// object is available. If a concrete image is already available, this object
/// will call the listener synchronously.
void addListener(ImageListener listener) {
_listeners.add(listener);
if (_resolved) {
if (_current != null) {
try {
listener(_image);
listener(_current);
} catch (exception, stack) {
_handleImageError('by a synchronously-called image listener', exception, stack);
}
......@@ -101,18 +170,16 @@ class ImageResource {
_listeners.remove(listener);
}
void _handleImageLoaded(ImageInfo image) {
_image = image;
_resolved = true;
_notifyListeners();
}
void _notifyListeners() {
assert(_resolved);
/// Calls all the registered listeners to notify them of a new image.
@protected
void setImage(ImageInfo image) {
_current = image;
if (_listeners.isEmpty)
return;
List<ImageListener> localListeners = new List<ImageListener>.from(_listeners);
for (ImageListener listener in localListeners) {
try {
listener(_image);
listener(image);
} catch (exception, stack) {
_handleImageError('by an image listener', exception, stack);
}
......@@ -130,14 +197,37 @@ class ImageResource {
@override
String toString() {
StringBuffer result = new StringBuffer();
result.write('$runtimeType(');
if (!_resolved)
result.write('unresolved');
final List<String> description = <String>[];
debugFillDescription(description);
return '$runtimeType(${description.join("; ")})';
}
/// Accumulates a list of strings describing the object's state. Subclasses
/// should override this to have their information included in [toString].
@protected
@mustCallSuper
void debugFillDescription(List<String> description) {
if (_current == null)
description.add('unresolved');
else
result.write('$_image');
result.write('; ${_listeners.length} listener${_listeners.length == 1 ? "" : "s" }');
result.write(')');
return result.toString();
description.add('$_current');
description.add('${_listeners.length} listener${_listeners.length == 1 ? "" : "s" }');
}
// TODO(ianh): remove once @protected allows in-file references
void _debugFillDescription(List<String> description) => debugFillDescription(description);
}
/// Manages the loading of [ui.Image] objects for static [ImageStream]s (those
/// with only one frame).
class OneFrameImageStreamCompleter extends ImageStreamCompleter {
/// Creates a manager for one-frame [ImageStream]s.
///
/// The image resource awaits the given [Future]. When the future resolves,
/// it notifies the [ImageListener]s that have been registered with
/// [addListener].
OneFrameImageStreamCompleter(Future<ImageInfo> image) {
assert(image != null);
image.then(setImage);
}
}
......@@ -6,13 +6,12 @@ import 'dart:async';
import 'dart:ui' as ui show window;
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:meta/meta.dart';
import 'asset_vendor.dart';
import 'banner.dart';
import 'basic.dart';
import 'binding.dart';
import 'container.dart';
import 'framework.dart';
import 'locale_query.dart';
import 'media_query.dart';
......@@ -28,8 +27,8 @@ typedef Future<LocaleQueryData> LocaleChangedCallback(Locale locale);
/// required for an application.
///
/// See also: [CheckedModeBanner], [DefaultTextStyle], [MediaQuery],
/// [LocaleQuery], [AssetVendor], [Title], [Navigator], [Overlay],
/// [SemanticsDebugger] (the widgets wrapped by this one).
/// [LocaleQuery], [Title], [Navigator], [Overlay], [SemanticsDebugger] (the
/// widgets wrapped by this one).
///
/// The [onGenerateRoute] argument is required, and corresponds to
/// [Navigator.onGenerateRoute].
......@@ -179,9 +178,6 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
data: new MediaQueryData.fromWindow(ui.window),
child: new LocaleQuery(
data: _localeData,
child: new AssetVendor(
bundle: rootBundle,
devicePixelRatio: ui.window.devicePixelRatio,
child: new Title(
title: config.title,
brightness: config.brightness,
......@@ -194,7 +190,6 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
)
)
)
)
);
if (config.textStyle != null) {
new DefaultTextStyle(
......
// Copyright 2016 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:collection';
import 'dart:convert';
import 'dart:ui' as ui show Image;
import 'package:flutter/services.dart';
import 'package:meta/meta.dart';
import 'package:mojo/core.dart' as core;
import 'media_query.dart';
import 'basic.dart';
import 'framework.dart';
// Base class for asset resolvers.
abstract class _AssetResolver { // ignore: one_member_abstracts
// Return a resolved asset key for the asset named [name].
Future<String> resolve(String name);
}
// Asset bundle capable of producing assets via the resolution logic of an
// asset resolver.
//
// Wraps an underlying [AssetBundle] and forwards calls after resolving the
// asset key.
class _ResolvingAssetBundle extends CachingAssetBundle {
_ResolvingAssetBundle({ this.bundle, this.resolver }) {
assert(bundle != null);
assert(resolver != null);
}
final AssetBundle bundle;
final _AssetResolver resolver;
final Map<String, String> keyCache = <String, String>{};
@override
Future<core.MojoDataPipeConsumer> load(String key) async {
if (!keyCache.containsKey(key))
keyCache[key] = await resolver.resolve(key);
return await bundle.load(keyCache[key]);
}
}
/// Abstraction for reading images out of a Mojo data pipe.
///
/// Useful for mocking purposes in unit tests.
typedef Future<ui.Image> ImageDecoder(core.MojoDataPipeConsumer pipe);
// Asset bundle that understands how specific asset keys represent image scale.
class _ResolutionAwareAssetBundle extends _ResolvingAssetBundle {
_ResolutionAwareAssetBundle({
AssetBundle bundle,
_ResolutionAwareAssetResolver resolver,
ImageDecoder imageDecoder
}) : _imageDecoder = imageDecoder,
super(
bundle: bundle,
resolver: resolver
);
@override
_ResolutionAwareAssetResolver get resolver => super.resolver;
final ImageDecoder _imageDecoder;
@override
Future<ImageInfo> fetchImage(String key) async {
core.MojoDataPipeConsumer pipe = await load(key);
// At this point the key should be in our key cache, and the image
// resource should be in our image cache
double scale = resolver.getScale(keyCache[key]);
return new ImageInfo(
image: await _imageDecoder(pipe),
scale: scale
);
}
}
// Base class for resolvers that use the asset manifest to retrieve a list
// of asset variants to choose from.
abstract class _VariantAssetResolver extends _AssetResolver {
_VariantAssetResolver({ this.bundle });
final AssetBundle bundle;
// TODO(kgiesing): Ideally, this cache would be on an object with the same
// lifetime as the asset bundle it wraps. However, that won't matter until we
// need to change AssetVendors frequently; as of this writing we only have
// one.
Map<String, List<String>> _assetManifest;
Future<Null> _initializer;
Future<Null> _loadManifest() async {
String json = await bundle.loadString("AssetManifest.json");
_assetManifest = JSON.decode(json);
}
@override
Future<String> resolve(String name) async {
_initializer ??= _loadManifest();
await _initializer;
// If there's no asset manifest, just return the main asset
if (_assetManifest == null)
return name;
// Allow references directly to variants: if the supplied name is not a
// key, just return it
List<String> variants = _assetManifest[name];
if (variants == null)
return name;
else
return chooseVariant(name, variants);
}
String chooseVariant(String main, List<String> variants);
}
// Asset resolver that understands how to determine the best match for the
// current device pixel ratio
class _ResolutionAwareAssetResolver extends _VariantAssetResolver {
_ResolutionAwareAssetResolver({ AssetBundle bundle, this.devicePixelRatio })
: super(bundle: bundle);
final double devicePixelRatio;
// We assume the main asset is designed for a device pixel ratio of 1.0
static const double _naturalResolution = 1.0;
static final RegExp _extractRatioRegExp = new RegExp(r"/?(\d+(\.\d*)?)x/");
double getScale(String key) {
Match match = _extractRatioRegExp.firstMatch(key);
if (match != null && match.groupCount > 0)
return double.parse(match.group(1));
return 1.0;
}
// 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];
double lower = candidates.lastKeyBefore(value);
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];
}
@override
String chooseVariant(String main, List<String> candidates) {
SplayTreeMap<double, String> mapping = new SplayTreeMap<double, String>();
for (String candidate in candidates)
mapping[getScale(candidate)] = candidate;
mapping[_naturalResolution] = main;
return _findNearest(mapping, devicePixelRatio);
}
}
/// Establishes an asset resolution strategy for its descendants.
///
/// Given a main asset and a set of variants, AssetVendor chooses the most
/// appropriate asset for the current context. The current asset resolution
/// strategy knows how to find the asset most closely matching the current
/// device pixel ratio - see [MediaQuery].
///
/// 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
/// ```
class AssetVendor extends StatefulWidget {
/// Creates a widget that establishes an asset resolution strategy for its descendants.
AssetVendor({
Key key,
@required this.bundle,
this.devicePixelRatio,
this.imageDecoder: decodeImageFromDataPipe,
this.child
}) : super(key: key) {
assert(bundle != null);
}
/// The bundle from which to load the assets.
final AssetBundle bundle;
/// If non-null, the device pixel ratio to assume when selecting assets.
final double devicePixelRatio;
/// The function to use for decoding images.
final ImageDecoder imageDecoder;
/// The widget below this widget in the tree.
final Widget child;
@override
_AssetVendorState createState() => new _AssetVendorState();
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('bundle: $bundle');
if (devicePixelRatio != null)
description.add('devicePixelRatio: $devicePixelRatio');
}
}
class _AssetVendorState extends State<AssetVendor> {
_ResolvingAssetBundle _bundle;
void _initBundle() {
_bundle = new _ResolutionAwareAssetBundle(
bundle: config.bundle,
imageDecoder: config.imageDecoder,
resolver: new _ResolutionAwareAssetResolver(
bundle: config.bundle,
devicePixelRatio: config.devicePixelRatio
)
);
}
@override
void initState() {
super.initState();
_initBundle();
}
@override
void didUpdateConfig(AssetVendor oldConfig) {
if (config.bundle != oldConfig.bundle ||
config.devicePixelRatio != oldConfig.devicePixelRatio) {
_initBundle();
}
}
@override
Widget build(BuildContext context) {
return new DefaultAssetBundle(bundle: _bundle, child: config.child);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('bundle: $_bundle');
}
}
This diff is collapsed.
// Copyright 2016 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 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart';
import 'package:meta/meta.dart';
import 'basic.dart';
import 'framework.dart';
import 'image.dart';
/// A widget that paints a [Decoration] either before or after its child paints.
///
/// [Container] insets its child by the widths of the borders; this widget does
/// not.
///
/// Commonly used with [BoxDecoration].
class DecoratedBox extends SingleChildRenderObjectWidget {
/// Creates a widget that paints a [Decoration].
///
/// The [decoration] and [position] arguments must not be null. By default the
/// decoration paints behind the child.
DecoratedBox({
Key key,
@required this.decoration,
this.position: DecorationPosition.background,
Widget child
}) : super(key: key, child: child) {
assert(decoration != null);
assert(position != null);
}
/// What decoration to paint.
///
/// Commonly a [BoxDecoration].
final Decoration decoration;
/// Whether to paint the box decoration behind or in front of the child.
final DecorationPosition position;
@override
RenderDecoratedBox createRenderObject(BuildContext context) {
return new RenderDecoratedBox(
decoration: decoration,
position: position,
configuration: createLocalImageConfiguration(context)
);
}
@override
void updateRenderObject(BuildContext context, RenderDecoratedBox renderObject) {
renderObject
..decoration = decoration
..configuration = createLocalImageConfiguration(context)
..position = position;
}
}
/// A convenience widget that combines common painting, positioning, and sizing
/// widgets.
///
/// A container first surrounds the child with [padding] (inflated by any
/// borders present in the [decoration]) and then applies additional
/// [constraints] to the padded extent (incorporating the [width] and [height]
/// as constraints, if either is non-null). The container is then surrounded by
/// additional empty space described from the [margin].
///
/// During painting, the container first applies the given [transform], then
/// paints the [decoration] to fill the padded extent, then it paints the child,
/// and finally paints the [foregroundDecoration], also filling the padded
/// extent.
class Container extends StatelessWidget {
/// Creates a widget that combines common painting, positioning, and sizing widgets.
Container({
Key key,
this.padding,
this.decoration,
this.foregroundDecoration,
double width,
double height,
BoxConstraints constraints,
this.margin,
this.transform,
this.child
}) : constraints =
(width != null || height != null)
? constraints?.tighten(width: width, height: height)
?? new BoxConstraints.tightFor(width: width, height: height)
: constraints,
super(key: key) {
assert(margin == null || margin.isNonNegative);
assert(padding == null || padding.isNonNegative);
assert(decoration == null || decoration.debugAssertValid());
}
/// The child contained by the container.
///
/// If null, the container will expand to fill all available space in its parent.
final Widget child;
/// Empty space to inscribe inside the decoration.
final EdgeInsets padding;
/// The decoration to paint behind the child.
final Decoration decoration;
/// The decoration to paint in front of the child.
final Decoration foregroundDecoration;
/// Additional constraints to apply to the child.
final BoxConstraints constraints;
/// Empty space to surround the decoration.
final EdgeInsets margin;
/// The transformation matrix to apply before painting the container.
final Matrix4 transform;
EdgeInsets get _paddingIncludingDecoration {
if (decoration == null || decoration.padding == null)
return padding;
EdgeInsets decorationPadding = decoration.padding;
if (padding == null)
return decorationPadding;
return padding + decorationPadding;
}
@override
Widget build(BuildContext context) {
Widget current = child;
if (child == null && (constraints == null || !constraints.isTight))
current = new ConstrainedBox(constraints: const BoxConstraints.expand());
EdgeInsets effectivePadding = _paddingIncludingDecoration;
if (effectivePadding != null)
current = new Padding(padding: effectivePadding, child: current);
if (decoration != null)
current = new DecoratedBox(decoration: decoration, child: current);
if (foregroundDecoration != null) {
current = new DecoratedBox(
decoration: foregroundDecoration,
position: DecorationPosition.foreground,
child: current
);
}
if (constraints != null)
current = new ConstrainedBox(constraints: constraints, child: current);
if (margin != null)
current = new Padding(padding: margin, child: current);
if (transform != null)
current = new Transform(transform: transform, child: current);
return current;
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (constraints != null)
description.add('$constraints');
if (decoration != null)
description.add('bg: $decoration');
if (foregroundDecoration != null)
description.add('fg: $foregroundDecoration');
if (margin != null)
description.add('margin: $margin');
if (padding != null)
description.add('padding: $padding');
if (transform != null)
description.add('has transform');
}
}
// Copyright 2015 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:io' show Platform;
import 'package:meta/meta.dart';
import 'package:flutter/services.dart';
import 'basic.dart';
import 'framework.dart';
import 'media_query.dart';
/// Creates an [ImageConfiguration] based on the given [BuildContext] (and
/// optionally size).
///
/// This is the object that must be passed to [BoxPainter.paint] and to
/// [ImageProvider.resolve].
ImageConfiguration createLocalImageConfiguration(BuildContext context, { Size size }) {
return new ImageConfiguration(
bundle: DefaultAssetBundle.of(context),
devicePixelRatio: MediaQuery.of(context).devicePixelRatio,
// TODO(ianh): provide the locale,
size: size,
platform: Platform.operatingSystem
);
}
/// A widget that displays an image.
///
/// Several constructors are provided for the various ways that an image can be
/// specified:
///
/// * [new Image], for obtaining an image from an [ImageProvider].
/// * [new Image.fromNetwork], for obtaining an image from a URL.
/// * [new Image.fromAssetBundle], for obtaining an image from an [AssetBundle]
/// using a key.
///
/// To automatically perform pixel-density-aware asset resolution, specify the
/// image using an [AssetImage] and make sure that a [MaterialApp], [WidgetsApp],
/// or [MediaQuery] widget exists above the [Image] widget in the widget tree.
///
/// The image is painted using [paintImage], which describes the meanings of the
/// various fields on this class in more detail.
class Image extends StatefulWidget {
/// Creates a widget that displays an image.
///
/// To show an image from the network or from an asset bundle, consider using
/// [new Image.fromNetwork] and [new Image.fromAssetBundle] respectively.
///
/// The [image] and [repeat] arguments must not be null.
Image({
Key key,
@required this.image,
this.width,
this.height,
this.color,
this.fit,
this.alignment,
this.repeat: ImageRepeat.noRepeat,
this.centerSlice,
this.gaplessPlayback: false
}) : super(key: key) {
assert(image != null);
}
/// Creates a widget that displays an [ImageStream] obtained from the network.
///
/// The [src], [scale], and [repeat] arguments must not be null.
Image.fromNetwork({
Key key,
@required String src,
double scale: 1.0,
this.width,
this.height,
this.color,
this.fit,
this.alignment,
this.repeat: ImageRepeat.noRepeat,
this.centerSlice,
this.gaplessPlayback: false
}) : image = new NetworkImage(src, scale: scale),
super(key: key);
/// Creates a widget that displays an [ImageStream] obtained from an asset
/// bundle. The key for the image is given by the `name` argument.
///
/// If the `bundle` argument is omitted or null, then the
/// [DefaultAssetBundle] will be used.
///
/// If the `scale` argument is omitted or null, then pixel-density-aware asset
/// resolution will be attempted.
///
/// If [width] and [height] are both specified, and [scale] is not, then
/// size-aware asset resolution will be attempted also.
///
/// The [name] and [repeat] arguments must not be null.
Image.fromAssetBundle({
Key key,
AssetBundle bundle,
@required String name,
double scale,
this.width,
this.height,
this.color,
this.fit,
this.alignment,
this.repeat: ImageRepeat.noRepeat,
this.centerSlice,
this.gaplessPlayback: false
}) : image = scale != null ? new ExactAssetImage(name, bundle: bundle, scale: scale)
: new AssetImage(name, bundle: bundle),
super(key: key);
/// The image to display.
final ImageProvider image;
/// If non-null, require the image to have this width.
///
/// If null, the image will pick a size that best preserves its intrinsic
/// aspect ratio.
final double width;
/// If non-null, require the image to have this height.
///
/// If null, the image will pick a size that best preserves its intrinsic
/// aspect ratio.
final double height;
/// If non-null, apply this color filter to the image before painting.
final Color color;
/// How to inscribe the image into the place allocated during layout.
///
/// The default varies based on the other fields. See the discussion at
/// [paintImage].
final ImageFit fit;
/// How to align the image within its bounds.
///
/// An alignment of (0.0, 0.0) aligns the image to the top-left corner of its
/// layout bounds. An alignment of (1.0, 0.5) aligns the image to the middle
/// of the right edge of its layout bounds.
final FractionalOffset alignment;
/// How to paint any portions of the layout bounds not covered by the image.
final ImageRepeat repeat;
/// The center slice for a nine-patch image.
///
/// The region of the image inside the center slice will be stretched both
/// horizontally and vertically to fit the image into its destination. The
/// region of the image above and below the center slice will be stretched
/// only horizontally and the region of the image to the left and right of
/// the center slice will be stretched only vertically.
final Rect centerSlice;
/// Whether to continue showing the old image (true), or briefly show nothing
/// (false), when the image provider changes.
// TODO(ianh): Find a better name.
final bool gaplessPlayback;
@override
_ImageState createState() => new _ImageState();
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('image: $image');
if (width != null)
description.add('width: $width');
if (height != null)
description.add('height: $height');
if (color != null)
description.add('color: $color');
if (fit != null)
description.add('fit: $fit');
if (alignment != null)
description.add('alignment: $alignment');
if (repeat != ImageRepeat.noRepeat)
description.add('repeat: $repeat');
if (centerSlice != null)
description.add('centerSlice: $centerSlice');
}
}
class _ImageState extends State<Image> {
ImageStream _imageStream;
ImageInfo _imageInfo;
@override
void initState() {
super.initState();
_resolveImage();
}
@override
void didUpdateConfig(Image oldConfig) {
if (config.image != oldConfig.image)
_resolveImage();
}
@override
void dependenciesChanged() {
_resolveImage();
super.dependenciesChanged();
}
@override
void dispose() {
_imageStream.removeListener(_handleImageChanged);
super.dispose();
}
void _resolveImage() {
final ImageStream oldImageStream = _imageStream;
_imageStream = config.image.resolve(createLocalImageConfiguration(
context,
size: config.width != null && config.height != null ? new Size(config.width, config.height) : null
));
assert(_imageStream != null);
if (_imageStream.key != oldImageStream?.key) {
oldImageStream?.removeListener(_handleImageChanged);
if (!config.gaplessPlayback)
setState(() { _imageInfo = null; });
_imageStream.addListener(_handleImageChanged);
}
}
void _handleImageChanged(ImageInfo imageInfo) {
setState(() {
_imageInfo = imageInfo;
});
}
@override
Widget build(BuildContext context) {
return new RawImage(
image: _imageInfo?.image,
width: config.width,
height: config.height,
scale: _imageInfo?.scale ?? 1.0,
color: config.color,
fit: config.fit,
alignment: config.alignment,
repeat: config.repeat,
centerSlice: config.centerSlice
);
}
}
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'basic.dart';
import 'container.dart';
import 'framework.dart';
import 'package:meta/meta.dart';
......
......@@ -7,6 +7,7 @@ import 'dart:async';
import 'package:flutter/rendering.dart' show RenderStack;
import 'basic.dart';
import 'container.dart';
import 'framework.dart';
import 'overlay.dart';
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'basic.dart';
import 'container.dart';
import 'framework.dart';
import 'gesture_detector.dart';
import 'navigator.dart';
......
......@@ -9,6 +9,7 @@ import 'package:meta/meta.dart';
import 'debug.dart';
import 'framework.dart';
import 'image.dart';
export 'package:flutter/rendering.dart' show
FixedColumnWidth,
......@@ -145,6 +146,7 @@ class Table extends RenderObjectWidget {
defaultColumnWidth: defaultColumnWidth,
border: border,
rowDecorations: _rowDecorations,
configuration: createLocalImageConfiguration(context),
defaultVerticalAlignment: defaultVerticalAlignment,
textBaseline: textBaseline
);
......@@ -159,6 +161,7 @@ class Table extends RenderObjectWidget {
..defaultColumnWidth = defaultColumnWidth
..border = border
..rowDecorations = _rowDecorations
..configuration = createLocalImageConfiguration(context)
..defaultVerticalAlignment = defaultVerticalAlignment
..textBaseline = textBaseline;
}
......
......@@ -5,6 +5,7 @@
import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'container.dart';
import 'editable.dart';
import 'framework.dart';
import 'gesture_detector.dart';
......
......@@ -9,13 +9,13 @@
library widgets;
export 'src/widgets/app.dart';
export 'src/widgets/asset_vendor.dart';
export 'src/widgets/auto_layout.dart';
export 'src/widgets/banner.dart';
export 'src/widgets/basic.dart';
export 'src/widgets/binding.dart';
export 'src/widgets/child_view.dart';
export 'src/widgets/clamp_overscrolls.dart';
export 'src/widgets/container.dart';
export 'src/widgets/debug.dart';
export 'src/widgets/dismissable.dart';
export 'src/widgets/drag_target.dart';
......@@ -26,6 +26,7 @@ export 'src/widgets/framework.dart';
export 'src/widgets/gesture_detector.dart';
export 'src/widgets/gridpaper.dart';
export 'src/widgets/heroes.dart';
export 'src/widgets/image.dart';
export 'src/widgets/implicit_animations.dart';
export 'src/widgets/layout_builder.dart';
export 'src/widgets/lazy_block.dart';
......
......@@ -3,27 +3,43 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
class TestBoxPainter extends BoxPainter {
TestBoxPainter(VoidCallback onChanged): super(onChanged);
@override
void paint(Canvas canvas, Rect rect) { }
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { }
}
class TestDecoration extends Decoration {
final List<VoidCallback> listeners = <VoidCallback>[];
@override
bool get needsListeners => true;
int listeners = 0;
@override
void addChangeListener(VoidCallback listener) { listeners.add(listener); }
Decoration lerpFrom(Decoration a, double t) {
if (t == 0.0)
return a;
if (t == 1.0)
return this;
return new TestDecoration();
}
@override
void removeChangeListener(VoidCallback listener) { listeners.remove(listener); }
Decoration lerpTo(Decoration b, double t) {
if (t == 1.0)
return b;
if (t == 0.0)
return this;
return new TestDecoration();
}
@override
BoxPainter createBoxPainter() => new TestBoxPainter();
BoxPainter createBoxPainter([VoidCallback onChanged]) {
if (onChanged != null)
listeners += 1;
return new TestBoxPainter(onChanged);
}
}
void main() {
......@@ -56,15 +72,15 @@ void main() {
expect(value, isTrue);
});
testWidgets('Switch listens to decorations', (WidgetTester tester) async {
testWidgets('Switch listens to the decorations it paints', (WidgetTester tester) async {
TestDecoration activeDecoration = new TestDecoration();
TestDecoration inactiveDecoration = new TestDecoration();
Widget build(TestDecoration activeDecoration, TestDecoration inactiveDecoration) {
Widget build(bool active, TestDecoration activeDecoration, TestDecoration inactiveDecoration) {
return new Material(
child: new Center(
child: new Switch(
value: false,
value: active,
onChanged: null,
activeThumbDecoration: activeDecoration,
inactiveThumbDecoration: inactiveDecoration
......@@ -73,19 +89,27 @@ void main() {
);
}
await tester.pumpWidget(build(activeDecoration, inactiveDecoration));
// no build yet
expect(activeDecoration.listeners, 0);
expect(inactiveDecoration.listeners, 0);
expect(activeDecoration.listeners.length, 1);
expect(inactiveDecoration.listeners.length, 1);
await tester.pumpWidget(build(false, activeDecoration, inactiveDecoration));
expect(activeDecoration.listeners, 0);
expect(inactiveDecoration.listeners, 1);
await tester.pumpWidget(build(activeDecoration, null));
await tester.pumpWidget(build(true, activeDecoration, inactiveDecoration));
// started the animation, but we're on frame 0
expect(activeDecoration.listeners, 0);
expect(inactiveDecoration.listeners, 2);
expect(activeDecoration.listeners.length, 1);
expect(inactiveDecoration.listeners.length, 0);
await tester.pump(const Duration(milliseconds: 30)); // slightly into the animation
// we're painting some lerped decoration that doesn't exactly match either
expect(activeDecoration.listeners, 0);
expect(inactiveDecoration.listeners, 2);
await tester.pumpWidget(new Container(key: new UniqueKey()));
await tester.pump(const Duration(seconds: 1)); // ended animation
expect(activeDecoration.listeners, 1);
expect(inactiveDecoration.listeners, 2);
expect(activeDecoration.listeners.length, 0);
expect(inactiveDecoration.listeners.length, 0);
});
}
......@@ -12,10 +12,10 @@ void main() {
imageCache.maximumSize = 2;
TestImageInfo a = (await imageCache.loadProvider(new TestProvider(1, 1)).first);
TestImageInfo b = (await imageCache.loadProvider(new TestProvider(2, 2)).first);
TestImageInfo c = (await imageCache.loadProvider(new TestProvider(3, 3)).first);
TestImageInfo d = (await imageCache.loadProvider(new TestProvider(1, 4)).first);
TestImageInfo a = await extractOneFrame(new TestProvider(1, 1).resolve(ImageConfiguration.empty));
TestImageInfo b = await extractOneFrame(new TestProvider(2, 2).resolve(ImageConfiguration.empty));
TestImageInfo c = await extractOneFrame(new TestProvider(3, 3).resolve(ImageConfiguration.empty));
TestImageInfo d = await extractOneFrame(new TestProvider(1, 4).resolve(ImageConfiguration.empty));
expect(a.value, equals(1));
expect(b.value, equals(2));
expect(c.value, equals(3));
......@@ -23,18 +23,18 @@ void main() {
imageCache.maximumSize = 0;
TestImageInfo e = (await imageCache.loadProvider(new TestProvider(1, 5)).first);
TestImageInfo e = await extractOneFrame(new TestProvider(1, 5).resolve(ImageConfiguration.empty));
expect(e.value, equals(5));
TestImageInfo f = (await imageCache.loadProvider(new TestProvider(1, 6)).first);
TestImageInfo f = await extractOneFrame(new TestProvider(1, 6).resolve(ImageConfiguration.empty));
expect(f.value, equals(6));
imageCache.maximumSize = 3;
TestImageInfo g = (await imageCache.loadProvider(new TestProvider(1, 7)).first);
TestImageInfo g = await extractOneFrame(new TestProvider(1, 7).resolve(ImageConfiguration.empty));
expect(g.value, equals(7));
TestImageInfo h = (await imageCache.loadProvider(new TestProvider(1, 8)).first);
TestImageInfo h = await extractOneFrame(new TestProvider(1, 8).resolve(ImageConfiguration.empty));
expect(h.value, equals(7));
});
......
......@@ -12,69 +12,69 @@ void main() {
imageCache.maximumSize = 3;
TestImageInfo a = (await imageCache.loadProvider(new TestProvider(1, 1)).first);
TestImageInfo a = await extractOneFrame(new TestProvider(1, 1).resolve(ImageConfiguration.empty));
expect(a.value, equals(1));
TestImageInfo b = (await imageCache.loadProvider(new TestProvider(1, 2)).first);
TestImageInfo b = await extractOneFrame(new TestProvider(1, 2).resolve(ImageConfiguration.empty));
expect(b.value, equals(1));
TestImageInfo c = (await imageCache.loadProvider(new TestProvider(1, 3)).first);
TestImageInfo c = await extractOneFrame(new TestProvider(1, 3).resolve(ImageConfiguration.empty));
expect(c.value, equals(1));
TestImageInfo d = (await imageCache.loadProvider(new TestProvider(1, 4)).first);
TestImageInfo d = await extractOneFrame(new TestProvider(1, 4).resolve(ImageConfiguration.empty));
expect(d.value, equals(1));
TestImageInfo e = (await imageCache.loadProvider(new TestProvider(1, 5)).first);
TestImageInfo e = await extractOneFrame(new TestProvider(1, 5).resolve(ImageConfiguration.empty));
expect(e.value, equals(1));
TestImageInfo f = (await imageCache.loadProvider(new TestProvider(1, 6)).first);
TestImageInfo f = await extractOneFrame(new TestProvider(1, 6).resolve(ImageConfiguration.empty));
expect(f.value, equals(1));
expect(f, equals(a));
// cache still only has one entry in it: 1(1)
TestImageInfo g = (await imageCache.loadProvider(new TestProvider(2, 7)).first);
TestImageInfo g = await extractOneFrame(new TestProvider(2, 7).resolve(ImageConfiguration.empty));
expect(g.value, equals(7));
// cache has two entries in it: 1(1), 2(7)
TestImageInfo h = (await imageCache.loadProvider(new TestProvider(1, 8)).first);
TestImageInfo h = await extractOneFrame(new TestProvider(1, 8).resolve(ImageConfiguration.empty));
expect(h.value, equals(1));
// cache still has two entries in it: 2(7), 1(1)
TestImageInfo i = (await imageCache.loadProvider(new TestProvider(3, 9)).first);
TestImageInfo i = await extractOneFrame(new TestProvider(3, 9).resolve(ImageConfiguration.empty));
expect(i.value, equals(9));
// cache has three entries in it: 2(7), 1(1), 3(9)
TestImageInfo j = (await imageCache.loadProvider(new TestProvider(1, 10)).first);
TestImageInfo j = await extractOneFrame(new TestProvider(1, 10).resolve(ImageConfiguration.empty));
expect(j.value, equals(1));
// cache still has three entries in it: 2(7), 3(9), 1(1)
TestImageInfo k = (await imageCache.loadProvider(new TestProvider(4, 11)).first);
TestImageInfo k = await extractOneFrame(new TestProvider(4, 11).resolve(ImageConfiguration.empty));
expect(k.value, equals(11));
// cache has three entries: 3(9), 1(1), 4(11)
TestImageInfo l = (await imageCache.loadProvider(new TestProvider(1, 12)).first);
TestImageInfo l = await extractOneFrame(new TestProvider(1, 12).resolve(ImageConfiguration.empty));
expect(l.value, equals(1));
// cache has three entries: 3(9), 4(11), 1(1)
TestImageInfo m = (await imageCache.loadProvider(new TestProvider(2, 13)).first);
TestImageInfo m = await extractOneFrame(new TestProvider(2, 13).resolve(ImageConfiguration.empty));
expect(m.value, equals(13));
// cache has three entries: 4(11), 1(1), 2(13)
TestImageInfo n = (await imageCache.loadProvider(new TestProvider(3, 14)).first);
TestImageInfo n = await extractOneFrame(new TestProvider(3, 14).resolve(ImageConfiguration.empty));
expect(n.value, equals(14));
// cache has three entries: 1(1), 2(13), 3(14)
TestImageInfo o = (await imageCache.loadProvider(new TestProvider(4, 15)).first);
TestImageInfo o = await extractOneFrame(new TestProvider(4, 15).resolve(ImageConfiguration.empty));
expect(o.value, equals(15));
// cache has three entries: 2(13), 3(14), 4(15)
TestImageInfo p = (await imageCache.loadProvider(new TestProvider(1, 16)).first);
TestImageInfo p = await extractOneFrame(new TestProvider(1, 16).resolve(ImageConfiguration.empty));
expect(p.value, equals(16));
// cache has three entries: 3(14), 4(15), 1(16)
......
......@@ -5,6 +5,7 @@
import 'dart:async';
import 'dart:ui' as ui show Image;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
class TestImageInfo implements ImageInfo {
......@@ -22,27 +23,33 @@ class TestImageInfo implements ImageInfo {
String toString() => '$runtimeType($value)';
}
class TestProvider extends ImageProvider {
const TestProvider(this.equalityValue, this.imageValue);
class TestProvider extends ImageProvider<int> {
const TestProvider(this.key, this.imageValue);
final int key;
final int imageValue;
final int equalityValue;
@override
Future<ImageInfo> loadImage() async {
return new TestImageInfo(imageValue);
Future<int> obtainKey(ImageConfiguration configuration) {
return new Future<int>.value(key);
}
@override
bool operator ==(dynamic other) {
if (other is! TestProvider)
return false;
final TestProvider typedOther = other;
return equalityValue == typedOther.equalityValue;
ImageStreamCompleter load(int key) {
return new OneFrameImageStreamCompleter(
new SynchronousFuture<ImageInfo>(new TestImageInfo(imageValue))
);
}
@override
int get hashCode => equalityValue.hashCode;
String toString() => '$runtimeType($key, $imageValue)';
}
@override
String toString() => '$runtimeType($equalityValue, $imageValue)';
Future<ImageInfo> extractOneFrame(ImageStream stream) {
Completer<ImageInfo> completer = new Completer<ImageInfo>();
void listener(ImageInfo image) {
completer.complete(image);
stream.removeListener(listener);
}
stream.addListener(listener);
return completer.future;
}
\ No newline at end of file
......@@ -5,11 +5,11 @@
import 'dart:async';
import 'dart:ui' as ui show Image;
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:mojo/core.dart' as core;
import 'package:flutter_test/flutter_test.dart';
import 'package:mojo/core.dart' as mojo;
class TestImage extends ui.Image {
TestImage(this.scale);
......@@ -25,7 +25,7 @@ class TestImage extends ui.Image {
void dispose() { }
}
class TestMojoDataPipeConsumer extends core.MojoDataPipeConsumer {
class TestMojoDataPipeConsumer extends mojo.MojoDataPipeConsumer {
TestMojoDataPipeConsumer(this.scale) : super(null);
final double scale;
}
......@@ -41,21 +41,10 @@ String testManifest = '''
}
''';
class TestAssetBundle extends AssetBundle {
// Image loading logic routes through load(key)
@override
ImageResource loadImage(String key) => null;
@override
Future<String> loadString(String key) {
if (key == 'AssetManifest.json')
return (new Completer<String>()..complete(testManifest)).future;
return null;
}
class TestAssetBundle extends CachingAssetBundle {
@override
Future<core.MojoDataPipeConsumer> load(String key) {
core.MojoDataPipeConsumer pipe;
Future<mojo.MojoDataPipeConsumer> load(String key) {
mojo.MojoDataPipeConsumer pipe;
switch (key) {
case 'assets/image.png':
pipe = new TestMojoDataPipeConsumer(1.0);
......@@ -73,18 +62,27 @@ class TestAssetBundle extends AssetBundle {
pipe = new TestMojoDataPipeConsumer(4.0);
break;
}
return (new Completer<core.MojoDataPipeConsumer>()..complete(pipe)).future;
return (new Completer<mojo.MojoDataPipeConsumer>()..complete(pipe)).future;
}
@override
Future<String> loadString(String key, { bool cache: true }) {
if (key == 'AssetManifest.json')
return new SynchronousFuture<String>(testManifest);
return null;
}
@override
String toString() => '$runtimeType@$hashCode()';
}
Future<ui.Image> testDecodeImageFromDataPipe(core.MojoDataPipeConsumer pipe) {
TestMojoDataPipeConsumer testPipe = pipe;
assert(testPipe != null);
ui.Image image = new TestImage(testPipe.scale);
return (new Completer<ui.Image>()..complete(image)).future;
class TestAssetImage extends AssetImage {
TestAssetImage(String name) : super(name);
@override
Future<ui.Image> decodeImage(TestMojoDataPipeConsumer pipe) {
return new Future<ui.Image>.value(new TestImage(pipe.scale));
}
}
Widget buildImageAtRatio(String image, Key key, double ratio, bool inferSize) {
......@@ -97,19 +95,17 @@ Widget buildImageAtRatio(String image, Key key, double ratio, bool inferSize) {
devicePixelRatio: ratio,
padding: const EdgeInsets.all(0.0)
),
child: new AssetVendor(
child: new DefaultAssetBundle(
bundle: new TestAssetBundle(),
devicePixelRatio: ratio,
imageDecoder: testDecodeImageFromDataPipe,
child: new Center(
child: inferSize ?
new AssetImage(
new Image(
key: key,
name: image
image: new TestAssetImage(image)
) :
new AssetImage(
new Image(
key: key,
name: image,
image: new TestAssetImage(image),
height: imageSize,
width: imageSize,
fit: ImageFit.fill
......
......@@ -5,99 +5,99 @@
import 'dart:async';
import 'dart:ui' as ui show Image;
import 'package:mojo/core.dart' as core;
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/services.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
void main() {
testWidgets('Verify NetworkImage sets an ObjectKey on its ImageResource if it doesn\'t have a key', (WidgetTester tester) async {
final String testUrl = 'https://foo.bar/baz1.png';
await tester.pumpWidget(
new NetworkImage(
scale: 1.0,
src: testUrl
)
);
ImageResource imageResource = imageCache.load(testUrl, scale: 1.0);
expect(find.byKey(new ObjectKey(imageResource)), findsOneWidget);
});
testWidgets('Verify NetworkImage doesn\'t set an ObjectKey on its ImageResource if it has a key', (WidgetTester tester) async {
final String testUrl = 'https://foo.bar/baz2.png';
testWidgets('Verify Image resets its RenderImage when changing providers', (WidgetTester tester) async {
final GlobalKey key = new GlobalKey();
TestImageProvider imageProvider1 = new TestImageProvider();
await tester.pumpWidget(
new NetworkImage(
key: new GlobalKey(),
scale: 1.0,
src: testUrl
new Container(
key: key,
child: new Image(
image: imageProvider1
)
),
null,
EnginePhase.layout
);
RenderImage renderImage = key.currentContext.findRenderObject();
expect(renderImage.image, isNull);
ImageResource imageResource = imageCache.load(testUrl, scale: 1.0);
expect(find.byKey(new ObjectKey(imageResource)), findsNothing);
});
testWidgets('Verify AsyncImage sets an ObjectKey on its ImageResource if it doesn\'t have a key', (WidgetTester tester) async {
ImageProvider imageProvider = new TestImageProvider();
await tester.pumpWidget(new AsyncImage(provider: imageProvider));
imageProvider1.complete();
await tester.idle(); // resolve the future from the image provider
await tester.pump(null, EnginePhase.layout);
ImageResource imageResource = imageCache.loadProvider(imageProvider);
expect(find.byKey(new ObjectKey(imageResource)), findsOneWidget);
});
renderImage = key.currentContext.findRenderObject();
expect(renderImage.image, isNotNull);
testWidgets('Verify AsyncImage doesn\'t set an ObjectKey on its ImageResource if it has a key', (WidgetTester tester) async {
ImageProvider imageProvider = new TestImageProvider();
TestImageProvider imageProvider2 = new TestImageProvider();
await tester.pumpWidget(
new AsyncImage(
key: new GlobalKey(),
provider: imageProvider
new Container(
key: key,
child: new Image(
image: imageProvider2
)
),
null,
EnginePhase.layout
);
ImageResource imageResource = imageCache.loadProvider(imageProvider);
expect(find.byKey(new ObjectKey(imageResource)), findsNothing);
renderImage = key.currentContext.findRenderObject();
expect(renderImage.image, isNull);
});
testWidgets('Verify AssetImage sets an ObjectKey on its ImageResource if it doesn\'t have a key', (WidgetTester tester) async {
final String name = 'foo';
final AssetBundle assetBundle = new TestAssetBundle();
testWidgets('Verify Image doesn\'t reset its RenderImage when changing providers if it has gaplessPlayback set', (WidgetTester tester) async {
final GlobalKey key = new GlobalKey();
TestImageProvider imageProvider1 = new TestImageProvider();
await tester.pumpWidget(
new AssetImage(
name: name,
bundle: assetBundle
new Container(
key: key,
child: new Image(
gaplessPlayback: true,
image: imageProvider1
)
),
null,
EnginePhase.layout
);
RenderImage renderImage = key.currentContext.findRenderObject();
expect(renderImage.image, isNull);
ImageResource imageResource = assetBundle.loadImage(name);
expect(find.byKey(new ObjectKey(imageResource)), findsOneWidget);
});
imageProvider1.complete();
await tester.idle(); // resolve the future from the image provider
await tester.pump(null, EnginePhase.layout);
renderImage = key.currentContext.findRenderObject();
expect(renderImage.image, isNotNull);
testWidgets('Verify AssetImage doesn\'t set an ObjectKey on its ImageResource if it has a key', (WidgetTester tester) async {
final String name = 'foo';
final AssetBundle assetBundle = new TestAssetBundle();
TestImageProvider imageProvider2 = new TestImageProvider();
await tester.pumpWidget(
new AssetImage(
key: new GlobalKey(),
name: name,
bundle: assetBundle
new Container(
key: key,
child: new Image(
gaplessPlayback: true,
image: imageProvider2
)
),
null,
EnginePhase.layout
);
ImageResource imageResource = assetBundle.loadImage(name);
expect(find.byKey(new ObjectKey(imageResource)), findsNothing);
renderImage = key.currentContext.findRenderObject();
expect(renderImage.image, isNotNull);
});
testWidgets('Verify AsyncImage resets its RenderImage when changing providers if it doesn\'t have a key', (WidgetTester tester) async {
testWidgets('Verify Image resets its RenderImage when changing providers if it has a key', (WidgetTester tester) async {
final GlobalKey key = new GlobalKey();
TestImageProvider imageProvider1 = new TestImageProvider();
await tester.pumpWidget(
new Container(
new Image(
key: key,
child: new AsyncImage(
provider: imageProvider1
)
image: imageProvider1
),
null,
EnginePhase.layout
......@@ -114,11 +114,9 @@ void main() {
TestImageProvider imageProvider2 = new TestImageProvider();
await tester.pumpWidget(
new Container(
new Image(
key: key,
child: new AsyncImage(
provider: imageProvider2
)
image: imageProvider2
),
null,
EnginePhase.layout
......@@ -126,16 +124,16 @@ void main() {
renderImage = key.currentContext.findRenderObject();
expect(renderImage.image, isNull);
});
testWidgets('Verify AsyncImage doesn\'t reset its RenderImage when changing providers if it has a key', (WidgetTester tester) async {
testWidgets('Verify Image doesn\'t reset its RenderImage when changing providers if it has gaplessPlayback set', (WidgetTester tester) async {
final GlobalKey key = new GlobalKey();
TestImageProvider imageProvider1 = new TestImageProvider();
await tester.pumpWidget(
new AsyncImage(
new Image(
key: key,
provider: imageProvider1
gaplessPlayback: true,
image: imageProvider1
),
null,
EnginePhase.layout
......@@ -152,9 +150,10 @@ void main() {
TestImageProvider imageProvider2 = new TestImageProvider();
await tester.pumpWidget(
new AsyncImage(
new Image(
key: key,
provider: imageProvider2
gaplessPlayback: true,
image: imageProvider2
),
null,
EnginePhase.layout
......@@ -166,28 +165,23 @@ void main() {
}
class TestImageProvider extends ImageProvider {
class TestImageProvider extends ImageProvider<TestImageProvider> {
final Completer<ImageInfo> _completer = new Completer<ImageInfo>();
@override
Future<ImageInfo> loadImage() => _completer.future;
void complete() {
_completer.complete(new ImageInfo(image:new TestImage()));
Future<TestImageProvider> obtainKey(ImageConfiguration configuration) {
return new SynchronousFuture<TestImageProvider>(this);
}
}
class TestAssetBundle extends AssetBundle {
final ImageResource _imageResource = new ImageResource(new Completer<ImageInfo>().future);
@override
ImageResource loadImage(String key) => _imageResource;
ImageStreamCompleter load(TestImageProvider key) => new OneFrameImageStreamCompleter(_completer.future);
@override
Future<String> loadString(String key) => new Completer<String>().future;
void complete() {
_completer.complete(new ImageInfo(image: new TestImage()));
}
@override
Future<core.MojoDataPipeConsumer> load(String key) => new Completer<core.MojoDataPipeConsumer>().future;
String toString() => '$runtimeType($hashCode)';
}
class TestImage extends ui.Image {
......@@ -198,6 +192,5 @@ class TestImage extends ui.Image {
int get height => 100;
@override
void dispose() {
}
void dispose() { }
}
......@@ -485,7 +485,11 @@ class _Block {
}
}
return new NetworkImage(src: path, width: width, height: height);
return new Image(
image: new NetworkImage(path),
width: width,
height: height
);
}
}
......
......@@ -21,9 +21,16 @@ class ImageMap {
}
Future<ui.Image> _loadImage(String url) async {
ui.Image image = (await _bundle.loadImage(url).first).image;
ImageStream stream = new NetworkImage(url).resolve(ImageConfiguration.empty);
Completer<ui.Image> completer = new Completer<ui.Image>();
void listener(ImageInfo frame) {
final ui.Image image = frame.image;
_images[url] = image;
return image;
completer.complete(image);
stream.removeListener(listener);
}
stream.addListener(listener);
return completer.future;
}
/// Returns a preloaded image, given its [url].
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment