Commit 39be1c37 authored by Jason Simmons's avatar Jason Simmons Committed by GitHub

Tell image listeners if they are being called synchronously by the ImageStream (#5161)

Image listeners installed in paint handlers need to know whether the listener
is being called during the paint.

Fixes https://github.com/flutter/flutter/issues/4937
parent 2656006c
...@@ -1315,12 +1315,13 @@ class _BoxDecorationPainter extends BoxPainter { ...@@ -1315,12 +1315,13 @@ class _BoxDecorationPainter extends BoxPainter {
); );
} }
void _imageListener(ImageInfo value) { void _imageListener(ImageInfo value, bool synchronousCall) {
if (_image == value) if (_image == value)
return; return;
_image = value; _image = value;
assert(onChanged != null); assert(onChanged != null);
onChanged(); if (!synchronousCall)
onChanged();
} }
@override @override
......
...@@ -46,8 +46,11 @@ class ImageInfo { ...@@ -46,8 +46,11 @@ class ImageInfo {
/// Signature for callbacks reporting that an image is available. /// Signature for callbacks reporting that an image is available.
/// ///
/// synchronousCall is true if the listener is being invoked during the call
/// to addListener.
///
/// Used by [ImageStream]. /// Used by [ImageStream].
typedef void ImageListener(ImageInfo image); typedef void ImageListener(ImageInfo image, bool synchronousCall);
/// A handle to an image resource. /// A handle to an image resource.
/// ///
...@@ -154,11 +157,15 @@ class ImageStreamCompleter { ...@@ -154,11 +157,15 @@ class ImageStreamCompleter {
/// 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.
///
/// The listener will be passed a flag indicating whether a synchronous call
/// occurred. If the listener is added within a render object paint function,
/// then use this flag to avoid calling markNeedsPaint during a paint.
void addListener(ImageListener listener) { void addListener(ImageListener listener) {
_listeners.add(listener); _listeners.add(listener);
if (_current != null) { if (_current != null) {
try { try {
listener(_current); listener(_current, true);
} catch (exception, stack) { } catch (exception, stack) {
_handleImageError('by a synchronously-called image listener', exception, stack); _handleImageError('by a synchronously-called image listener', exception, stack);
} }
...@@ -179,7 +186,7 @@ class ImageStreamCompleter { ...@@ -179,7 +186,7 @@ class ImageStreamCompleter {
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, false);
} catch (exception, stack) { } catch (exception, stack) {
_handleImageError('by an image listener', exception, stack); _handleImageError('by an image listener', exception, stack);
} }
......
...@@ -226,7 +226,7 @@ class _ImageState extends State<Image> { ...@@ -226,7 +226,7 @@ class _ImageState extends State<Image> {
} }
} }
void _handleImageChanged(ImageInfo imageInfo) { void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) {
setState(() { setState(() {
_imageInfo = imageInfo; _imageInfo = imageInfo;
}); });
......
...@@ -2,9 +2,48 @@ ...@@ -2,9 +2,48 @@
// 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 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:flutter/services.dart';
import 'package:quiver/testing/async.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../services/mocks_for_image_cache.dart';
class TestCanvas implements Canvas {
@override
void noSuchMethod(Invocation invocation) {}
}
class SynchronousTestImageProvider extends ImageProvider<int> {
@override
Future<int> obtainKey(ImageConfiguration configuration) {
return new SynchronousFuture<int>(1);
}
@override
ImageStreamCompleter load(int key) {
return new OneFrameImageStreamCompleter(
new SynchronousFuture<ImageInfo>(new TestImageInfo(key))
);
}
}
class AsyncTestImageProvider extends ImageProvider<int> {
@override
Future<int> obtainKey(ImageConfiguration configuration) {
return new Future<int>.value(2);
}
@override
ImageStreamCompleter load(int key) {
return new OneFrameImageStreamCompleter(
new Future<ImageInfo>.value(new TestImageInfo(key))
);
}
}
void main() { void main() {
test("Decoration.lerp()", () { test("Decoration.lerp()", () {
...@@ -20,4 +59,44 @@ void main() { ...@@ -20,4 +59,44 @@ void main() {
c = Decoration.lerp(a, b, 1.0); c = Decoration.lerp(a, b, 1.0);
expect(c.backgroundColor, equals(b.backgroundColor)); expect(c.backgroundColor, equals(b.backgroundColor));
}); });
test("BoxDecorationImageListenerSync", () {
ImageProvider imageProvider = new SynchronousTestImageProvider();
BackgroundImage backgroundImage = new BackgroundImage(image: imageProvider);
BoxDecoration boxDecoration = new BoxDecoration(backgroundImage: backgroundImage);
bool onChangedCalled = false;
BoxPainter boxPainter = boxDecoration.createBoxPainter(() {
onChangedCalled = true;
});
TestCanvas canvas = new TestCanvas();
ImageConfiguration imageConfiguration = new ImageConfiguration(size: Size.zero);
boxPainter.paint(canvas, Offset.zero, imageConfiguration);
// The onChanged callback should not be invoked during the call to boxPainter.paint
expect(onChangedCalled, equals(false));
});
test("BoxDecorationImageListenerAsync", () {
new FakeAsync().run((FakeAsync async) {
ImageProvider imageProvider = new AsyncTestImageProvider();
BackgroundImage backgroundImage = new BackgroundImage(image: imageProvider);
BoxDecoration boxDecoration = new BoxDecoration(backgroundImage: backgroundImage);
bool onChangedCalled = false;
BoxPainter boxPainter = boxDecoration.createBoxPainter(() {
onChangedCalled = true;
});
TestCanvas canvas = new TestCanvas();
ImageConfiguration imageConfiguration = new ImageConfiguration(size: Size.zero);
boxPainter.paint(canvas, Offset.zero, imageConfiguration);
// The onChanged callback should be invoked asynchronously.
expect(onChangedCalled, equals(false));
async.flushMicrotasks();
expect(onChangedCalled, equals(true));
});
});
} }
...@@ -46,7 +46,7 @@ class TestProvider extends ImageProvider<int> { ...@@ -46,7 +46,7 @@ class TestProvider extends ImageProvider<int> {
Future<ImageInfo> extractOneFrame(ImageStream stream) { Future<ImageInfo> extractOneFrame(ImageStream stream) {
Completer<ImageInfo> completer = new Completer<ImageInfo>(); Completer<ImageInfo> completer = new Completer<ImageInfo>();
void listener(ImageInfo image) { void listener(ImageInfo image, bool synchronousCall) {
completer.complete(image); completer.complete(image);
stream.removeListener(listener); stream.removeListener(listener);
} }
......
...@@ -23,7 +23,7 @@ class ImageMap { ...@@ -23,7 +23,7 @@ class ImageMap {
Future<ui.Image> _loadImage(String url) async { Future<ui.Image> _loadImage(String url) async {
ImageStream stream = new AssetImage(url, bundle: _bundle).resolve(ImageConfiguration.empty); ImageStream stream = new AssetImage(url, bundle: _bundle).resolve(ImageConfiguration.empty);
Completer<ui.Image> completer = new Completer<ui.Image>(); Completer<ui.Image> completer = new Completer<ui.Image>();
void listener(ImageInfo frame) { void listener(ImageInfo frame, bool synchronousCall) {
final ui.Image image = frame.image; final ui.Image image = frame.image;
_images[url] = image; _images[url] = image;
completer.complete(image); completer.complete(image);
......
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