Add Image.memory for making images from typed data (#8877)

Fixes #7503
Fixes #8870
diff --git a/packages/flutter/lib/src/services/image_provider.dart b/packages/flutter/lib/src/services/image_provider.dart
index ab83712..6148184 100644
--- a/packages/flutter/lib/src/services/image_provider.dart
+++ b/packages/flutter/lib/src/services/image_provider.dart
@@ -424,7 +424,7 @@
     if (other.runtimeType != runtimeType)
       return false;
     final FileImage typedOther = other;
-    return file?.path == file?.path
+    return file?.path == typedOther.file?.path
         && scale == typedOther.scale;
   }
 
@@ -435,6 +435,58 @@
   String toString() => '$runtimeType("${file?.path}", scale: $scale)';
 }
 
+/// Decodes the given [Uint8List] buffer as an image, associating it with the
+/// given scale.
+class MemoryImage extends ImageProvider<MemoryImage> {
+  /// Creates an object that decodes a [Uint8List] buffer as an image.
+  ///
+  /// The arguments must not be null.
+  const MemoryImage(this.bytes, { this.scale: 1.0 });
+
+  /// The bytes to decode into an image.
+  final Uint8List bytes;
+
+  /// The scale to place in the [ImageInfo] object of the image.
+  final double scale;
+
+  @override
+  Future<MemoryImage> obtainKey(ImageConfiguration configuration) {
+    return new SynchronousFuture<MemoryImage>(this);
+  }
+
+  @override
+  ImageStreamCompleter load(MemoryImage key) {
+    return new OneFrameImageStreamCompleter(_loadAsync(key));
+  }
+
+  Future<ImageInfo> _loadAsync(MemoryImage key) async {
+    assert(key == this);
+
+    final ui.Image image = await decodeImageFromList(bytes);
+    if (image == null)
+      return null;
+
+    return new ImageInfo(
+      image: image,
+      scale: key.scale,
+    );
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    if (other.runtimeType != runtimeType)
+      return false;
+    final MemoryImage typedOther = other;
+    return bytes == typedOther.bytes
+        && scale == typedOther.scale;
+  }
+
+  @override
+  int get hashCode => hashValues(bytes.hashCode, scale);
+
+  @override
+  String toString() => '$runtimeType(${bytes.runtimeType}#${bytes.hashCode}, scale: $scale)';
+}
 /// Fetches an image from an [AssetBundle], associating it with the given scale.
 ///
 /// This implementation requires an explicit final [name] and [scale] on
diff --git a/packages/flutter/lib/src/widgets/image.dart b/packages/flutter/lib/src/widgets/image.dart
index 85b668c..322ba71 100644
--- a/packages/flutter/lib/src/widgets/image.dart
+++ b/packages/flutter/lib/src/widgets/image.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'dart:io' show File, Platform;
+import 'dart:typed_data';
 
 import 'package:flutter/foundation.dart';
 import 'package:flutter/services.dart';
@@ -14,6 +15,7 @@
 export 'package:flutter/services.dart' show
   AssetImage,
   ExactAssetImage,
+  MemoryImage,
   NetworkImage,
   FileImage;
 
@@ -37,11 +39,12 @@
 /// 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.asset], for obtaining an image from an [AssetBundle]
-///   using a key.
-/// * [new Image.network], for obtaining an image from a URL.
-/// * [new Image.file], for obtaining an image from a [File].
+///  * [new Image], for obtaining an image from an [ImageProvider].
+///  * [new Image.asset], for obtaining an image from an [AssetBundle]
+///    using a key.
+///  * [new Image.network], for obtaining an image from a URL.
+///  * [new Image.file], for obtaining an image from a [File].
+///  * [new Image.memory], for obtaining an image from a [Uint8List].
 ///
 /// To automatically perform pixel-density-aware asset resolution, specify the
 /// image using an [AssetImage] and make sure that a [MaterialApp], [WidgetsApp],
@@ -141,6 +144,23 @@
                              : new AssetImage(name, bundle: bundle),
        super(key: key);
 
+  /// Creates a widget that displays an [ImageStream] obtained from a [Uint8List].
+  ///
+  /// The [bytes], [scale], and [repeat] arguments must not be null.
+  Image.memory(Uint8List bytes, {
+    Key key,
+    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 MemoryImage(bytes, scale: scale),
+       super(key: key);
+
   /// The image to display.
   final ImageProvider image;
 
diff --git a/packages/flutter/test/services/image_data.dart b/packages/flutter/test/services/image_data.dart
new file mode 100644
index 0000000..02680e2
--- /dev/null
+++ b/packages/flutter/test/services/image_data.dart
@@ -0,0 +1,11 @@
+// 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.
+
+const List<int> kTransparentImage = const <int>[
+  0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49,
+  0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06,
+  0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44,
+  0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D,
+  0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE,
+];
diff --git a/packages/flutter/test/services/image_decoder_test.dart b/packages/flutter/test/services/image_decoder_test.dart
index d0c28a7..28e388a 100644
--- a/packages/flutter/test/services/image_decoder_test.dart
+++ b/packages/flutter/test/services/image_decoder_test.dart
@@ -8,17 +8,11 @@
 import 'package:flutter/services.dart';
 import 'package:test/test.dart';
 
-const List<int> transparentImage = const <int>[
-  0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49,
-  0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06,
-  0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44,
-  0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D,
-  0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE,
-];
+import 'image_data.dart';
 
 void main() {
   test('Image decoder control test', () async {
-    final ui.Image image = await decodeImageFromList(new Uint8List.fromList(transparentImage));
+    final ui.Image image = await decodeImageFromList(new Uint8List.fromList(kTransparentImage));
     expect(image, isNotNull);
     expect(image.width, 1);
     expect(image.height, 1);
diff --git a/packages/flutter/test/widgets/image_test.dart b/packages/flutter/test/widgets/image_test.dart
index 9146e40..572f1b1 100644
--- a/packages/flutter/test/widgets/image_test.dart
+++ b/packages/flutter/test/widgets/image_test.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:typed_data';
 import 'dart:ui' as ui show Image;
 
 import 'package:flutter/foundation.dart';
@@ -11,6 +12,8 @@
 import 'package:flutter/widgets.dart';
 import 'package:flutter_test/flutter_test.dart';
 
+import '../services/image_data.dart';
+
 void main() {
   testWidgets('Verify Image resets its RenderImage when changing providers', (WidgetTester tester) async {
     final GlobalKey key = new GlobalKey();
@@ -299,6 +302,9 @@
     expect(image.toString(), matches(new RegExp(r'_ImageState#[0-9]+\(_StateLifecycle.defunct; not mounted; stream: ImageStream\(OneFrameImageStreamCompleter; \[100×100\] @ 1\.0x; 0 listeners\); pixels: \[100×100\] @ 1\.0x\)')));
   });
 
+  testWidgets('Image.memory control test', (WidgetTester tester) async {
+    await tester.pumpWidget(new Image.memory(new Uint8List.fromList(kTransparentImage)));
+  });
 }
 
 class TestImageProvider extends ImageProvider<TestImageProvider> {