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 {
);
}
void _imageListener(ImageInfo value) {
void _imageListener(ImageInfo value, bool synchronousCall) {
if (_image == value)
return;
_image = value;
assert(onChanged != null);
onChanged();
if (!synchronousCall)
onChanged();
}
@override
......
......@@ -46,8 +46,11 @@ class ImageInfo {
/// 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].
typedef void ImageListener(ImageInfo image);
typedef void ImageListener(ImageInfo image, bool synchronousCall);
/// A handle to an image resource.
///
......@@ -154,11 +157,15 @@ class ImageStreamCompleter {
/// Adds a listener callback that is called whenever a concrete [ImageInfo]
/// object is available. If a concrete image is already available, this object
/// will call the listener synchronously.
///
/// 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) {
_listeners.add(listener);
if (_current != null) {
try {
listener(_current);
listener(_current, true);
} catch (exception, stack) {
_handleImageError('by a synchronously-called image listener', exception, stack);
}
......@@ -179,7 +186,7 @@ class ImageStreamCompleter {
List<ImageListener> localListeners = new List<ImageListener>.from(_listeners);
for (ImageListener listener in localListeners) {
try {
listener(image);
listener(image, false);
} catch (exception, stack) {
_handleImageError('by an image listener', exception, stack);
}
......
......@@ -226,7 +226,7 @@ class _ImageState extends State<Image> {
}
}
void _handleImageChanged(ImageInfo imageInfo) {
void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) {
setState(() {
_imageInfo = imageInfo;
});
......
......@@ -2,9 +2,48 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/services.dart';
import 'package:quiver/testing/async.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() {
test("Decoration.lerp()", () {
......@@ -20,4 +59,44 @@ void main() {
c = Decoration.lerp(a, b, 1.0);
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> {
Future<ImageInfo> extractOneFrame(ImageStream stream) {
Completer<ImageInfo> completer = new Completer<ImageInfo>();
void listener(ImageInfo image) {
void listener(ImageInfo image, bool synchronousCall) {
completer.complete(image);
stream.removeListener(listener);
}
......
......@@ -23,7 +23,7 @@ class ImageMap {
Future<ui.Image> _loadImage(String url) async {
ImageStream stream = new AssetImage(url, bundle: _bundle).resolve(ImageConfiguration.empty);
Completer<ui.Image> completer = new Completer<ui.Image>();
void listener(ImageInfo frame) {
void listener(ImageInfo frame, bool synchronousCall) {
final ui.Image image = frame.image;
_images[url] = 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