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
...@@ -57,9 +57,9 @@ class FancyItemDelegate extends LazyBlockDelegate { ...@@ -57,9 +57,9 @@ class FancyItemDelegate extends LazyBlockDelegate {
@override @override
Widget buildItem(BuildContext context, int index) { Widget buildItem(BuildContext context, int index) {
if (index % 2 == 0) if (index % 2 == 0)
return new FancyImageItem(index, key: new Key("Item $index")); return new FancyImageItem(index, key: new Key('Item $index'));
else else
return new FancyGalleryItem(index, key: new Key("Item $index")); return new FancyGalleryItem(index, key: new Key('Item $index'));
} }
@override @override
...@@ -78,7 +78,7 @@ class ComplexLayoutState extends State<ComplexLayout> { ...@@ -78,7 +78,7 @@ class ComplexLayoutState extends State<ComplexLayout> {
icon: Icons.create, icon: Icons.create,
tooltip: 'Search', tooltip: 'Search',
onPressed: () { onPressed: () {
print("Pressed search"); print('Pressed search');
} }
), ),
new TopBarMenu() new TopBarMenu()
...@@ -88,7 +88,7 @@ class ComplexLayoutState extends State<ComplexLayout> { ...@@ -88,7 +88,7 @@ class ComplexLayoutState extends State<ComplexLayout> {
children: <Widget>[ children: <Widget>[
new Flexible( new Flexible(
child: new LazyBlock( child: new LazyBlock(
key: new Key("main-scroll"), key: new Key('main-scroll'),
delegate: new FancyItemDelegate() delegate: new FancyItemDelegate()
) )
), ),
...@@ -104,47 +104,47 @@ class TopBarMenu extends StatelessWidget { ...@@ -104,47 +104,47 @@ class TopBarMenu extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new PopupMenuButton<String>( return new PopupMenuButton<String>(
onSelected: (String value) { print("Selected: $value"); }, onSelected: (String value) { print('Selected: $value'); },
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[ itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
new PopupMenuItem<String>( new PopupMenuItem<String>(
value: "Friends", value: 'Friends',
child: new MenuItemWithIcon(Icons.people, "Friends", "5 new") child: new MenuItemWithIcon(Icons.people, 'Friends', '5 new')
), ),
new PopupMenuItem<String>( new PopupMenuItem<String>(
value: "Events", value: 'Events',
child: new MenuItemWithIcon(Icons.event, "Events", "12 upcoming") child: new MenuItemWithIcon(Icons.event, 'Events', '12 upcoming')
), ),
new PopupMenuItem<String>( new PopupMenuItem<String>(
value: "Events", value: 'Events',
child: new MenuItemWithIcon(Icons.group, "Groups", "14") child: new MenuItemWithIcon(Icons.group, 'Groups', '14')
), ),
new PopupMenuItem<String>( new PopupMenuItem<String>(
value: "Events", value: 'Events',
child: new MenuItemWithIcon(Icons.image, "Pictures", "12") child: new MenuItemWithIcon(Icons.image, 'Pictures', '12')
), ),
new PopupMenuItem<String>( new PopupMenuItem<String>(
value: "Events", value: 'Events',
child: new MenuItemWithIcon(Icons.near_me, "Nearby", "33") child: new MenuItemWithIcon(Icons.near_me, 'Nearby', '33')
), ),
new PopupMenuItem<String>( new PopupMenuItem<String>(
value: "Friends", value: 'Friends',
child: new MenuItemWithIcon(Icons.people, "Friends", "5") child: new MenuItemWithIcon(Icons.people, 'Friends', '5')
), ),
new PopupMenuItem<String>( new PopupMenuItem<String>(
value: "Events", value: 'Events',
child: new MenuItemWithIcon(Icons.event, "Events", "12") child: new MenuItemWithIcon(Icons.event, 'Events', '12')
), ),
new PopupMenuItem<String>( new PopupMenuItem<String>(
value: "Events", value: 'Events',
child: new MenuItemWithIcon(Icons.group, "Groups", "14") child: new MenuItemWithIcon(Icons.group, 'Groups', '14')
), ),
new PopupMenuItem<String>( new PopupMenuItem<String>(
value: "Events", value: 'Events',
child: new MenuItemWithIcon(Icons.image, "Pictures", "12") child: new MenuItemWithIcon(Icons.image, 'Pictures', '12')
), ),
new PopupMenuItem<String>( new PopupMenuItem<String>(
value: "Events", value: 'Events',
child: new MenuItemWithIcon(Icons.near_me, "Nearby", "33") child: new MenuItemWithIcon(Icons.near_me, 'Nearby', '33')
) )
] ]
); );
...@@ -182,7 +182,7 @@ class FancyImageItem extends StatelessWidget { ...@@ -182,7 +182,7 @@ class FancyImageItem extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new BlockBody( return new BlockBody(
children: <Widget>[ children: <Widget>[
new UserHeader("Ali Connors $index"), new UserHeader('Ali Connors $index'),
new ItemDescription(), new ItemDescription(),
new ItemImageBox(), new ItemImageBox(),
new InfoBar(), new InfoBar(),
...@@ -205,7 +205,7 @@ class FancyGalleryItem extends StatelessWidget { ...@@ -205,7 +205,7 @@ class FancyGalleryItem extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new BlockBody( return new BlockBody(
children: <Widget>[ children: <Widget>[
new UserHeader("Ali Connors"), new UserHeader('Ali Connors'),
new ItemGalleryBox(index), new ItemGalleryBox(index),
new InfoBar(), new InfoBar(),
new Padding( new Padding(
...@@ -227,8 +227,8 @@ class InfoBar extends StatelessWidget { ...@@ -227,8 +227,8 @@ class InfoBar extends StatelessWidget {
child: new Row( child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
new MiniIconWithText(Icons.thumb_up, "42"), new MiniIconWithText(Icons.thumb_up, '42'),
new Text("3 Comments", style: Theme.of(context).textTheme.caption) new Text('3 Comments', style: Theme.of(context).textTheme.caption)
] ]
) )
); );
...@@ -243,9 +243,9 @@ class IconBar extends StatelessWidget { ...@@ -243,9 +243,9 @@ class IconBar extends StatelessWidget {
child: new Row( child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
new IconWithText(Icons.thumb_up, "Like"), new IconWithText(Icons.thumb_up, 'Like'),
new IconWithText(Icons.comment, "Comment"), new IconWithText(Icons.comment, 'Comment'),
new IconWithText(Icons.share, "Share"), new IconWithText(Icons.share, 'Share'),
] ]
) )
); );
...@@ -263,7 +263,7 @@ class IconWithText extends StatelessWidget { ...@@ -263,7 +263,7 @@ class IconWithText extends StatelessWidget {
return new Row( return new Row(
mainAxisAlignment: MainAxisAlignment.collapse, mainAxisAlignment: MainAxisAlignment.collapse,
children: <Widget>[ children: <Widget>[
new IconButton(icon: icon, onPressed: () { print("Pressed $title button"); } ), new IconButton(icon: icon, onPressed: () { print('Pressed $title button'); } ),
new Text(title) new Text(title)
] ]
); );
...@@ -325,8 +325,8 @@ class UserHeader extends StatelessWidget { ...@@ -325,8 +325,8 @@ class UserHeader extends StatelessWidget {
children: <Widget>[ children: <Widget>[
new Padding( new Padding(
padding: new EdgeInsets.only(right: 8.0), padding: new EdgeInsets.only(right: 8.0),
child: new AssetImage( child: new Image(
name: "packages/flutter_gallery_assets/ali_connors_sml.png", image: new AssetImage('packages/flutter_gallery_assets/ali_connors_sml.png'),
width: 32.0, width: 32.0,
height: 32.0 height: 32.0
) )
...@@ -340,13 +340,13 @@ class UserHeader extends StatelessWidget { ...@@ -340,13 +340,13 @@ class UserHeader extends StatelessWidget {
style: Theme.of(context).textTheme.body1, style: Theme.of(context).textTheme.body1,
children: <TextSpan>[ children: <TextSpan>[
new TextSpan(text: userName, style: new TextStyle(fontWeight: FontWeight.bold)), new TextSpan(text: userName, style: new TextStyle(fontWeight: FontWeight.bold)),
new TextSpan(text: " shared a new "), new TextSpan(text: ' shared a new '),
new TextSpan(text: "photo", style: new TextStyle(fontWeight: FontWeight.bold)) new TextSpan(text: 'photo', style: new TextStyle(fontWeight: FontWeight.bold))
] ]
)), )),
new Row( new Row(
children: <Widget>[ children: <Widget>[
new Text("Yesterday at 11:55 • ", style: Theme.of(context).textTheme.caption), new Text('Yesterday at 11:55 • ', style: Theme.of(context).textTheme.caption),
new Icon(icon: Icons.people, size: 16.0, color: Theme.of(context).textTheme.caption.color) new Icon(icon: Icons.people, size: 16.0, color: Theme.of(context).textTheme.caption.color)
] ]
) )
...@@ -365,7 +365,7 @@ class ItemDescription extends StatelessWidget { ...@@ -365,7 +365,7 @@ class ItemDescription extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Padding( return new Padding(
padding: new EdgeInsets.all(8.0), padding: new EdgeInsets.all(8.0),
child: new Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.") child: new Text('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.')
); );
} }
} }
...@@ -383,8 +383,8 @@ class ItemImageBox extends StatelessWidget { ...@@ -383,8 +383,8 @@ class ItemImageBox extends StatelessWidget {
children: <Widget>[ children: <Widget>[
new SizedBox( new SizedBox(
height: 230.0, height: 230.0,
child: new AssetImage( child: new Image(
name: "packages/flutter_gallery_assets/top_10_australian_beaches.png" image: new AssetImage('packages/flutter_gallery_assets/top_10_australian_beaches.png')
) )
), ),
new Theme( new Theme(
...@@ -392,8 +392,8 @@ class ItemImageBox extends StatelessWidget { ...@@ -392,8 +392,8 @@ class ItemImageBox extends StatelessWidget {
child: new Row( child: new Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[ children: <Widget>[
new IconButton(icon: Icons.edit, onPressed: () { print("Pressed edit button"); }), new IconButton(icon: Icons.edit, onPressed: () { print('Pressed edit button'); }),
new IconButton(icon: Icons.zoom_in, onPressed: () { print("Pressed zoom button"); }) new IconButton(icon: Icons.zoom_in, onPressed: () { print('Pressed zoom button'); })
] ]
) )
), ),
...@@ -411,11 +411,11 @@ class ItemImageBox extends StatelessWidget { ...@@ -411,11 +411,11 @@ class ItemImageBox extends StatelessWidget {
style: new TextStyle(color: Colors.white), style: new TextStyle(color: Colors.white),
children: <TextSpan>[ children: <TextSpan>[
new TextSpan( new TextSpan(
text: "Photo by " text: 'Photo by '
), ),
new TextSpan( new TextSpan(
style: new TextStyle(fontWeight: FontWeight.bold), style: new TextStyle(fontWeight: FontWeight.bold),
text: "Magic Mike" text: 'Magic Mike'
) )
] ]
) )
...@@ -430,9 +430,9 @@ class ItemImageBox extends StatelessWidget { ...@@ -430,9 +430,9 @@ class ItemImageBox extends StatelessWidget {
child: new Column( child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
new Text("Where can you find that amazing sunset?", style: Theme.of(context).textTheme.body2), new Text('Where can you find that amazing sunset?', style: Theme.of(context).textTheme.body2),
new Text("The sun sets over stinson beach", style: Theme.of(context).textTheme.body1), new Text('The sun sets over stinson beach', style: Theme.of(context).textTheme.body1),
new Text("flutter.io/amazingsunsets", style: Theme.of(context).textTheme.caption) new Text('flutter.io/amazingsunsets', style: Theme.of(context).textTheme.caption)
] ]
) )
) )
...@@ -451,7 +451,7 @@ class ItemGalleryBox extends StatelessWidget { ...@@ -451,7 +451,7 @@ class ItemGalleryBox extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<String> tabNames = <String>[ List<String> tabNames = <String>[
"A", "B", "C", "D" 'A', 'B', 'C', 'D'
]; ];
return new SizedBox( return new SizedBox(
...@@ -464,7 +464,7 @@ class ItemGalleryBox extends StatelessWidget { ...@@ -464,7 +464,7 @@ class ItemGalleryBox extends StatelessWidget {
child: new TabBarView<String>( child: new TabBarView<String>(
children: tabNames.map((String tabName) { children: tabNames.map((String tabName) {
return new Container( return new Container(
key: new Key("Tab $index - $tabName"), key: new Key('Tab $index - $tabName'),
child: new Padding( child: new Padding(
padding: new EdgeInsets.all(8.0), padding: new EdgeInsets.all(8.0),
child: new Card( child: new Card(
...@@ -484,16 +484,16 @@ class ItemGalleryBox extends StatelessWidget { ...@@ -484,16 +484,16 @@ class ItemGalleryBox extends StatelessWidget {
children: <Widget>[ children: <Widget>[
new IconButton( new IconButton(
icon: Icons.share, icon: Icons.share,
onPressed: () { print("Pressed share"); } onPressed: () { print('Pressed share'); }
), ),
new IconButton( new IconButton(
icon: Icons.event, icon: Icons.event,
onPressed: () { print("Pressed event"); } onPressed: () { print('Pressed event'); }
), ),
new Flexible( new Flexible(
child: new Padding( child: new Padding(
padding: new EdgeInsets.only(left: 8.0), padding: new EdgeInsets.only(left: 8.0),
child: new Text("This is item $tabName") child: new Text('This is item $tabName')
) )
) )
] ]
...@@ -531,11 +531,11 @@ class BottomBar extends StatelessWidget { ...@@ -531,11 +531,11 @@ class BottomBar extends StatelessWidget {
child: new Row( child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
new BottomBarButton(Icons.new_releases, "News"), new BottomBarButton(Icons.new_releases, 'News'),
new BottomBarButton(Icons.people, "Requests"), new BottomBarButton(Icons.people, 'Requests'),
new BottomBarButton(Icons.chat, "Messenger"), new BottomBarButton(Icons.chat, 'Messenger'),
new BottomBarButton(Icons.bookmark, "Bookmark"), new BottomBarButton(Icons.bookmark, 'Bookmark'),
new BottomBarButton(Icons.alarm, "Alarm") new BottomBarButton(Icons.alarm, 'Alarm')
] ]
) )
); );
...@@ -556,7 +556,7 @@ class BottomBarButton extends StatelessWidget { ...@@ -556,7 +556,7 @@ class BottomBarButton extends StatelessWidget {
children: <Widget>[ children: <Widget>[
new IconButton( new IconButton(
icon: icon, icon: icon,
onPressed: () { print("Pressed: $title"); } onPressed: () { print('Pressed: $title'); }
), ),
new Text(title, style: Theme.of(context).textTheme.caption) new Text(title, style: Theme.of(context).textTheme.caption)
] ]
......
...@@ -429,7 +429,7 @@ class CardCollectionState extends State<CardCollection> { ...@@ -429,7 +429,7 @@ class CardCollectionState extends State<CardCollection> {
if (_sunshine) { if (_sunshine) {
cardCollection = new Stack( cardCollection = new Stack(
children: <Widget>[ 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) new ShaderMask(child: cardCollection, shaderCallback: _createShader)
] ]
); );
......
...@@ -157,8 +157,8 @@ class WeatherButton extends StatelessWidget { ...@@ -157,8 +157,8 @@ class WeatherButton extends StatelessWidget {
child: new InkWell( child: new InkWell(
onTap: onPressed, onTap: onPressed,
child: new Center( child: new Center(
child: new AssetImage( child: new Image(
name: icon, image: new AssetImage(icon),
width: _kWeatherIconSize, width: _kWeatherIconSize,
height: _kWeatherIconSize height: _kWeatherIconSize
) )
......
...@@ -66,8 +66,8 @@ class TravelDestinationItem extends StatelessWidget { ...@@ -66,8 +66,8 @@ class TravelDestinationItem extends StatelessWidget {
top: 0.0, top: 0.0,
bottom: 0.0, bottom: 0.0,
right: 0.0, right: 0.0,
child: new AssetImage( child: new Image(
name: destination.assetName, image: new AssetImage(destination.assetName),
fit: ImageFit.cover fit: ImageFit.cover
) )
), ),
......
...@@ -129,8 +129,8 @@ class ContactsDemoState extends State<ContactsDemo> { ...@@ -129,8 +129,8 @@ class ContactsDemoState extends State<ContactsDemo> {
title : new Text('Ali Connors'), title : new Text('Ali Connors'),
background: new Stack( background: new Stack(
children: <Widget>[ children: <Widget>[
new AssetImage( new Image(
name: 'packages/flutter_gallery_assets/ali_connors.png', image: new AssetImage('packages/flutter_gallery_assets/ali_connors.png'),
fit: ImageFit.cover, fit: ImageFit.cover,
height: _appBarHeight height: _appBarHeight
), ),
......
...@@ -66,8 +66,8 @@ class GridDemoPhotoItem extends StatelessWidget { ...@@ -66,8 +66,8 @@ class GridDemoPhotoItem extends StatelessWidget {
body: new Material( body: new Material(
child: new Hero( child: new Hero(
tag: photoHeroTag, tag: photoHeroTag,
child: new AssetImage( child: new Image(
name: photo.assetName, image: new AssetImage(photo.assetName),
fit: ImageFit.cover fit: ImageFit.cover
) )
) )
...@@ -84,8 +84,8 @@ class GridDemoPhotoItem extends StatelessWidget { ...@@ -84,8 +84,8 @@ class GridDemoPhotoItem extends StatelessWidget {
child: new Hero( child: new Hero(
key: new Key(photo.assetName), key: new Key(photo.assetName),
tag: photoHeroTag, tag: photoHeroTag,
child: new AssetImage( child: new Image(
name: photo.assetName, image: new AssetImage(photo.assetName),
fit: ImageFit.cover fit: ImageFit.cover
) )
) )
......
...@@ -105,8 +105,8 @@ class _PestoDemoState extends State<PestoDemo> { ...@@ -105,8 +105,8 @@ class _PestoDemoState extends State<PestoDemo> {
bottom: extraPadding bottom: extraPadding
), ),
child: new Center( child: new Center(
child: new AssetImage( child: new Image(
name: _kLogoImages[bestHeight], image: new AssetImage(_kLogoImages[bestHeight]),
fit: ImageFit.scaleDown fit: ImageFit.scaleDown
) )
) )
...@@ -133,8 +133,8 @@ class _PestoDemoState extends State<PestoDemo> { ...@@ -133,8 +133,8 @@ class _PestoDemoState extends State<PestoDemo> {
padding: const EdgeInsets.all(2.0), padding: const EdgeInsets.all(2.0),
margin: const EdgeInsets.only(bottom: 8.0), margin: const EdgeInsets.only(bottom: 8.0),
child: new ClipOval( child: new ClipOval(
child: new AssetImage( child: new Image(
name: _kUserImage, image: new AssetImage(_kUserImage),
fit: ImageFit.contain fit: ImageFit.contain
) )
) )
...@@ -237,8 +237,8 @@ class _RecipeCard extends StatelessWidget { ...@@ -237,8 +237,8 @@ class _RecipeCard extends StatelessWidget {
child: new Column( child: new Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
new AssetImage( new Image(
name: recipe.imagePath, image: new AssetImage(recipe.imagePath),
fit: ImageFit.scaleDown fit: ImageFit.scaleDown
), ),
new Flexible( new Flexible(
...@@ -246,10 +246,10 @@ class _RecipeCard extends StatelessWidget { ...@@ -246,10 +246,10 @@ class _RecipeCard extends StatelessWidget {
children: <Widget>[ children: <Widget>[
new Padding( new Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: new AssetImage( child: new Image(
image: new AssetImage(recipe.ingredientsImagePath),
width: 48.0, width: 48.0,
height: 48.0, height: 48.0
name: recipe.ingredientsImagePath
) )
), ),
new Column( new Column(
...@@ -341,7 +341,7 @@ class _RecipePageState extends State<_RecipePage> { ...@@ -341,7 +341,7 @@ class _RecipePageState extends State<_RecipePage> {
decoration: new BoxDecoration( decoration: new BoxDecoration(
backgroundColor: Theme.of(context).canvasColor, backgroundColor: Theme.of(context).canvasColor,
backgroundImage: new BackgroundImage( backgroundImage: new BackgroundImage(
image: DefaultAssetBundle.of(context).loadImage(config.recipe.imagePath), image: new AssetImage(config.recipe.imagePath),
alignment: FractionalOffset.topCenter, alignment: FractionalOffset.topCenter,
fit: fullWidth ? ImageFit.fitWidth : ImageFit.cover fit: fullWidth ? ImageFit.fitWidth : ImageFit.cover
) )
...@@ -428,10 +428,10 @@ class _RecipeSheet extends StatelessWidget { ...@@ -428,10 +428,10 @@ class _RecipeSheet extends StatelessWidget {
children: <Widget>[ children: <Widget>[
new TableCell( new TableCell(
verticalAlignment: TableCellVerticalAlignment.middle, verticalAlignment: TableCellVerticalAlignment.middle,
child: new AssetImage( child: new Image(
image: new AssetImage(recipe.ingredientsImagePath),
width: 32.0, width: 32.0,
height: 32.0, height: 32.0,
name: recipe.ingredientsImagePath,
alignment: FractionalOffset.centerLeft, alignment: FractionalOffset.centerLeft,
fit: ImageFit.scaleDown fit: ImageFit.scaleDown
) )
......
...@@ -36,9 +36,9 @@ class VendorItem extends StatelessWidget { ...@@ -36,9 +36,9 @@ class VendorItem extends StatelessWidget {
child: new ClipRRect( child: new ClipRRect(
xRadius: 12.0, xRadius: 12.0,
yRadius: 12.0, yRadius: 12.0,
child: new AssetImage( child: new Image(
fit: ImageFit.cover, image: new AssetImage(vendor.avatarAsset),
name: vendor.avatarAsset fit: ImageFit.cover
) )
) )
), ),
...@@ -165,9 +165,9 @@ class FeatureItem extends StatelessWidget { ...@@ -165,9 +165,9 @@ class FeatureItem extends StatelessWidget {
minHeight: 340.0, minHeight: 340.0,
maxHeight: 340.0, maxHeight: 340.0,
alignment: FractionalOffset.topRight, alignment: FractionalOffset.topRight,
child: new AssetImage( child: new Image(
fit: ImageFit.cover, image: new AssetImage(product.imageAsset),
name: product.imageAsset fit: ImageFit.cover
) )
) )
) )
...@@ -229,9 +229,9 @@ class ProductItem extends StatelessWidget { ...@@ -229,9 +229,9 @@ class ProductItem extends StatelessWidget {
new Hero( new Hero(
tag: productHeroTag, tag: productHeroTag,
key: new ObjectKey(product), key: new ObjectKey(product),
child: new AssetImage( child: new Image(
fit: ImageFit.contain, image: new AssetImage(product.imageAsset),
name: product.imageAsset fit: ImageFit.contain
) )
), ),
new Material( new Material(
......
...@@ -41,9 +41,9 @@ class OrderItem extends StatelessWidget { ...@@ -41,9 +41,9 @@ class OrderItem extends StatelessWidget {
height: 248.0, height: 248.0,
child: new Hero( child: new Hero(
tag: productHeroTag, tag: productHeroTag,
child: new AssetImage( child: new Image(
fit: ImageFit.contain, image: new AssetImage(product.imageAsset),
name: product.imageAsset fit: ImageFit.contain
) )
) )
) )
...@@ -192,9 +192,9 @@ class _OrderPageState extends State<OrderPage> { ...@@ -192,9 +192,9 @@ class _OrderPageState extends State<OrderPage> {
.map((Product product) { .map((Product product) {
return new Card( return new Card(
elevation: 0, elevation: 0,
child: new AssetImage( child: new Image(
fit: ImageFit.contain, image: new AssetImage(product.imageAsset),
name: product.imageAsset fit: ImageFit.contain
) )
); );
}).toList() }).toList()
......
...@@ -222,8 +222,8 @@ new ScrollableGrid( ...@@ -222,8 +222,8 @@ new ScrollableGrid(
footer: new GridTileBar( footer: new GridTileBar(
title: new Text(url) title: new Text(url)
), ),
child: new NetworkImage( child: new Image(
src: url, image: new NetworkImage(url),
fit: ImageFit.cover fit: ImageFit.cover
) )
); );
......
...@@ -66,8 +66,8 @@ class GalleryHomeState extends State<GalleryHome> { ...@@ -66,8 +66,8 @@ class GalleryHomeState extends State<GalleryHome> {
appBar: new AppBar( appBar: new AppBar(
expandedHeight: _kFlexibleSpaceMaxHeight, expandedHeight: _kFlexibleSpaceMaxHeight,
flexibleSpace: new FlexibleSpaceBar( flexibleSpace: new FlexibleSpaceBar(
background: new AssetImage( background: new Image(
name: 'packages/flutter_gallery_assets/appbar_background.jpg', image: new AssetImage('packages/flutter_gallery_assets/appbar_background.jpg'),
fit: ImageFit.cover, fit: ImageFit.cover,
height: _kFlexibleSpaceMaxHeight height: _kFlexibleSpaceMaxHeight
), ),
......
...@@ -36,17 +36,19 @@ test 1 1 ...@@ -36,17 +36,19 @@ test 1 1
class TestAssetBundle extends AssetBundle { class TestAssetBundle extends AssetBundle {
@override @override
ImageResource loadImage(String key) => null; Future<core.MojoDataPipeConsumer> load(String key) => null;
@override @override
Future<String> loadString(String key) { Future<String> loadString(String key, { bool cache: true }) {
if (key == 'lib/gallery/example_code.dart') if (key == 'lib/gallery/example_code.dart')
return new Future<String>.value(testCodeFile); return new Future<String>.value(testCodeFile);
return null; return null;
} }
@override @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 @override
String toString() => '$runtimeType@$hashCode()'; String toString() => '$runtimeType@$hashCode()';
......
...@@ -51,7 +51,9 @@ void attachWidgetTreeToRenderTree(RenderProxyBox container) { ...@@ -51,7 +51,9 @@ void attachWidgetTreeToRenderTree(RenderProxyBox container) {
new RaisedButton( new RaisedButton(
child: new Row( child: new Row(
children: <Widget>[ children: <Widget>[
new NetworkImage(src: "http://flutter.io/favicon.ico"), new Image(
image: new NetworkImage('http://flutter.io/favicon.ico')
),
new Text('PRESS ME'), new Text('PRESS ME'),
] ]
), ),
......
...@@ -14,3 +14,4 @@ export 'src/foundation/basic_types.dart'; ...@@ -14,3 +14,4 @@ export 'src/foundation/basic_types.dart';
export 'src/foundation/binding.dart'; export 'src/foundation/binding.dart';
export 'src/foundation/change_notifier.dart'; export 'src/foundation/change_notifier.dart';
export 'src/foundation/print.dart'; export 'src/foundation/print.dart';
export 'src/foundation/synchronous_future.dart';
...@@ -22,7 +22,9 @@ export 'src/services/haptic_feedback.dart'; ...@@ -22,7 +22,9 @@ export 'src/services/haptic_feedback.dart';
export 'src/services/host_messages.dart'; export 'src/services/host_messages.dart';
export 'src/services/image_cache.dart'; export 'src/services/image_cache.dart';
export 'src/services/image_decoder.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/keyboard.dart';
export 'src/services/path_provider.dart'; export 'src/services/path_provider.dart';
export 'src/services/shell.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 { ...@@ -34,6 +34,9 @@ class _DropDownMenuPainter extends CustomPainter {
elevation = elevation, elevation = elevation,
resize = resize, resize = resize,
_painter = new BoxDecoration( _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, backgroundColor: color,
borderRadius: 2.0, borderRadius: 2.0,
boxShadow: kElevationToShadow[elevation] boxShadow: kElevationToShadow[elevation]
...@@ -59,7 +62,9 @@ class _DropDownMenuPainter extends CustomPainter { ...@@ -59,7 +62,9 @@ class _DropDownMenuPainter extends CustomPainter {
end: size.height 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 @override
......
...@@ -80,11 +80,11 @@ class Switch extends StatelessWidget { ...@@ -80,11 +80,11 @@ class Switch extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
ThemeData themeData = Theme.of(context); final ThemeData themeData = Theme.of(context);
final bool isDark = themeData.brightness == Brightness.dark; final bool isDark = themeData.brightness == Brightness.dark;
Color activeThumbColor = activeColor ?? themeData.accentColor; final Color activeThumbColor = activeColor ?? themeData.accentColor;
Color activeTrackColor = activeThumbColor.withAlpha(0x80); final Color activeTrackColor = activeThumbColor.withAlpha(0x80);
Color inactiveThumbColor; Color inactiveThumbColor;
Color inactiveTrackColor; Color inactiveTrackColor;
...@@ -104,9 +104,18 @@ class Switch extends StatelessWidget { ...@@ -104,9 +104,18 @@ class Switch extends StatelessWidget {
inactiveThumbDecoration: inactiveThumbDecoration, inactiveThumbDecoration: inactiveThumbDecoration,
activeTrackColor: activeTrackColor, activeTrackColor: activeTrackColor,
inactiveTrackColor: inactiveTrackColor, inactiveTrackColor: inactiveTrackColor,
configuration: createLocalImageConfiguration(context),
onChanged: onChanged 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 { class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
...@@ -119,6 +128,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -119,6 +128,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
this.inactiveThumbDecoration, this.inactiveThumbDecoration,
this.activeTrackColor, this.activeTrackColor,
this.inactiveTrackColor, this.inactiveTrackColor,
this.configuration,
this.onChanged this.onChanged
}) : super(key: key); }) : super(key: key);
...@@ -129,6 +139,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -129,6 +139,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
final Decoration inactiveThumbDecoration; final Decoration inactiveThumbDecoration;
final Color activeTrackColor; final Color activeTrackColor;
final Color inactiveTrackColor; final Color inactiveTrackColor;
final ImageConfiguration configuration;
final ValueChanged<bool> onChanged; final ValueChanged<bool> onChanged;
@override @override
...@@ -140,6 +151,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -140,6 +151,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
inactiveThumbDecoration: inactiveThumbDecoration, inactiveThumbDecoration: inactiveThumbDecoration,
activeTrackColor: activeTrackColor, activeTrackColor: activeTrackColor,
inactiveTrackColor: inactiveTrackColor, inactiveTrackColor: inactiveTrackColor,
configuration: configuration,
onChanged: onChanged onChanged: onChanged
); );
...@@ -153,6 +165,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -153,6 +165,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
..inactiveThumbDecoration = inactiveThumbDecoration ..inactiveThumbDecoration = inactiveThumbDecoration
..activeTrackColor = activeTrackColor ..activeTrackColor = activeTrackColor
..inactiveTrackColor = inactiveTrackColor ..inactiveTrackColor = inactiveTrackColor
..configuration = configuration
..onChanged = onChanged; ..onChanged = onChanged;
} }
} }
...@@ -173,11 +186,13 @@ class _RenderSwitch extends RenderToggleable { ...@@ -173,11 +186,13 @@ class _RenderSwitch extends RenderToggleable {
Decoration inactiveThumbDecoration, Decoration inactiveThumbDecoration,
Color activeTrackColor, Color activeTrackColor,
Color inactiveTrackColor, Color inactiveTrackColor,
ImageConfiguration configuration,
ValueChanged<bool> onChanged ValueChanged<bool> onChanged
}) : _activeThumbDecoration = activeThumbDecoration, }) : _activeThumbDecoration = activeThumbDecoration,
_inactiveThumbDecoration = inactiveThumbDecoration, _inactiveThumbDecoration = inactiveThumbDecoration,
_activeTrackColor = activeTrackColor, _activeTrackColor = activeTrackColor,
_inactiveTrackColor = inactiveTrackColor, _inactiveTrackColor = inactiveTrackColor,
_configuration = configuration,
super( super(
value: value, value: value,
activeColor: activeColor, activeColor: activeColor,
...@@ -196,43 +211,19 @@ class _RenderSwitch extends RenderToggleable { ...@@ -196,43 +211,19 @@ class _RenderSwitch extends RenderToggleable {
set activeThumbDecoration(Decoration value) { set activeThumbDecoration(Decoration value) {
if (value == _activeThumbDecoration) if (value == _activeThumbDecoration)
return; return;
_removeActiveThumbListenerIfNeeded();
_activeThumbDecoration = value; _activeThumbDecoration = value;
_addActiveThumbListenerIfNeeded();
markNeedsPaint(); 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 get inactiveThumbDecoration => _inactiveThumbDecoration;
Decoration _inactiveThumbDecoration; Decoration _inactiveThumbDecoration;
set inactiveThumbDecoration(Decoration value) { set inactiveThumbDecoration(Decoration value) {
if (value == _inactiveThumbDecoration) if (value == _inactiveThumbDecoration)
return; return;
_removeInactiveThumbListenerIfNeeded();
_inactiveThumbDecoration = value; _inactiveThumbDecoration = value;
_addInactiveThumbListenerIfNeeded();
markNeedsPaint(); 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 get activeTrackColor => _activeTrackColor;
Color _activeTrackColor; Color _activeTrackColor;
set activeTrackColor(Color value) { set activeTrackColor(Color value) {
...@@ -253,17 +244,20 @@ class _RenderSwitch extends RenderToggleable { ...@@ -253,17 +244,20 @@ class _RenderSwitch extends RenderToggleable {
markNeedsPaint(); markNeedsPaint();
} }
@override ImageConfiguration get configuration => _configuration;
void attach(PipelineOwner owner) { ImageConfiguration _configuration;
super.attach(owner); set configuration (ImageConfiguration value) {
_addInactiveThumbListenerIfNeeded(); assert(value != null);
_addActiveThumbListenerIfNeeded(); if (value == _configuration)
return;
_configuration = value;
markNeedsPaint();
} }
@override @override
void detach() { void detach() {
_removeActiveThumbListenerIfNeeded(); _cachedThumbPainter?.dispose();
_removeInactiveThumbListenerIfNeeded(); _cachedThumbPainter = null;
super.detach(); super.detach();
} }
...@@ -318,22 +312,22 @@ class _RenderSwitch extends RenderToggleable { ...@@ -318,22 +312,22 @@ class _RenderSwitch extends RenderToggleable {
final bool isActive = onChanged != null; final bool isActive = onChanged != null;
final double currentPosition = position.value; 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 the track
Paint paint = new Paint() final Paint paint = new Paint()
..color = trackColor; ..color = trackColor;
double trackHorizontalPadding = kRadialReactionRadius - _kTrackRadius; final double trackHorizontalPadding = kRadialReactionRadius - _kTrackRadius;
Rect trackRect = new Rect.fromLTWH( final Rect trackRect = new Rect.fromLTWH(
offset.dx + trackHorizontalPadding, offset.dx + trackHorizontalPadding,
offset.dy + (size.height - _kTrackHeight) / 2.0, offset.dy + (size.height - _kTrackHeight) / 2.0,
size.width - 2.0 * trackHorizontalPadding, size.width - 2.0 * trackHorizontalPadding,
_kTrackHeight _kTrackHeight
); );
RRect trackRRect = new RRect.fromRectXY(trackRect, _kTrackRadius, _kTrackRadius); final RRect trackRRect = new RRect.fromRectXY(trackRect, _kTrackRadius, _kTrackRadius);
canvas.drawRRect(trackRRect, paint); canvas.drawRRect(trackRRect, paint);
Point thumbPosition = new Point( final Point thumbPosition = new Point(
kRadialReactionRadius + currentPosition * _trackInnerLength, kRadialReactionRadius + currentPosition * _trackInnerLength,
size.height / 2.0 size.height / 2.0
); );
...@@ -342,25 +336,25 @@ class _RenderSwitch extends RenderToggleable { ...@@ -342,25 +336,25 @@ class _RenderSwitch extends RenderToggleable {
BoxPainter thumbPainter; BoxPainter thumbPainter;
if (_inactiveThumbDecoration == null && _activeThumbDecoration == null) { 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) { if (thumbColor != _cachedThumbColor || _cachedThumbPainter == null) {
_cachedThumbColor = thumbColor; _cachedThumbColor = thumbColor;
_cachedThumbPainter = _createDefaultThumbDecoration(thumbColor).createBoxPainter(); _cachedThumbPainter = _createDefaultThumbDecoration(thumbColor).createBoxPainter(markNeedsPaint);
} }
thumbPainter = _cachedThumbPainter; thumbPainter = _cachedThumbPainter;
} else { } else {
Decoration startDecoration = _inactiveThumbDecoration ?? _createDefaultThumbDecoration(inactiveColor); final Decoration startDecoration = _inactiveThumbDecoration ?? _createDefaultThumbDecoration(inactiveColor);
Decoration endDecoration = _activeThumbDecoration ?? _createDefaultThumbDecoration(isActive ? activeTrackColor : inactiveColor); final Decoration endDecoration = _activeThumbDecoration ?? _createDefaultThumbDecoration(isActive ? activeTrackColor : inactiveColor);
thumbPainter = Decoration.lerp(startDecoration, endDecoration, currentPosition).createBoxPainter(); thumbPainter = Decoration.lerp(startDecoration, endDecoration, currentPosition).createBoxPainter(markNeedsPaint);
} }
// The thumb contracts slightly during the animation // The thumb contracts slightly during the animation
double inset = 1.0 - (currentPosition - 0.5).abs() * 2.0; final double inset = 1.0 - (currentPosition - 0.5).abs() * 2.0;
double radius = _kThumbRadius - inset; final double radius = _kThumbRadius - inset;
Rect thumbRect = new Rect.fromLTRB(thumbPosition.x + offset.dx - radius, thumbPainter.paint(
thumbPosition.y + offset.dy - radius, canvas,
thumbPosition.x + offset.dx + radius, thumbPosition.toOffset() + offset,
thumbPosition.y + offset.dy + radius); configuration.copyWith(size: new Size.fromRadius(radius))
thumbPainter.paint(canvas, thumbRect); );
} }
} }
...@@ -291,4 +291,12 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic ...@@ -291,4 +291,12 @@ abstract class RenderToggleable extends RenderConstrainedBox implements Semantic
@override @override
void handleSemanticScrollDown() { } 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 { ...@@ -1039,20 +1039,29 @@ class FractionalOffset {
} }
/// A background image for a box. /// 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 { class BackgroundImage {
/// Creates a background image. /// Creates a background image.
/// ///
/// The [image] argument must not be null. /// The [image] argument must not be null.
BackgroundImage({ const BackgroundImage({
ImageResource image, this.image,
this.fit, this.fit,
this.repeat: ImageRepeat.noRepeat, this.repeat: ImageRepeat.noRepeat,
this.centerSlice, this.centerSlice,
this.colorFilter, this.colorFilter,
this.alignment 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. /// 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; final ImageFit fit;
/// How to paint any portions of the box not covered by the background image. /// How to paint any portions of the box not covered by the background image.
...@@ -1077,44 +1086,6 @@ class BackgroundImage { ...@@ -1077,44 +1086,6 @@ class BackgroundImage {
/// of the right edge of its layout bounds. /// of the right edge of its layout bounds.
final FractionalOffset alignment; 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 @override
bool operator ==(dynamic other) { bool operator ==(dynamic other) {
if (identical(this, other)) if (identical(this, other))
...@@ -1122,19 +1093,19 @@ class BackgroundImage { ...@@ -1122,19 +1093,19 @@ class BackgroundImage {
if (other is! BackgroundImage) if (other is! BackgroundImage)
return false; return false;
final BackgroundImage typedOther = other; final BackgroundImage typedOther = other;
return fit == typedOther.fit && return image == typedOther.image &&
fit == typedOther.fit &&
repeat == typedOther.repeat && repeat == typedOther.repeat &&
centerSlice == typedOther.centerSlice && centerSlice == typedOther.centerSlice &&
colorFilter == typedOther.colorFilter && colorFilter == typedOther.colorFilter &&
alignment == typedOther.alignment && alignment == typedOther.alignment;
_imageResource == typedOther._imageResource;
} }
@override @override
int get hashCode => hashValues(fit, repeat, centerSlice, colorFilter, alignment, _imageResource); int get hashCode => hashValues(image, fit, repeat, centerSlice, colorFilter, alignment);
@override @override
String toString() => 'BackgroundImage($fit, $repeat)'; String toString() => 'BackgroundImage($image, $fit, $repeat)';
} }
/// The shape to use when rendering a BoxDecoration. /// The shape to use when rendering a BoxDecoration.
...@@ -1318,24 +1289,6 @@ class BoxDecoration extends Decoration { ...@@ -1318,24 +1289,6 @@ class BoxDecoration extends Decoration {
return result.join('\n'); 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 @override
bool hitTest(Size size, Point position) { bool hitTest(Size size, Point position) {
assert(shape != null); assert(shape != null);
...@@ -1358,12 +1311,15 @@ class BoxDecoration extends Decoration { ...@@ -1358,12 +1311,15 @@ class BoxDecoration extends Decoration {
} }
@override @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. /// An object that paints a [BoxDecoration] into a canvas.
class _BoxDecorationPainter extends BoxPainter { class _BoxDecorationPainter extends BoxPainter {
_BoxDecorationPainter(this._decoration) { _BoxDecorationPainter(this._decoration, VoidCallback onChange) : super(onChange) {
assert(_decoration != null); assert(_decoration != null);
} }
...@@ -1430,11 +1386,20 @@ class _BoxDecorationPainter extends BoxPainter { ...@@ -1430,11 +1386,20 @@ class _BoxDecorationPainter extends BoxPainter {
_paintBox(canvas, rect, _getBackgroundPaint(rect)); _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; final BackgroundImage backgroundImage = _decoration.backgroundImage;
if (backgroundImage == null) if (backgroundImage == null)
return; 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) if (image == null)
return; return;
paintImage( paintImage(
...@@ -1448,12 +1413,31 @@ class _BoxDecorationPainter extends BoxPainter { ...@@ -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 /// Paint the box decoration into the given location on the given canvas
@override @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); _paintShadows(canvas, rect);
_paintBackgroundColor(canvas, rect); _paintBackgroundColor(canvas, rect);
_paintBackgroundImage(canvas, rect); _paintBackgroundImage(canvas, rect, configuration);
_decoration.border?.paint( _decoration.border?.paint(
canvas, canvas,
rect, rect,
......
...@@ -2,10 +2,15 @@ ...@@ -2,10 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/services.dart';
import 'package:meta/meta.dart';
import 'basic_types.dart'; import 'basic_types.dart';
import 'edge_insets.dart'; import 'edge_insets.dart';
export 'basic_types.dart' show Point, Offset, Size;
export 'edge_insets.dart' show EdgeInsets; export 'edge_insets.dart' show EdgeInsets;
export 'package:flutter/services.dart' show ImageConfiguration;
// This group of classes is intended for painting in cartesian coordinates. // This group of classes is intended for painting in cartesian coordinates.
...@@ -64,26 +69,12 @@ abstract class Decoration { ...@@ -64,26 +69,12 @@ abstract class Decoration {
/// otherwise. /// otherwise.
bool hitTest(Size size, Point position) => true; 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. /// 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 @override
String toString([String prefix = '']) => '$prefix$runtimeType'; String toString([String prefix = '']) => '$prefix$runtimeType';
...@@ -93,16 +84,26 @@ abstract class Decoration { ...@@ -93,16 +84,26 @@ abstract class Decoration {
/// ///
/// [BoxPainter] objects can cache resources so that they can be used /// [BoxPainter] objects can cache resources so that they can be used
/// multiple times. /// 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 /// 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] /// If this object caches resources for painting (e.g. [Paint] objects), the
/// objects), the cache may be flushed when [paint] is called with a /// cache may be flushed when [paint] is called with a new configuration. For
/// new [Rect]. For this reason, it may be more efficient to call /// this reason, it may be more efficient to call
/// [Decoration.createBoxPainter] for each different rectangle that /// [Decoration.createBoxPainter] for each different rectangle that is being
/// is being painted in a particular frame. /// painted in a particular frame.
/// ///
/// For example, if a decoration's owner wants to paint a particular /// For example, if a decoration's owner wants to paint a particular
/// decoration once for its whole size, and once just in the bottom /// decoration once for its whole size, and once just in the bottom
...@@ -110,5 +111,21 @@ abstract class BoxPainter { // ignore: one_member_abstracts ...@@ -110,5 +111,21 @@ abstract class BoxPainter { // ignore: one_member_abstracts
/// However, when its size changes, it could continue using those /// However, when its size changes, it could continue using those
/// same instances, since the previous resources would no longer be /// same instances, since the previous resources would no longer be
/// relevant and thus losing them would not be an issue. /// 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 ...@@ -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 /// The render image attempts to find a size for itself that fits in the given
/// constraints and preserves the image's intrinisc aspect ratio. /// 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 { class RenderImage extends RenderBox {
/// Creates a render box that displays an image. /// Creates a render box that displays an image.
RenderImage({ RenderImage({
...@@ -112,6 +115,9 @@ class RenderImage extends RenderBox { ...@@ -112,6 +115,9 @@ class RenderImage extends RenderBox {
} }
/// How to inscribe the image into the place allocated during layout. /// 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 get fit => _fit;
ImageFit _fit; ImageFit _fit;
set fit (ImageFit value) { set fit (ImageFit value) {
......
...@@ -6,6 +6,7 @@ import 'dart:ui' as ui show ImageFilter; ...@@ -6,6 +6,7 @@ import 'dart:ui' as ui show ImageFilter;
import 'package:flutter/animation.dart'; import 'package:flutter/animation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart'; import 'package:vector_math/vector_math_64.dart';
import 'box.dart'; import 'box.dart';
...@@ -1093,17 +1094,23 @@ enum DecorationPosition { ...@@ -1093,17 +1094,23 @@ enum DecorationPosition {
class RenderDecoratedBox extends RenderProxyBox { class RenderDecoratedBox extends RenderProxyBox {
/// Creates a decorated box. /// Creates a decorated box.
/// ///
/// The [decoration] and [position] arguments must not be null. By default the /// The [decoration], [position], and [configuration] arguments must not be
/// decoration paints behind the child. /// 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({ RenderDecoratedBox({
Decoration decoration, @required Decoration decoration,
DecorationPosition position: DecorationPosition.background, DecorationPosition position: DecorationPosition.background,
ImageConfiguration configuration: ImageConfiguration.empty,
RenderBox child RenderBox child
}) : _decoration = decoration, }) : _decoration = decoration,
_position = position, _position = position,
_configuration = configuration,
super(child) { super(child) {
assert(decoration != null); assert(decoration != null);
assert(position != null); assert(position != null);
assert(configuration != null);
} }
BoxPainter _painter; BoxPainter _painter;
...@@ -1117,10 +1124,9 @@ class RenderDecoratedBox extends RenderProxyBox { ...@@ -1117,10 +1124,9 @@ class RenderDecoratedBox extends RenderProxyBox {
assert(newDecoration != null); assert(newDecoration != null);
if (newDecoration == _decoration) if (newDecoration == _decoration)
return; return;
_removeListenerIfNeeded(); _painter?.dispose();
_painter = null; _painter = null;
_decoration = newDecoration; _decoration = newDecoration;
_addListenerIfNeeded();
markNeedsPaint(); markNeedsPaint();
} }
...@@ -1135,29 +1141,23 @@ class RenderDecoratedBox extends RenderProxyBox { ...@@ -1135,29 +1141,23 @@ class RenderDecoratedBox extends RenderProxyBox {
markNeedsPaint(); markNeedsPaint();
} }
bool get _needsListeners { /// The settings to pass to the decoration when painting, so that it can
return attached && _decoration.needsListeners; /// resolve images appropriately. See [ImageProvider.resolve] and
} /// [BoxPainter.paint].
ImageConfiguration get configuration => _configuration;
void _addListenerIfNeeded() { ImageConfiguration _configuration;
if (_needsListeners) set configuration (ImageConfiguration newConfiguration) {
_decoration.addChangeListener(markNeedsPaint); assert(newConfiguration != null);
} if (newConfiguration == _configuration)
return;
void _removeListenerIfNeeded() { _configuration = newConfiguration;
if (_needsListeners) markNeedsPaint();
_decoration.removeChangeListener(markNeedsPaint);
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_addListenerIfNeeded();
} }
@override @override
void detach() { void detach() {
_removeListenerIfNeeded(); _painter?.dispose();
_painter = null;
super.detach(); super.detach();
} }
...@@ -1170,12 +1170,13 @@ class RenderDecoratedBox extends RenderProxyBox { ...@@ -1170,12 +1170,13 @@ class RenderDecoratedBox extends RenderProxyBox {
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
assert(size.width != null); assert(size.width != null);
assert(size.height != null); assert(size.height != null);
_painter ??= _decoration.createBoxPainter(); _painter ??= _decoration.createBoxPainter(markNeedsPaint);
final ImageConfiguration filledConfiguration = configuration.copyWith(size: size);
if (position == DecorationPosition.background) if (position == DecorationPosition.background)
_painter.paint(context.canvas, offset & size); _painter.paint(context.canvas, offset, filledConfiguration);
super.paint(context, offset); super.paint(context, offset);
if (position == DecorationPosition.foreground) if (position == DecorationPosition.foreground)
_painter.paint(context.canvas, offset & size); _painter.paint(context.canvas, offset, filledConfiguration);
} }
@override @override
...@@ -1183,6 +1184,7 @@ class RenderDecoratedBox extends RenderProxyBox { ...@@ -1183,6 +1184,7 @@ class RenderDecoratedBox extends RenderProxyBox {
super.debugFillDescription(description); super.debugFillDescription(description);
description.add('decoration:'); description.add('decoration:');
description.addAll(_decoration.toString(" ").split('\n')); description.addAll(_decoration.toString(" ").split('\n'));
description.add('configuration: $configuration');
} }
} }
...@@ -1466,11 +1468,11 @@ abstract class CustomPainter { ...@@ -1466,11 +1468,11 @@ abstract class CustomPainter {
/// ///
/// To paint an image on a [Canvas]: /// To paint an image on a [Canvas]:
/// ///
/// 1. Obtain an [ImageResource], for example by using the [ImageCache.load] /// 1. Obtain an [ImageStream], for example by calling [ImageProvider.resolve]
/// method on the [imageCache] singleton. /// on an [AssetImage] or [NetworkImage] object.
/// ///
/// 2. Whenever the [ImageResource]'s underlying [ImageInfo] object changes /// 2. Whenever the [ImageStream]'s underlying [ImageInfo] object changes
/// (see [ImageResource.addListener]), create a new instance of your custom /// (see [ImageStream.addListener]), create a new instance of your custom
/// paint delegate, giving it the new [ImageInfo] object. /// paint delegate, giving it the new [ImageInfo] object.
/// ///
/// 3. In your delegate's [paint] method, call the [Canvas.drawImage], /// 3. In your delegate's [paint] method, call the [Canvas.drawImage],
......
...@@ -465,6 +465,7 @@ class RenderTable extends RenderBox { ...@@ -465,6 +465,7 @@ class RenderTable extends RenderBox {
/// * `children` must either be null or contain lists of all the same length. /// * `children` must either be null or contain lists of all the same length.
/// if `children` is not null, then `rows` must be null. /// if `children` is not null, then `rows` must be null.
/// * [defaultColumnWidth] must not be null. /// * [defaultColumnWidth] must not be null.
/// * [configuration] must not be null (but has a default value).
RenderTable({ RenderTable({
int columns, int columns,
int rows, int rows,
...@@ -472,6 +473,7 @@ class RenderTable extends RenderBox { ...@@ -472,6 +473,7 @@ class RenderTable extends RenderBox {
TableColumnWidth defaultColumnWidth: const FlexColumnWidth(1.0), TableColumnWidth defaultColumnWidth: const FlexColumnWidth(1.0),
TableBorder border, TableBorder border,
List<Decoration> rowDecorations, List<Decoration> rowDecorations,
ImageConfiguration configuration: ImageConfiguration.empty,
Decoration defaultRowDecoration, Decoration defaultRowDecoration,
TableCellVerticalAlignment defaultVerticalAlignment: TableCellVerticalAlignment.top, TableCellVerticalAlignment defaultVerticalAlignment: TableCellVerticalAlignment.top,
TextBaseline textBaseline, TextBaseline textBaseline,
...@@ -481,13 +483,15 @@ class RenderTable extends RenderBox { ...@@ -481,13 +483,15 @@ class RenderTable extends RenderBox {
assert(rows == null || rows >= 0); assert(rows == null || rows >= 0);
assert(rows == null || children == null); assert(rows == null || children == null);
assert(defaultColumnWidth != null); assert(defaultColumnWidth != null);
assert(configuration != null);
_columns = columns ?? (children != null && children.length > 0 ? children.first.length : 0); _columns = columns ?? (children != null && children.length > 0 ? children.first.length : 0);
_rows = rows ?? 0; _rows = rows ?? 0;
_children = new List<RenderBox>()..length = _columns * _rows; _children = new List<RenderBox>()..length = _columns * _rows;
_columnWidths = columnWidths ?? new HashMap<int, TableColumnWidth>(); _columnWidths = columnWidths ?? new HashMap<int, TableColumnWidth>();
_defaultColumnWidth = defaultColumnWidth; _defaultColumnWidth = defaultColumnWidth;
_border = border; _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; _defaultVerticalAlignment = defaultVerticalAlignment;
_textBaseline = textBaseline; _textBaseline = textBaseline;
if (children != null) { if (children != null) {
...@@ -619,30 +623,25 @@ class RenderTable extends RenderBox { ...@@ -619,30 +623,25 @@ class RenderTable extends RenderBox {
set rowDecorations(List<Decoration> value) { set rowDecorations(List<Decoration> value) {
if (_rowDecorations == value) if (_rowDecorations == value)
return; return;
_removeListenersIfNeeded();
_rowDecorations = value; _rowDecorations = value;
_rowDecorationPainters = _rowDecorations != null ? new List<BoxPainter>(_rowDecorations.length) : null; if (_rowDecorationPainters != null) {
_addListenersIfNeeded(); for (BoxPainter painter in _rowDecorationPainters)
} painter?.dispose();
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);
}
} }
_rowDecorationPainters = _rowDecorations != null ? new List<BoxPainter>(_rowDecorations.length) : null;
} }
void _addListenersIfNeeded() { /// The settings to pass to the [rowDecorations] when painting, so that they
Set<Decoration> visitedDecorations = new Set<Decoration>(); /// can resolve images appropriately. See [ImageProvider.resolve] and
if (_rowDecorations != null && attached) { /// [BoxPainter.paint].
for (Decoration decoration in _rowDecorations) { ImageConfiguration get configuration => _configuration;
if (decoration != null && decoration.needsListeners && visitedDecorations.add(decoration)) ImageConfiguration _configuration;
decoration.addChangeListener(markNeedsPaint); 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. /// How cells that do not explicitly specify a vertical alignment are aligned vertically.
...@@ -798,12 +797,15 @@ class RenderTable extends RenderBox { ...@@ -798,12 +797,15 @@ class RenderTable extends RenderBox {
super.attach(owner); super.attach(owner);
for (RenderBox child in _children) for (RenderBox child in _children)
child?.attach(owner); child?.attach(owner);
_addListenersIfNeeded();
} }
@override @override
void detach() { void detach() {
_removeListenersIfNeeded(); if (_rowDecorationPainters != null) {
for (BoxPainter painter in _rowDecorationPainters)
painter?.dispose();
_rowDecorationPainters = null;
}
for (RenderBox child in _children) for (RenderBox child in _children)
child?.detach(); child?.detach();
super.detach(); super.detach();
...@@ -1214,13 +1216,12 @@ class RenderTable extends RenderBox { ...@@ -1214,13 +1216,12 @@ class RenderTable extends RenderBox {
if (_rowDecorations.length <= y) if (_rowDecorations.length <= y)
break; break;
if (_rowDecorations[y] != null) { if (_rowDecorations[y] != null) {
_rowDecorationPainters[y] ??= _rowDecorations[y].createBoxPainter(); _rowDecorationPainters[y] ??= _rowDecorations[y].createBoxPainter(markNeedsPaint);
_rowDecorationPainters[y].paint(canvas, new Rect.fromLTRB( _rowDecorationPainters[y].paint(
offset.dx, canvas,
offset.dy + _rowTops[y], new Offset(offset.dx, offset.dy + _rowTops[y]),
offset.dx + size.width, configuration.copyWith(size: new Size(size.width, _rowTops[y+1] - _rowTops[y]))
offset.dy + _rowTops[y+1] );
));
} }
} }
} }
......
...@@ -3,16 +3,15 @@ ...@@ -3,16 +3,15 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/http.dart' as http; import 'package:flutter/http.dart' as http;
import 'package:mojo/core.dart' as core; import 'package:mojo/core.dart' as core;
import 'package:mojo_services/mojo/asset_bundle/asset_bundle.mojom.dart' as mojom; 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'; import 'shell.dart';
/// A collection of resources used by the application. /// A collection of resources used by the application.
...@@ -43,20 +42,33 @@ import 'shell.dart'; ...@@ -43,20 +42,33 @@ import 'shell.dart';
/// * [NetworkAssetBundle] /// * [NetworkAssetBundle]
/// * [rootBundle] /// * [rootBundle]
abstract class AssetBundle { 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. /// Retrieve a binary resource from the asset bundle as a data stream.
Future<core.MojoDataPipeConsumer> load(String key); 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 @override
String toString() => '$runtimeType@$hashCode()'; String toString() => '$runtimeType@$hashCode()';
} }
/// An [AssetBundle] that loads resources over the network. /// 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 { class NetworkAssetBundle extends AssetBundle {
/// Creates an network asset bundle that resolves asset keys as URLs relative /// Creates an network asset bundle that resolves asset keys as URLs relative
/// to the given base URL. /// to the given base URL.
...@@ -71,52 +83,76 @@ class NetworkAssetBundle extends AssetBundle { ...@@ -71,52 +83,76 @@ class NetworkAssetBundle extends AssetBundle {
return await http.readDataPipe(_urlFromKey(key)); return await http.readDataPipe(_urlFromKey(key));
} }
/// Retrieve an image from the asset bundle.
///
/// Images are cached in the [imageCache].
@override @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 @override
Future<String> loadString(String key) async { Future<dynamic> loadStructuredData(String key, Future<dynamic> parser(String value)) async {
return (await http.get(_urlFromKey(key))).body; assert(key != null);
assert(parser != null);
return parser(await loadString(key));
} }
@override @override
String toString() => '$runtimeType@$hashCode($_baseUrl)'; 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 { abstract class CachingAssetBundle extends AssetBundle {
final Map<String, ImageResource> _imageResourceCache = // TODO(ianh): Replace this with an intelligent cache, see https://github.com/flutter/flutter/issues/3568
<String, ImageResource>{}; final Map<String, Future<String>> _stringCache = <String, Future<String>>{};
final Map<String, Future<String>> _stringCache = final Map<String, Future<dynamic>> _structuredDataCache = <String, Future<dynamic>>{};
<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)));
}
@override @override
ImageResource loadImage(String key) { Future<String> loadString(String key, { bool cache: true }) {
return _imageResourceCache.putIfAbsent(key, () { if (cache)
return new ImageResource(fetchImage(key)); return _stringCache.putIfAbsent(key, () => _fetchString(key));
}); return _fetchString(key);
} }
Future<String> _fetchString(String key) async { Future<String> _fetchString(String key) async {
core.MojoDataPipeConsumer pipe = await load(key); final core.MojoDataPipeConsumer pipe = await load(key);
ByteData data = await core.DataPipeDrainer.drainHandle(pipe); final ByteData data = await core.DataPipeDrainer.drainHandle(pipe);
return new String.fromCharCodes(new Uint8List.view(data.buffer)); 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 @override
Future<String> loadString(String key) { Future<dynamic> loadStructuredData(String key, Future<dynamic> parser(String value)) {
return _stringCache.putIfAbsent(key, () => _fetchString(key)); 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 { ...@@ -127,15 +163,16 @@ class MojoAssetBundle extends CachingAssetBundle {
/// Retrieves the asset bundle located at the given URL, unpacks it, and provides it contents. /// Retrieves the asset bundle located at the given URL, unpacks it, and provides it contents.
factory MojoAssetBundle.fromNetwork(String relativeUrl) { factory MojoAssetBundle.fromNetwork(String relativeUrl) {
mojom.AssetBundleProxy bundle = new mojom.AssetBundleProxy.unbound(); final mojom.AssetBundleProxy bundle = new mojom.AssetBundleProxy.unbound();
_fetchAndUnpackBundle(relativeUrl, bundle); _fetchAndUnpackBundleAsychronously(relativeUrl, bundle);
return new MojoAssetBundle(bundle); return new MojoAssetBundle(bundle);
} }
static Future<Null> _fetchAndUnpackBundle(String relativeUrl, mojom.AssetBundleProxy bundle) async { static Future<Null> _fetchAndUnpackBundleAsychronously(String relativeUrl, mojom.AssetBundleProxy bundle) async {
core.MojoDataPipeConsumer bundleData = await http.readDataPipe(Uri.base.resolve(relativeUrl)); final core.MojoDataPipeConsumer bundleData = await http.readDataPipe(Uri.base.resolve(relativeUrl));
mojom.AssetUnpackerProxy unpacker = shell.connectToApplicationService( final mojom.AssetUnpackerProxy unpacker = shell.connectToApplicationService(
'mojo:asset_bundle', mojom.AssetUnpacker.connectToService); 'mojo:asset_bundle', mojom.AssetUnpacker.connectToService
);
unpacker.unpackZipStream(bundleData, bundle); unpacker.unpackZipStream(bundleData, bundle);
unpacker.close(); unpacker.close();
} }
...@@ -166,11 +203,15 @@ AssetBundle _initRootBundle() { ...@@ -166,11 +203,15 @@ AssetBundle _initRootBundle() {
/// Rather than using [rootBundle] directly, consider obtaining the /// Rather than using [rootBundle] directly, consider obtaining the
/// [AssetBundle] for the current [BuildContext] using [DefaultAssetBundle.of]. /// [AssetBundle] for the current [BuildContext] using [DefaultAssetBundle.of].
/// This layer of indirection lets ancestor widgets substitute a different /// 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 /// directly replying upon the [rootBundle] created at build time. For
/// convenience, the [WidgetsApp] or [MaterialApp] widget at the top of the /// convenience, the [WidgetsApp] or [MaterialApp] widget at the top of the
/// widget hierarchy configures the [DefaultAssetBundle] to be the [rootBundle]. /// 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: /// See also:
/// ///
/// * [DefaultAssetBundle] /// * [DefaultAssetBundle]
......
...@@ -2,89 +2,9 @@ ...@@ -2,89 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:ui' show hashValues, Image;
import 'package:flutter/foundation.dart'; import 'image_stream.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);
}
const int _kDefaultSize = 1000; const int _kDefaultSize = 1000;
...@@ -93,20 +13,21 @@ 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 /// 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 /// size can be adjusted using [maximumSize]. Images that are actively in use
/// (i.e. to which the application is holding references, either via /// (i.e. to which the application is holding references, either via
/// [ImageResource] objects, [ImageInfo] objects, or raw [ui.Image] objects) may /// [ImageStream] objects, [ImageStreamCompleter] objects, [ImageInfo] objects,
/// get evicted from the cache (and thus need to be refetched from the network /// or raw [ui.Image] objects) may get evicted from the cache (and thus need to
/// if they are referenced in the [load] method), but the raw bits are kept in /// be refetched from the network if they are referenced in the [putIfAbsent]
/// memory for as long as the application is using them. /// 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 /// Generally this class is not used directly. The [ImageProvider] class and its
/// custom [ImageProvider]. /// subclasses automatically handle the caching of images.
class ImageCache { class ImageCache {
ImageCache._(); final LinkedHashMap<Object, ImageStreamCompleter> _cache = new LinkedHashMap<Object, ImageStreamCompleter>();
final LinkedHashMap<ImageProvider, ImageResource> _cache =
new LinkedHashMap<ImageProvider, ImageResource>();
/// Maximum number of entries to store in the cache. /// Maximum number of entries to store in the cache.
/// ///
...@@ -134,53 +55,35 @@ class ImageCache { ...@@ -134,53 +55,35 @@ class ImageCache {
} }
} }
/// Calls the [ImageProvider.loadImage] method on the given image provider, if /// Returns the previously cached [ImageStream] for the given key, if available;
/// necessary, and returns an [ImageResource] that encapsulates a [Future] for /// if not, calls the given callback to obtain it first. In either case, the
/// the given image. /// key is moved to the "most recently used" position.
/// ///
/// If the given [ImageProvider] has already been used and is still in the /// The arguments cannot be null. The `loader` cannot return null.
/// cache, then the [ImageResource] object is immediately usable and the ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader()) {
/// provider is not called. assert(key != null);
ImageResource loadProvider(ImageProvider provider) { assert(loader != null);
assert(provider != null); ImageStreamCompleter result = _cache[key];
ImageResource result = _cache[provider];
if (result != null) { 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 { } else {
if (_cache.length == maximumSize && maximumSize > 0) if (_cache.length == maximumSize && maximumSize > 0)
_cache.remove(_cache.keys.first); _cache.remove(_cache.keys.first);
result = new ImageResource(provider.loadImage());; result = loader();
} }
if (maximumSize > 0) { if (maximumSize > 0) {
assert(_cache.length < maximumSize); assert(_cache.length < maximumSize);
_cache[provider] = result; _cache[key] = result;
} }
assert(_cache.length <= maximumSize); assert(_cache.length <= maximumSize);
return result; 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 singleton that implements the Flutter framework's image cache.
/// ///
/// The simplest use of this object is as follows: /// The cache is used internally by [ImageProvider] and should generally not be
/// /// accessed directly.
/// ```dart final ImageCache imageCache = new ImageCache();
/// imageCache.load(myImageUrl).first.then(myImageHandler);
/// ```
///
/// ...where `myImageHandler` is a function with one argument, an [ImageInfo]
/// object.
final ImageCache imageCache = new ImageCache._();
...@@ -14,6 +14,9 @@ import 'package:mojo/core.dart' show MojoDataPipeConsumer; ...@@ -14,6 +14,9 @@ import 'package:mojo/core.dart' show MojoDataPipeConsumer;
/// in the data pipe as an image. If successful, the returned [Future] resolves /// in the data pipe as an image. If successful, the returned [Future] resolves
/// to the decoded image. Otherwise, the [Future] resolves to [null]. /// to the decoded image. Otherwise, the [Future] resolves to [null].
Future<ui.Image> decodeImageFromDataPipe(MojoDataPipeConsumer consumerHandle) { 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>(); Completer<ui.Image> completer = new Completer<ui.Image>();
ui.decodeImageFromDataPipe(consumerHandle.handle.h, (ui.Image image) { ui.decodeImageFromDataPipe(consumerHandle.handle.h, (ui.Image image) {
completer.complete(image); completer.complete(image);
......
// 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';
import 'dart:ui' show Size, Locale, hashValues;
import 'dart:ui' as ui show Image;
import 'package:flutter/foundation.dart';
import 'package:flutter/http.dart' as http;
import 'package:meta/meta.dart';
import 'package:mojo/core.dart' as mojo;
import 'asset_bundle.dart';
import 'image_cache.dart';
import 'image_decoder.dart';
import 'image_stream.dart';
/// Configuration information passed to the [ImageProvider.resolve] method to
/// select a specific image.
class ImageConfiguration {
/// Creates an object holding the configuration information for an [ImageProvider].
///
/// All the arguments are optional. Configuration information is merely
/// advisory and best-effort.
const ImageConfiguration({
this.bundle,
this.devicePixelRatio,
this.locale,
this.size,
this.platform
});
/// Creates an object holding the configuration information for an [ImageProvider].
///
/// All the arguments are optional. Configuration information is merely
/// advisory and best-effort.
ImageConfiguration copyWith({
AssetBundle bundle,
double devicePixelRatio,
Locale locale,
Size size,
String platform
}) {
return new ImageConfiguration(
bundle: bundle ?? this.bundle,
devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
locale: locale ?? this.locale,
size: size ?? this.size,
platform: platform ?? this.platform
);
}
/// The preferred [AssetBundle] to use if the [ImageProvider] needs one and
/// does not have one already selected.
final AssetBundle bundle;
/// The device pixel ratio where the image will be shown.
final double devicePixelRatio;
/// The language and region for which to select the image.
final Locale locale;
/// The size at which the image will be rendered.
final Size size;
/// A string (same as [Platform.operatingSystem]) that represents the platform
/// for which assets should be used. This allows images to be specified in a
/// platform-neutral fashion yet use different assets on different platforms,
/// to match local conventions e.g. for color matching or shadows.
final String platform;
/// An image configuration that provides no additional information.
///
/// Useful when resolving an [ImageProvider] without any context.
static const ImageConfiguration empty = const ImageConfiguration();
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType)
return false;
final ImageConfiguration typedOther = other;
return typedOther.bundle == bundle
&& typedOther.devicePixelRatio == devicePixelRatio
&& typedOther.locale == locale
&& typedOther.size == size
&& typedOther.platform == platform;
}
@override
int get hashCode => hashValues(bundle, devicePixelRatio, locale, size, platform);
@override
String toString() {
StringBuffer result = new StringBuffer();
result.write('ImageConfiguration(');
bool hasArguments = false;
if (bundle != null) {
if (hasArguments)
result.write(', ');
result.write('bundle: $bundle');
}
if (devicePixelRatio != null) {
if (hasArguments)
result.write(', ');
result.write('devicePixelRatio: $devicePixelRatio');
}
if (locale != null) {
if (hasArguments)
result.write(', ');
result.write('locale: $locale');
}
if (size != null) {
if (hasArguments)
result.write(', ');
result.write('size: $size');
}
if (platform != null) {
if (hasArguments)
result.write(', ');
result.write('platform: $platform');
}
result.write(')');
return result.toString();
}
}
/// Identifies an image without committing to the precise final asset. This
/// allows a set of images to be identified and for the precise image to later
/// be resolved based on the environment, e.g. the device pixel ratio.
///
/// To obtain an [ImageStream] from an [ImageProvider], call [resolve],
/// passing it an [ImageConfiguration] object.
///
/// ImageProvides uses the global [imageCache] to cache images.
///
/// The type argument `T` is the type of the object used to represent a resolved
/// configuration. This is also the type used for the key in the image cache. It
/// should be immutable and implement [operator ==] and [hashCode]. Subclasses should
/// subclass a variant of [ImageProvider] with an explicit `T` type argument.
///
/// The type argument does not have to be specified when using the type as an
/// argument (where any image provider is acceptable).
@optionalTypeArgs
abstract class ImageProvider<T> {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const ImageProvider();
/// Resolves this image provider using the given `configuration`, returning
/// an [ImageStream].
///
/// This is the public entry-point of the [ImageProvider] class hierarchy.
///
/// Subclasses should implement [obtainKey] and [load], which are used by this
/// method.
ImageStream resolve(ImageConfiguration configuration) {
assert(configuration != null);
final ImageStream stream = new ImageStream();
T obtainedKey;
obtainKey(configuration).then((T key) {
obtainedKey = key;
stream.setCompleter(imageCache.putIfAbsent(key, () => load(key)));
}).catchError(
(dynamic exception, StackTrace stack) async {
FlutterError.reportError(new FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: 'while resolving an image',
silent: true, // could be a network error or whatnot
informationCollector: (StringBuffer information) {
information.writeln('Image provider: $this');
information.writeln('Image configuration: $configuration');
if (obtainedKey != null)
information.writeln('Image key: $obtainedKey');
}
));
return null;
}
);
return stream;
}
/// Converts an ImageProvider's settings plus an ImageConfiguration to a key
/// that describes the precise image to load.
///
/// The type of the key is determined by the subclass. It is a value that
/// unambiguously identifies the image (_including its scale_) that the [load]
/// method will fetch. Different [ImageProvider]s given the same constructor
/// arguments and [ImageConfiguration] objects should return keys that are
/// '==' to each other (possibly by using a class for the key that itself
/// implements [operator ==]).
@protected
Future<T> obtainKey(ImageConfiguration configuration);
/// Converts a key into an [ImageStreamCompleter], and begins fetching the
/// image.
@protected
ImageStreamCompleter load(T key);
@override
String toString() => '$runtimeType()';
}
/// A subclass of [ImageProvider] that knows how to invoke
/// [decodeImageFromDataPipe].
///
/// This factors out the common logic of many [ImageProvider] classes,
/// simplifying what subclasses must implement to just three small methods:
///
/// * [obtainKey], to resolve an [ImageConfiguration].
/// * [getScale], to determine the scale of the image associated with a
/// particular key.
/// * [loadDataPipe], to obtain the [mojo.MojoDataPipeConsumer] object that
/// contains the actual image data.
abstract class DataPipeImageProvider<T> extends ImageProvider<T> {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const DataPipeImageProvider();
@override
ImageStreamCompleter load(T key) {
return new OneFrameImageStreamCompleter(_loadAsync(key));
}
Future<ImageInfo> _loadAsync(T key) async {
final mojo.MojoDataPipeConsumer dataPipe = await loadDataPipe(key);
if (dataPipe == null)
throw 'Unable to read data';
final ui.Image image = await decodeImage(dataPipe);
if (image == null)
throw 'Unable to decode image data';
return new ImageInfo(image: image, scale: getScale(key));
}
/// Converts raw image data from a [mojo.MojoDataPipeConsumer] data pipe into
/// a decoded [ui.Image] which can be passed to a [Canvas].
///
/// By default, this just uses [decodeImageFromDataPipe]. This method could be
/// overridden in subclasses (e.g. for testing).
Future<ui.Image> decodeImage(mojo.MojoDataPipeConsumer pipe) => decodeImageFromDataPipe(pipe);
/// Returns the data pipe that contains the image data to decode.
///
/// Must be implemented by subclasses of [DataPipeImageProvider].
@protected
Future<mojo.MojoDataPipeConsumer> loadDataPipe(T key);
/// Returns the scale to use when creating the [ImageInfo] for the given key.
///
/// Must be implemented by subclasses of [DataPipeImageProvider].
@protected
double getScale(T key);
}
/// Fetches the given URL from the network, associating it with the given scale.
///
/// Cache headers from the server are ignored.
// TODO(ianh): Find some way to honour cache headers to the extent that when the
// last reference to an image is released, we proactively evict the image from
// our cache if the headers describe the image as having expired at that point.
class NetworkImage extends DataPipeImageProvider<NetworkImage> {
/// Creates an object that fetches the image at the given URL.
///
/// The arguments must not be null.
const NetworkImage(this.url, { this.scale: 1.0 });
/// The URL from which the image will be fetched.
final String url;
/// The scale to place in the [ImageInfo] object of the image.
final double scale;
@override
Future<NetworkImage> obtainKey(ImageConfiguration configuration) {
return new SynchronousFuture<NetworkImage>(this);
}
@override
Future<mojo.MojoDataPipeConsumer> loadDataPipe(NetworkImage key) async {
assert(key == this);
return http.readDataPipe(Uri.base.resolve(key.url));
}
@override
double getScale(NetworkImage key) {
assert(key == this);
return key.scale;
}
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType)
return false;
final NetworkImage typedOther = other;
return url == typedOther.url
&& scale == typedOther.scale;
}
@override
int get hashCode => hashValues(url, scale);
@override
String toString() => '$runtimeType("$url", scale: $scale)';
}
/// Key for the image obtained by an [AssetImage] or [AssetBundleImageProvider].
///
/// This is used to identify the precise resource in the [imageCache].
class AssetBundleImageKey {
/// Creates the key for an [AssetImage] or [AssetBundleImageProvider].
///
/// The arguments must not be null.
const AssetBundleImageKey({
@required this.bundle,
@required this.name,
@required this.scale
});
/// The bundle from which the image will be obtained.
///
/// The image is obtained by calling [AssetBundle.load] on the given [bundle]
/// using the key given by [name].
final AssetBundle bundle;
/// The key to use to obtain the resource from the [bundle]. This is the
/// argument passed to [AssetBundle.load].
final String name;
/// The scale to place in the [ImageInfo] object of the image.
final double scale;
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType)
return false;
final AssetBundleImageKey typedOther = other;
return bundle == typedOther.bundle
&& name == typedOther.name
&& scale == typedOther.scale;
}
@override
int get hashCode => hashValues(bundle, name, scale);
@override
String toString() => '$runtimeType(bundle: $bundle, name: $name, scale: $scale)';
}
/// A subclass of [DataPipeImageProvider] that knows about [AssetBundle]s.
///
/// This factors out the common logic of [AssetBundle]-based [ImageProvider]
/// classes, simplifying what subclasses must implement to just [obtainKey].
abstract class AssetBundleImageProvider extends DataPipeImageProvider<AssetBundleImageKey> {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const AssetBundleImageProvider();
@override
Future<AssetBundleImageKey> obtainKey(ImageConfiguration configuration);
@override
Future<mojo.MojoDataPipeConsumer> loadDataPipe(AssetBundleImageKey key) async {
return key.bundle.load(key.name);
}
@override
double getScale(AssetBundleImageKey key) {
return key.scale;
}
}
/// Fetches an image from an [AssetBundle], associating it with the given scale.
///
/// This implementation requires an explicit final [name] and [scale] on
/// construction, and ignores the device pixel ratio and size in the
/// configuration passed into [resolve]. For a resolution-aware variant that
/// uses the configuration to pick an appropriate image based on the device
/// pixel ratio and size, see [AssetImage].
class ExactAssetImage extends AssetBundleImageProvider {
/// Creates an object that fetches the given image from an asset bundle.
///
/// The [name] and [scale] arguments must not be null. The [scale] arguments
/// defaults to 1.0. The [bundle] argument may be null, in which case the
/// bundle provided in the [ImageConfiguration] passed to the [resolve] call
/// will be used instead.
ExactAssetImage(this.name, {
this.scale: 1.0,
this.bundle
}) {
assert(name != null);
assert(scale != null);
}
/// The key to use to obtain the resource from the [bundle]. This is the
/// argument passed to [AssetBundle.load].
final String name;
/// The scale to place in the [ImageInfo] object of the image.
final double scale;
/// 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;
@override
Future<AssetBundleImageKey> obtainKey(ImageConfiguration configuration) {
return new SynchronousFuture<AssetBundleImageKey>(new AssetBundleImageKey(
bundle: bundle ?? configuration.bundle ?? rootBundle,
name: name,
scale: scale
));
}
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType)
return false;
final ExactAssetImage typedOther = other;
return name == typedOther.name
&& scale == typedOther.scale
&& bundle == typedOther.bundle;
}
@override
int get hashCode => hashValues(name, scale, bundle);
@override
String toString() => '$runtimeType(name: $name, scale: $scale, bundle: $bundle)';
}
// 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 @@ ...@@ -5,11 +5,13 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui' as ui show Image; import 'dart:ui' as ui show Image;
import 'package:meta/meta.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
/// A [ui.Image] object with its corresponding scale. /// 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. /// actual data of the image once it has been obtained.
class ImageInfo { class ImageInfo {
/// Creates an [ImageInfo] object for the given image and scale. /// Creates an [ImageInfo] object for the given image and scale.
...@@ -44,52 +46,119 @@ class ImageInfo { ...@@ -44,52 +46,119 @@ class ImageInfo {
/// Signature for callbacks reporting that an image is available. /// Signature for callbacks reporting that an image is available.
/// ///
/// Used by [ImageResource]. /// Used by [ImageStream].
typedef void ImageListener(ImageInfo image); typedef void ImageListener(ImageInfo image);
/// A handle to an image resource. /// 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 /// (together represented by an [ImageInfo] object). The underlying image object
/// might change over time, either because the image is animating or because the /// might change over time, either because the image is animating or because the
/// underlying image resource was mutated. /// 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. /// 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, /// Once an [ImageStreamCompleter] is available, call [setCompleter].
/// it notifies the [ImageListener]s that have been registered with ImageStream();
/// [addListener].
ImageResource(this._futureImage) { /// The completer that has been assigned to this image stream.
_futureImage.then( ///
_handleImageLoaded, /// Generally there is no need to deal with the completer directly.
onError: (dynamic exception, dynamic stack) { ImageStreamCompleter get completer => _completer;
_handleImageError('while loading an image', exception, stack); 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; /// Adds a listener callback that is called whenever a concrete [ImageInfo]
Future<ImageInfo> _futureImage; /// object is available. If a concrete image is already available, this object
ImageInfo _image; /// will call the listener synchronously.
final List<ImageListener> _listeners = new List<ImageListener>(); 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 /// The key may change once in the lifetime of the object. When it changes, it
/// [addListener] to be notified whenever a a concrete image is available. /// will go from being different than other [ImageStream]'s keys to
Future<ImageInfo> get first => _futureImage; /// 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] /// Adds a listener callback that is called whenever a concrete [ImageInfo]
/// object is available. If a concrete image is already available, this object /// object is available. If a concrete image is already available, this object
/// will call the listener synchronously. /// will call the listener synchronously.
void addListener(ImageListener listener) { void addListener(ImageListener listener) {
_listeners.add(listener); _listeners.add(listener);
if (_resolved) { if (_current != null) {
try { try {
listener(_image); listener(_current);
} catch (exception, stack) { } catch (exception, stack) {
_handleImageError('by a synchronously-called image listener', exception, stack); _handleImageError('by a synchronously-called image listener', exception, stack);
} }
...@@ -101,18 +170,16 @@ class ImageResource { ...@@ -101,18 +170,16 @@ class ImageResource {
_listeners.remove(listener); _listeners.remove(listener);
} }
void _handleImageLoaded(ImageInfo image) { /// Calls all the registered listeners to notify them of a new image.
_image = image; @protected
_resolved = true; void setImage(ImageInfo image) {
_notifyListeners(); _current = image;
} if (_listeners.isEmpty)
return;
void _notifyListeners() {
assert(_resolved);
List<ImageListener> localListeners = new List<ImageListener>.from(_listeners); List<ImageListener> localListeners = new List<ImageListener>.from(_listeners);
for (ImageListener listener in localListeners) { for (ImageListener listener in localListeners) {
try { try {
listener(_image); listener(image);
} catch (exception, stack) { } catch (exception, stack) {
_handleImageError('by an image listener', exception, stack); _handleImageError('by an image listener', exception, stack);
} }
...@@ -130,14 +197,37 @@ class ImageResource { ...@@ -130,14 +197,37 @@ class ImageResource {
@override @override
String toString() { String toString() {
StringBuffer result = new StringBuffer(); final List<String> description = <String>[];
result.write('$runtimeType('); debugFillDescription(description);
if (!_resolved) return '$runtimeType(${description.join("; ")})';
result.write('unresolved'); }
/// 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 else
result.write('$_image'); description.add('$_current');
result.write('; ${_listeners.length} listener${_listeners.length == 1 ? "" : "s" }'); description.add('${_listeners.length} listener${_listeners.length == 1 ? "" : "s" }');
result.write(')'); }
return result.toString();
// 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'; ...@@ -6,13 +6,12 @@ import 'dart:async';
import 'dart:ui' as ui show window; import 'dart:ui' as ui show window;
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'asset_vendor.dart';
import 'banner.dart'; import 'banner.dart';
import 'basic.dart'; import 'basic.dart';
import 'binding.dart'; import 'binding.dart';
import 'container.dart';
import 'framework.dart'; import 'framework.dart';
import 'locale_query.dart'; import 'locale_query.dart';
import 'media_query.dart'; import 'media_query.dart';
...@@ -28,8 +27,8 @@ typedef Future<LocaleQueryData> LocaleChangedCallback(Locale locale); ...@@ -28,8 +27,8 @@ typedef Future<LocaleQueryData> LocaleChangedCallback(Locale locale);
/// required for an application. /// required for an application.
/// ///
/// See also: [CheckedModeBanner], [DefaultTextStyle], [MediaQuery], /// See also: [CheckedModeBanner], [DefaultTextStyle], [MediaQuery],
/// [LocaleQuery], [AssetVendor], [Title], [Navigator], [Overlay], /// [LocaleQuery], [Title], [Navigator], [Overlay], [SemanticsDebugger] (the
/// [SemanticsDebugger] (the widgets wrapped by this one). /// widgets wrapped by this one).
/// ///
/// The [onGenerateRoute] argument is required, and corresponds to /// The [onGenerateRoute] argument is required, and corresponds to
/// [Navigator.onGenerateRoute]. /// [Navigator.onGenerateRoute].
...@@ -179,9 +178,6 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv ...@@ -179,9 +178,6 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
data: new MediaQueryData.fromWindow(ui.window), data: new MediaQueryData.fromWindow(ui.window),
child: new LocaleQuery( child: new LocaleQuery(
data: _localeData, data: _localeData,
child: new AssetVendor(
bundle: rootBundle,
devicePixelRatio: ui.window.devicePixelRatio,
child: new Title( child: new Title(
title: config.title, title: config.title,
brightness: config.brightness, brightness: config.brightness,
...@@ -194,7 +190,6 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv ...@@ -194,7 +190,6 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
) )
) )
) )
)
); );
if (config.textStyle != null) { if (config.textStyle != null) {
new DefaultTextStyle( 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');
}
}
...@@ -50,6 +50,9 @@ export 'package:flutter/rendering.dart' show ...@@ -50,6 +50,9 @@ export 'package:flutter/rendering.dart' show
ViewportAnchor, ViewportAnchor,
ViewportDimensions, ViewportDimensions,
ViewportDimensionsChangeCallback; ViewportDimensionsChangeCallback;
export 'package:flutter/services.dart' show
AssetImage,
NetworkImage;
// PAINTING NODES // PAINTING NODES
...@@ -181,46 +184,6 @@ class BackdropFilter extends SingleChildRenderObjectWidget { ...@@ -181,46 +184,6 @@ class BackdropFilter extends SingleChildRenderObjectWidget {
} }
} }
/// 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) => new RenderDecoratedBox(decoration: decoration, position: position);
@override
void updateRenderObject(BuildContext context, RenderDecoratedBox renderObject) {
renderObject
..decoration = decoration
..position = position;
}
}
/// A widget that provides a canvas on which to draw during the paint phase. /// A widget that provides a canvas on which to draw during the paint phase.
/// ///
/// When asked to paint, [CustomPaint] first asks its [painter] to paint on the /// When asked to paint, [CustomPaint] first asks its [painter] to paint on the
...@@ -1363,130 +1326,6 @@ class Viewport extends SingleChildRenderObjectWidget { ...@@ -1363,130 +1326,6 @@ class Viewport extends SingleChildRenderObjectWidget {
} }
// CONTAINER
/// 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');
}
}
// LAYOUT NODES // LAYOUT NODES
/// A widget that uses the block layout algorithm for its children. /// A widget that uses the block layout algorithm for its children.
...@@ -2394,14 +2233,10 @@ class Text extends StatelessWidget { ...@@ -2394,14 +2233,10 @@ class Text extends StatelessWidget {
/// A widget that displays a [ui.Image] directly. /// A widget that displays a [ui.Image] directly.
/// ///
/// This widget is rarely used directly. Instead, consider using [AssetImage] or /// The image is painted using [paintImage], which describes the meanings of the
/// [NetworkImage], depending on whather you wish to display an image from the /// various fields on this class in more detail.
/// assert bundle or from the network.
///
/// See also:
/// ///
/// * [AssetImage] /// This widget is rarely used directly. Instead, consider using [Image].
/// * [NetworkImage]
class RawImage extends LeafRenderObjectWidget { class RawImage extends LeafRenderObjectWidget {
/// Creates a widget that displays an image. /// Creates a widget that displays an image.
/// ///
...@@ -2446,6 +2281,9 @@ class RawImage extends LeafRenderObjectWidget { ...@@ -2446,6 +2281,9 @@ class RawImage extends LeafRenderObjectWidget {
final Color color; final Color color;
/// How to inscribe the image into the place allocated during layout. /// 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; final ImageFit fit;
/// How to align the image within its bounds. /// How to align the image within its bounds.
...@@ -2517,252 +2355,10 @@ class RawImage extends LeafRenderObjectWidget { ...@@ -2517,252 +2355,10 @@ class RawImage extends LeafRenderObjectWidget {
} }
} }
/// A widget that displays an [ImageResource].
///
/// An image resource differs from an image in that it might yet let be loaded
/// from the underlying storage (e.g., the asset bundle or the network) and it
/// might change over time (e.g., an animated image).
///
/// This widget is rarely used directly. Instead, consider using [AssetImage] or
/// [NetworkImage], depending on whather you wish to display an image from the
/// assert bundle or from the network.
class RawImageResource extends StatefulWidget {
/// Creates a widget that displays an [ImageResource].
///
/// The [image] and [repeat] arguments must not be null.
RawImageResource({
Key key,
@required this.image,
this.width,
this.height,
this.color,
this.fit,
this.alignment,
this.repeat: ImageRepeat.noRepeat,
this.centerSlice
}) : super(key: key) {
assert(image != null);
}
/// The image to display.
final ImageResource 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.
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;
@override
_RawImageResourceState createState() => new _RawImageResourceState();
@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 _RawImageResourceState extends State<RawImageResource> {
@override
void initState() {
super.initState();
config.image.addListener(_handleImageChanged);
}
ImageInfo _resolvedImage;
void _handleImageChanged(ImageInfo resolvedImage) {
setState(() {
_resolvedImage = resolvedImage;
});
}
@override
void dispose() {
config.image.removeListener(_handleImageChanged);
super.dispose();
}
@override
void didUpdateConfig(RawImageResource oldConfig) {
if (config.image != oldConfig.image) {
oldConfig.image.removeListener(_handleImageChanged);
config.image.addListener(_handleImageChanged);
}
}
@override
Widget build(BuildContext context) {
return new RawImage(
image: _resolvedImage?.image,
width: config.width,
height: config.height,
scale: _resolvedImage == null ? 1.0 : _resolvedImage.scale,
color: config.color,
fit: config.fit,
alignment: config.alignment,
repeat: config.repeat,
centerSlice: config.centerSlice
);
}
}
/// A widget that displays an image loaded from the network.
class NetworkImage extends StatelessWidget {
/// Creates a widget that displays an image loaded from the network.
///
/// The [src], [scale], and [repeat] arguments must not be null.
NetworkImage({
Key key,
@required this.src,
this.width,
this.height,
this.scale: 1.0,
this.color,
this.fit,
this.alignment,
this.repeat: ImageRepeat.noRepeat,
this.centerSlice
}) : super(key: key) {
assert(src != null);
assert(scale != null);
assert(repeat != null);
}
/// The URL from which to load the image.
final String src;
/// 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;
/// Specifies the image's scale.
///
/// Used when determining the best display size for the image.
final double scale;
/// 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.
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;
@override
Widget build(BuildContext context) {
ImageResource imageResource = imageCache.load(src, scale: scale);
return new RawImageResource(
key: key == null ? new ObjectKey(imageResource) : null,
image: imageResource,
width: width,
height: height,
color: color,
fit: fit,
alignment: alignment,
repeat: repeat,
centerSlice: centerSlice
);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('src: $src');
if (width != null)
description.add('width: $width');
if (height != null)
description.add('height: $height');
if (scale != 1.0)
description.add('scale: $scale');
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');
}
}
/// A widget that determines the default asset bundle for its descendants. /// A widget that determines the default asset bundle for its descendants.
/// ///
/// For example, used by [AssetImage] to determine which bundle to use if no /// For example, used by [Image] to determine which bundle to use for
/// bundle is specified explicitly. /// [AssetImage]s if no bundle is specified explicitly.
class DefaultAssetBundle extends InheritedWidget { class DefaultAssetBundle extends InheritedWidget {
/// Creates a widget that determines the default asset bundle for its descendants. /// Creates a widget that determines the default asset bundle for its descendants.
/// ///
...@@ -2784,11 +2380,6 @@ class DefaultAssetBundle extends InheritedWidget { ...@@ -2784,11 +2380,6 @@ class DefaultAssetBundle extends InheritedWidget {
/// ///
/// If there is no [DefaultAssetBundle] ancestor widget in the tree /// If there is no [DefaultAssetBundle] ancestor widget in the tree
/// at the given context, then this will return the [rootBundle]. /// at the given context, then this will return the [rootBundle].
/// The [rootBundle] does not automatically select images based on
/// the current device pixel ratio. To get an asset bundle that
/// automatically performs pixel-density-aware asset resolution, use
/// a [MaterialApp], [WidgetsApp], or [AssetVendor] widget, which
/// introduce a suitably-configured [DefaultAssetBundle] widget.
static AssetBundle of(BuildContext context) { static AssetBundle of(BuildContext context) {
DefaultAssetBundle result = context.inheritFromWidgetOfExactType(DefaultAssetBundle); DefaultAssetBundle result = context.inheritFromWidgetOfExactType(DefaultAssetBundle);
return result?.bundle ?? rootBundle; return result?.bundle ?? rootBundle;
...@@ -2798,220 +2389,6 @@ class DefaultAssetBundle extends InheritedWidget { ...@@ -2798,220 +2389,6 @@ class DefaultAssetBundle extends InheritedWidget {
bool updateShouldNotify(DefaultAssetBundle old) => bundle != old.bundle; bool updateShouldNotify(DefaultAssetBundle old) => bundle != old.bundle;
} }
/// A widget that displays an image provided by an [ImageProvider].
///
/// This widget lets you customize how images are loaded by supplying your own
/// image provider. Internally, [NetworkImage] uses an [ImageProvider] that
/// loads the image from the network.
class AsyncImage extends StatelessWidget {
/// Creates a widget that displays an image provided by an [ImageProvider].
///
/// The [provider] and [repeat] arguments must not be null.
AsyncImage({
Key key,
@required this.provider,
this.width,
this.height,
this.color,
this.fit,
this.alignment,
this.repeat: ImageRepeat.noRepeat,
this.centerSlice
}) : super(key: key) {
assert(provider != null);
assert(repeat != null);
}
/// The object that will provide the image.
final ImageProvider provider;
/// 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.
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;
@override
Widget build(BuildContext context) {
ImageResource imageResource = imageCache.loadProvider(provider);
return new RawImageResource(
key: key == null ? new ObjectKey(imageResource) : null,
image: imageResource,
width: width,
height: height,
color: color,
fit: fit,
alignment: alignment,
repeat: repeat,
centerSlice: centerSlice
);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('provider: $provider');
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');
}
}
/// A widget that displays an image from an [AssetBundle].
///
/// By default, asset image will load the image from the closest enclosing
/// [DefaultAssetBundle].
///
/// To get an asset bundle that automatically performs
/// pixel-density-aware asset resolution, use a [MaterialApp],
/// [WidgetsApp], or [AssetVendor] widget, which introduce a
/// suitably-configured [DefaultAssetBundle] widget.
class AssetImage extends StatelessWidget {
/// Creates an [AssetImage].
///
/// The `name` argument must not be null.
// Don't add asserts here unless absolutely necessary, since it will
// require removing the const constructor, which is an API change.
const AssetImage({
Key key,
@required this.name,
this.bundle,
this.width,
this.height,
this.color,
this.fit,
this.alignment,
this.repeat: ImageRepeat.noRepeat,
this.centerSlice
}) : super(key: key);
/// The name of the image in the assert bundle.
final String name;
/// The bundle from which to load the image.
///
/// If null, the image will be loaded from the closest enclosing
/// [DefaultAssetBundle].
final AssetBundle bundle;
/// 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.
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;
@override
Widget build(BuildContext context) {
ImageResource imageResource = (bundle ?? DefaultAssetBundle.of(context)).loadImage(name);
return new RawImageResource(
key: key == null ? new ObjectKey(imageResource) : null,
image: imageResource,
width: width,
height: height,
color: color,
fit: fit,
alignment: alignment,
repeat: repeat,
centerSlice: centerSlice
);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('name: $name');
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');
if (bundle != null)
description.add('bundle: $bundle');
}
}
/// An adapter for placing a specific [RenderBox] in the widget tree. /// An adapter for placing a specific [RenderBox] in the widget tree.
/// ///
/// A given render object can be placed at most once in the widget tree. This /// A given render object can be placed at most once in the widget tree. This
......
// 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 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'basic.dart'; import 'basic.dart';
import 'container.dart';
import 'framework.dart'; import 'framework.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
......
...@@ -7,6 +7,7 @@ import 'dart:async'; ...@@ -7,6 +7,7 @@ import 'dart:async';
import 'package:flutter/rendering.dart' show RenderStack; import 'package:flutter/rendering.dart' show RenderStack;
import 'basic.dart'; import 'basic.dart';
import 'container.dart';
import 'framework.dart'; import 'framework.dart';
import 'overlay.dart'; import 'overlay.dart';
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'basic.dart'; import 'basic.dart';
import 'container.dart';
import 'framework.dart'; import 'framework.dart';
import 'gesture_detector.dart'; import 'gesture_detector.dart';
import 'navigator.dart'; import 'navigator.dart';
......
...@@ -9,6 +9,7 @@ import 'package:meta/meta.dart'; ...@@ -9,6 +9,7 @@ import 'package:meta/meta.dart';
import 'debug.dart'; import 'debug.dart';
import 'framework.dart'; import 'framework.dart';
import 'image.dart';
export 'package:flutter/rendering.dart' show export 'package:flutter/rendering.dart' show
FixedColumnWidth, FixedColumnWidth,
...@@ -145,6 +146,7 @@ class Table extends RenderObjectWidget { ...@@ -145,6 +146,7 @@ class Table extends RenderObjectWidget {
defaultColumnWidth: defaultColumnWidth, defaultColumnWidth: defaultColumnWidth,
border: border, border: border,
rowDecorations: _rowDecorations, rowDecorations: _rowDecorations,
configuration: createLocalImageConfiguration(context),
defaultVerticalAlignment: defaultVerticalAlignment, defaultVerticalAlignment: defaultVerticalAlignment,
textBaseline: textBaseline textBaseline: textBaseline
); );
...@@ -159,6 +161,7 @@ class Table extends RenderObjectWidget { ...@@ -159,6 +161,7 @@ class Table extends RenderObjectWidget {
..defaultColumnWidth = defaultColumnWidth ..defaultColumnWidth = defaultColumnWidth
..border = border ..border = border
..rowDecorations = _rowDecorations ..rowDecorations = _rowDecorations
..configuration = createLocalImageConfiguration(context)
..defaultVerticalAlignment = defaultVerticalAlignment ..defaultVerticalAlignment = defaultVerticalAlignment
..textBaseline = textBaseline; ..textBaseline = textBaseline;
} }
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'basic.dart'; import 'basic.dart';
import 'container.dart';
import 'editable.dart'; import 'editable.dart';
import 'framework.dart'; import 'framework.dart';
import 'gesture_detector.dart'; import 'gesture_detector.dart';
......
...@@ -9,13 +9,13 @@ ...@@ -9,13 +9,13 @@
library widgets; library widgets;
export 'src/widgets/app.dart'; export 'src/widgets/app.dart';
export 'src/widgets/asset_vendor.dart';
export 'src/widgets/auto_layout.dart'; export 'src/widgets/auto_layout.dart';
export 'src/widgets/banner.dart'; export 'src/widgets/banner.dart';
export 'src/widgets/basic.dart'; export 'src/widgets/basic.dart';
export 'src/widgets/binding.dart'; export 'src/widgets/binding.dart';
export 'src/widgets/child_view.dart'; export 'src/widgets/child_view.dart';
export 'src/widgets/clamp_overscrolls.dart'; export 'src/widgets/clamp_overscrolls.dart';
export 'src/widgets/container.dart';
export 'src/widgets/debug.dart'; export 'src/widgets/debug.dart';
export 'src/widgets/dismissable.dart'; export 'src/widgets/dismissable.dart';
export 'src/widgets/drag_target.dart'; export 'src/widgets/drag_target.dart';
...@@ -26,6 +26,7 @@ export 'src/widgets/framework.dart'; ...@@ -26,6 +26,7 @@ export 'src/widgets/framework.dart';
export 'src/widgets/gesture_detector.dart'; export 'src/widgets/gesture_detector.dart';
export 'src/widgets/gridpaper.dart'; export 'src/widgets/gridpaper.dart';
export 'src/widgets/heroes.dart'; export 'src/widgets/heroes.dart';
export 'src/widgets/image.dart';
export 'src/widgets/implicit_animations.dart'; export 'src/widgets/implicit_animations.dart';
export 'src/widgets/layout_builder.dart'; export 'src/widgets/layout_builder.dart';
export 'src/widgets/lazy_block.dart'; export 'src/widgets/lazy_block.dart';
......
...@@ -3,27 +3,43 @@ ...@@ -3,27 +3,43 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
class TestBoxPainter extends BoxPainter { class TestBoxPainter extends BoxPainter {
TestBoxPainter(VoidCallback onChanged): super(onChanged);
@override @override
void paint(Canvas canvas, Rect rect) { } void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { }
} }
class TestDecoration extends Decoration { class TestDecoration extends Decoration {
final List<VoidCallback> listeners = <VoidCallback>[]; int listeners = 0;
@override
bool get needsListeners => true;
@override @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 @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 @override
BoxPainter createBoxPainter() => new TestBoxPainter(); BoxPainter createBoxPainter([VoidCallback onChanged]) {
if (onChanged != null)
listeners += 1;
return new TestBoxPainter(onChanged);
}
} }
void main() { void main() {
...@@ -56,15 +72,15 @@ void main() { ...@@ -56,15 +72,15 @@ void main() {
expect(value, isTrue); 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 activeDecoration = new TestDecoration();
TestDecoration inactiveDecoration = new TestDecoration(); TestDecoration inactiveDecoration = new TestDecoration();
Widget build(TestDecoration activeDecoration, TestDecoration inactiveDecoration) { Widget build(bool active, TestDecoration activeDecoration, TestDecoration inactiveDecoration) {
return new Material( return new Material(
child: new Center( child: new Center(
child: new Switch( child: new Switch(
value: false, value: active,
onChanged: null, onChanged: null,
activeThumbDecoration: activeDecoration, activeThumbDecoration: activeDecoration,
inactiveThumbDecoration: inactiveDecoration inactiveThumbDecoration: inactiveDecoration
...@@ -73,19 +89,27 @@ void main() { ...@@ -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); await tester.pumpWidget(build(false, activeDecoration, inactiveDecoration));
expect(inactiveDecoration.listeners.length, 1); 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); await tester.pump(const Duration(milliseconds: 30)); // slightly into the animation
expect(inactiveDecoration.listeners.length, 0); // 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() { ...@@ -12,10 +12,10 @@ void main() {
imageCache.maximumSize = 2; imageCache.maximumSize = 2;
TestImageInfo a = (await imageCache.loadProvider(new TestProvider(1, 1)).first); TestImageInfo a = await extractOneFrame(new TestProvider(1, 1).resolve(ImageConfiguration.empty));
TestImageInfo b = (await imageCache.loadProvider(new TestProvider(2, 2)).first); TestImageInfo b = await extractOneFrame(new TestProvider(2, 2).resolve(ImageConfiguration.empty));
TestImageInfo c = (await imageCache.loadProvider(new TestProvider(3, 3)).first); TestImageInfo c = await extractOneFrame(new TestProvider(3, 3).resolve(ImageConfiguration.empty));
TestImageInfo d = (await imageCache.loadProvider(new TestProvider(1, 4)).first); TestImageInfo d = await extractOneFrame(new TestProvider(1, 4).resolve(ImageConfiguration.empty));
expect(a.value, equals(1)); expect(a.value, equals(1));
expect(b.value, equals(2)); expect(b.value, equals(2));
expect(c.value, equals(3)); expect(c.value, equals(3));
...@@ -23,18 +23,18 @@ void main() { ...@@ -23,18 +23,18 @@ void main() {
imageCache.maximumSize = 0; 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)); 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)); expect(f.value, equals(6));
imageCache.maximumSize = 3; 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)); 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)); expect(h.value, equals(7));
}); });
......
...@@ -12,69 +12,69 @@ void main() { ...@@ -12,69 +12,69 @@ void main() {
imageCache.maximumSize = 3; 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)); 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)); 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)); 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)); 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)); 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.value, equals(1));
expect(f, equals(a)); expect(f, equals(a));
// cache still only has one entry in it: 1(1) // 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)); expect(g.value, equals(7));
// cache has two entries in it: 1(1), 2(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)); expect(h.value, equals(1));
// cache still has two entries in it: 2(7), 1(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)); expect(i.value, equals(9));
// cache has three entries in it: 2(7), 1(1), 3(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)); expect(j.value, equals(1));
// cache still has three entries in it: 2(7), 3(9), 1(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)); expect(k.value, equals(11));
// cache has three entries: 3(9), 1(1), 4(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)); expect(l.value, equals(1));
// cache has three entries: 3(9), 4(11), 1(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)); expect(m.value, equals(13));
// cache has three entries: 4(11), 1(1), 2(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)); expect(n.value, equals(14));
// cache has three entries: 1(1), 2(13), 3(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)); expect(o.value, equals(15));
// cache has three entries: 2(13), 3(14), 4(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)); expect(p.value, equals(16));
// cache has three entries: 3(14), 4(15), 1(16) // cache has three entries: 3(14), 4(15), 1(16)
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui' as ui show Image; import 'dart:ui' as ui show Image;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
class TestImageInfo implements ImageInfo { class TestImageInfo implements ImageInfo {
...@@ -22,27 +23,33 @@ class TestImageInfo implements ImageInfo { ...@@ -22,27 +23,33 @@ class TestImageInfo implements ImageInfo {
String toString() => '$runtimeType($value)'; String toString() => '$runtimeType($value)';
} }
class TestProvider extends ImageProvider { class TestProvider extends ImageProvider<int> {
const TestProvider(this.equalityValue, this.imageValue); const TestProvider(this.key, this.imageValue);
final int key;
final int imageValue; final int imageValue;
final int equalityValue;
@override @override
Future<ImageInfo> loadImage() async { Future<int> obtainKey(ImageConfiguration configuration) {
return new TestImageInfo(imageValue); return new Future<int>.value(key);
} }
@override @override
bool operator ==(dynamic other) { ImageStreamCompleter load(int key) {
if (other is! TestProvider) return new OneFrameImageStreamCompleter(
return false; new SynchronousFuture<ImageInfo>(new TestImageInfo(imageValue))
final TestProvider typedOther = other; );
return equalityValue == typedOther.equalityValue;
} }
@override @override
int get hashCode => equalityValue.hashCode; String toString() => '$runtimeType($key, $imageValue)';
}
@override Future<ImageInfo> extractOneFrame(ImageStream stream) {
String toString() => '$runtimeType($equalityValue, $imageValue)'; 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 @@ ...@@ -5,11 +5,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui' as ui show Image; import 'dart:ui' as ui show Image;
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.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 { class TestImage extends ui.Image {
TestImage(this.scale); TestImage(this.scale);
...@@ -25,7 +25,7 @@ class TestImage extends ui.Image { ...@@ -25,7 +25,7 @@ class TestImage extends ui.Image {
void dispose() { } void dispose() { }
} }
class TestMojoDataPipeConsumer extends core.MojoDataPipeConsumer { class TestMojoDataPipeConsumer extends mojo.MojoDataPipeConsumer {
TestMojoDataPipeConsumer(this.scale) : super(null); TestMojoDataPipeConsumer(this.scale) : super(null);
final double scale; final double scale;
} }
...@@ -41,21 +41,10 @@ String testManifest = ''' ...@@ -41,21 +41,10 @@ String testManifest = '''
} }
'''; ''';
class TestAssetBundle extends AssetBundle { class TestAssetBundle extends CachingAssetBundle {
// 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;
}
@override @override
Future<core.MojoDataPipeConsumer> load(String key) { Future<mojo.MojoDataPipeConsumer> load(String key) {
core.MojoDataPipeConsumer pipe; mojo.MojoDataPipeConsumer pipe;
switch (key) { switch (key) {
case 'assets/image.png': case 'assets/image.png':
pipe = new TestMojoDataPipeConsumer(1.0); pipe = new TestMojoDataPipeConsumer(1.0);
...@@ -73,18 +62,27 @@ class TestAssetBundle extends AssetBundle { ...@@ -73,18 +62,27 @@ class TestAssetBundle extends AssetBundle {
pipe = new TestMojoDataPipeConsumer(4.0); pipe = new TestMojoDataPipeConsumer(4.0);
break; 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 @override
String toString() => '$runtimeType@$hashCode()'; String toString() => '$runtimeType@$hashCode()';
} }
Future<ui.Image> testDecodeImageFromDataPipe(core.MojoDataPipeConsumer pipe) { class TestAssetImage extends AssetImage {
TestMojoDataPipeConsumer testPipe = pipe; TestAssetImage(String name) : super(name);
assert(testPipe != null);
ui.Image image = new TestImage(testPipe.scale); @override
return (new Completer<ui.Image>()..complete(image)).future; 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) { Widget buildImageAtRatio(String image, Key key, double ratio, bool inferSize) {
...@@ -97,19 +95,17 @@ 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, devicePixelRatio: ratio,
padding: const EdgeInsets.all(0.0) padding: const EdgeInsets.all(0.0)
), ),
child: new AssetVendor( child: new DefaultAssetBundle(
bundle: new TestAssetBundle(), bundle: new TestAssetBundle(),
devicePixelRatio: ratio,
imageDecoder: testDecodeImageFromDataPipe,
child: new Center( child: new Center(
child: inferSize ? child: inferSize ?
new AssetImage( new Image(
key: key, key: key,
name: image image: new TestAssetImage(image)
) : ) :
new AssetImage( new Image(
key: key, key: key,
name: image, image: new TestAssetImage(image),
height: imageSize, height: imageSize,
width: imageSize, width: imageSize,
fit: ImageFit.fill fit: ImageFit.fill
......
...@@ -5,99 +5,99 @@ ...@@ -5,99 +5,99 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui' as ui show Image; import 'dart:ui' as ui show Image;
import 'package:mojo/core.dart' as core;
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
void main() { void main() {
testWidgets('Verify NetworkImage sets an ObjectKey on its ImageResource if it doesn\'t have a key', (WidgetTester tester) async { testWidgets('Verify Image resets its RenderImage when changing providers', (WidgetTester tester) async {
final String testUrl = 'https://foo.bar/baz1.png'; final GlobalKey key = new GlobalKey();
await tester.pumpWidget( TestImageProvider imageProvider1 = new TestImageProvider();
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';
await tester.pumpWidget( await tester.pumpWidget(
new NetworkImage( new Container(
key: new GlobalKey(), key: key,
scale: 1.0, child: new Image(
src: testUrl image: imageProvider1
) )
),
null,
EnginePhase.layout
); );
RenderImage renderImage = key.currentContext.findRenderObject();
expect(renderImage.image, isNull);
ImageResource imageResource = imageCache.load(testUrl, scale: 1.0); imageProvider1.complete();
expect(find.byKey(new ObjectKey(imageResource)), findsNothing); await tester.idle(); // resolve the future from the image provider
}); await tester.pump(null, EnginePhase.layout);
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));
ImageResource imageResource = imageCache.loadProvider(imageProvider); renderImage = key.currentContext.findRenderObject();
expect(find.byKey(new ObjectKey(imageResource)), findsOneWidget); expect(renderImage.image, isNotNull);
});
testWidgets('Verify AsyncImage doesn\'t set an ObjectKey on its ImageResource if it has a key', (WidgetTester tester) async { TestImageProvider imageProvider2 = new TestImageProvider();
ImageProvider imageProvider = new TestImageProvider();
await tester.pumpWidget( await tester.pumpWidget(
new AsyncImage( new Container(
key: new GlobalKey(), key: key,
provider: imageProvider child: new Image(
image: imageProvider2
) )
),
null,
EnginePhase.layout
); );
ImageResource imageResource = imageCache.loadProvider(imageProvider); renderImage = key.currentContext.findRenderObject();
expect(find.byKey(new ObjectKey(imageResource)), findsNothing); expect(renderImage.image, isNull);
}); });
testWidgets('Verify AssetImage sets an ObjectKey on its ImageResource if it doesn\'t have 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 String name = 'foo'; final GlobalKey key = new GlobalKey();
final AssetBundle assetBundle = new TestAssetBundle(); TestImageProvider imageProvider1 = new TestImageProvider();
await tester.pumpWidget( await tester.pumpWidget(
new AssetImage( new Container(
name: name, key: key,
bundle: assetBundle child: new Image(
gaplessPlayback: true,
image: imageProvider1
) )
),
null,
EnginePhase.layout
); );
RenderImage renderImage = key.currentContext.findRenderObject();
expect(renderImage.image, isNull);
ImageResource imageResource = assetBundle.loadImage(name); imageProvider1.complete();
expect(find.byKey(new ObjectKey(imageResource)), findsOneWidget); 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 { TestImageProvider imageProvider2 = new TestImageProvider();
final String name = 'foo';
final AssetBundle assetBundle = new TestAssetBundle();
await tester.pumpWidget( await tester.pumpWidget(
new AssetImage( new Container(
key: new GlobalKey(), key: key,
name: name, child: new Image(
bundle: assetBundle gaplessPlayback: true,
image: imageProvider2
) )
),
null,
EnginePhase.layout
); );
ImageResource imageResource = assetBundle.loadImage(name); renderImage = key.currentContext.findRenderObject();
expect(find.byKey(new ObjectKey(imageResource)), findsNothing); 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(); final GlobalKey key = new GlobalKey();
TestImageProvider imageProvider1 = new TestImageProvider(); TestImageProvider imageProvider1 = new TestImageProvider();
await tester.pumpWidget( await tester.pumpWidget(
new Container( new Image(
key: key, key: key,
child: new AsyncImage( image: imageProvider1
provider: imageProvider1
)
), ),
null, null,
EnginePhase.layout EnginePhase.layout
...@@ -114,11 +114,9 @@ void main() { ...@@ -114,11 +114,9 @@ void main() {
TestImageProvider imageProvider2 = new TestImageProvider(); TestImageProvider imageProvider2 = new TestImageProvider();
await tester.pumpWidget( await tester.pumpWidget(
new Container( new Image(
key: key, key: key,
child: new AsyncImage( image: imageProvider2
provider: imageProvider2
)
), ),
null, null,
EnginePhase.layout EnginePhase.layout
...@@ -126,16 +124,16 @@ void main() { ...@@ -126,16 +124,16 @@ void main() {
renderImage = key.currentContext.findRenderObject(); renderImage = key.currentContext.findRenderObject();
expect(renderImage.image, isNull); 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(); final GlobalKey key = new GlobalKey();
TestImageProvider imageProvider1 = new TestImageProvider(); TestImageProvider imageProvider1 = new TestImageProvider();
await tester.pumpWidget( await tester.pumpWidget(
new AsyncImage( new Image(
key: key, key: key,
provider: imageProvider1 gaplessPlayback: true,
image: imageProvider1
), ),
null, null,
EnginePhase.layout EnginePhase.layout
...@@ -152,9 +150,10 @@ void main() { ...@@ -152,9 +150,10 @@ void main() {
TestImageProvider imageProvider2 = new TestImageProvider(); TestImageProvider imageProvider2 = new TestImageProvider();
await tester.pumpWidget( await tester.pumpWidget(
new AsyncImage( new Image(
key: key, key: key,
provider: imageProvider2 gaplessPlayback: true,
image: imageProvider2
), ),
null, null,
EnginePhase.layout EnginePhase.layout
...@@ -166,28 +165,23 @@ void main() { ...@@ -166,28 +165,23 @@ void main() {
} }
class TestImageProvider extends ImageProvider { class TestImageProvider extends ImageProvider<TestImageProvider> {
final Completer<ImageInfo> _completer = new Completer<ImageInfo>(); final Completer<ImageInfo> _completer = new Completer<ImageInfo>();
@override @override
Future<ImageInfo> loadImage() => _completer.future; Future<TestImageProvider> obtainKey(ImageConfiguration configuration) {
return new SynchronousFuture<TestImageProvider>(this);
void complete() {
_completer.complete(new ImageInfo(image:new TestImage()));
} }
}
class TestAssetBundle extends AssetBundle {
final ImageResource _imageResource = new ImageResource(new Completer<ImageInfo>().future);
@override @override
ImageResource loadImage(String key) => _imageResource; ImageStreamCompleter load(TestImageProvider key) => new OneFrameImageStreamCompleter(_completer.future);
@override void complete() {
Future<String> loadString(String key) => new Completer<String>().future; _completer.complete(new ImageInfo(image: new TestImage()));
}
@override @override
Future<core.MojoDataPipeConsumer> load(String key) => new Completer<core.MojoDataPipeConsumer>().future; String toString() => '$runtimeType($hashCode)';
} }
class TestImage extends ui.Image { class TestImage extends ui.Image {
...@@ -198,6 +192,5 @@ class TestImage extends ui.Image { ...@@ -198,6 +192,5 @@ class TestImage extends ui.Image {
int get height => 100; int get height => 100;
@override @override
void dispose() { void dispose() { }
}
} }
...@@ -485,7 +485,11 @@ class _Block { ...@@ -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 { ...@@ -21,9 +21,16 @@ class ImageMap {
} }
Future<ui.Image> _loadImage(String url) async { 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; _images[url] = image;
return image; completer.complete(image);
stream.removeListener(listener);
}
stream.addListener(listener);
return completer.future;
} }
/// Returns a preloaded image, given its [url]. /// 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