mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Allow detection of images using more memory than necessary (#59877)
This commit is contained in:
parent
fd7a72ee6f
commit
06d0cd514e
124
dev/tracing_tests/test/image_painting_event_test.dart
Normal file
124
dev/tracing_tests/test/image_painting_event_test.dart
Normal file
@ -0,0 +1,124 @@
|
||||
// Copyright 2014 The Flutter 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:convert' show jsonEncode;
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:vm_service/vm_service.dart';
|
||||
import 'package:vm_service/vm_service_io.dart';
|
||||
|
||||
void main() {
|
||||
VmService vmService;
|
||||
LiveTestWidgetsFlutterBinding binding;
|
||||
setUpAll(() async {
|
||||
final developer.ServiceProtocolInfo info =
|
||||
await developer.Service.getInfo();
|
||||
|
||||
if (info.serverUri == null) {
|
||||
fail('This test _must_ be run with --enable-vmservice.');
|
||||
}
|
||||
|
||||
vmService = await vmServiceConnectUri('ws://localhost:${info.serverUri.port}${info.serverUri.path}ws');
|
||||
await vmService.streamListen(EventStreams.kExtension);
|
||||
|
||||
// Initialize bindings
|
||||
binding = LiveTestWidgetsFlutterBinding();
|
||||
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
|
||||
binding.attachRootWidget(const SizedBox.expand());
|
||||
expect(binding.framesEnabled, true);
|
||||
// Pump two frames to make sure we clear out any inter-frame comparisons.
|
||||
await binding.endOfFrame;
|
||||
await binding.endOfFrame;
|
||||
});
|
||||
|
||||
test('Image painting events - deduplicates across frames', () async {
|
||||
final Completer<Event> completer = Completer<Event>();
|
||||
vmService.onExtensionEvent.first.then(completer.complete);
|
||||
|
||||
const TestImage image = TestImage(width: 300, height: 300);
|
||||
final TestCanvas canvas = TestCanvas();
|
||||
paintImage(
|
||||
canvas: canvas,
|
||||
rect: const Rect.fromLTWH(50.0, 75.0, 200.0, 100.0),
|
||||
image: image,
|
||||
debugImageLabel: 'test.png',
|
||||
);
|
||||
|
||||
// Make sure that we don't report an identical image size info if we
|
||||
// redraw in the next frame.
|
||||
await binding.endOfFrame;
|
||||
|
||||
paintImage(
|
||||
canvas: canvas,
|
||||
rect: const Rect.fromLTWH(50.0, 75.0, 200.0, 100.0),
|
||||
image: image,
|
||||
debugImageLabel: 'test.png',
|
||||
);
|
||||
await binding.endOfFrame;
|
||||
|
||||
final Event event = await completer.future;
|
||||
expect(event.extensionKind, 'Flutter.ImageSizesForFrame');
|
||||
expect(
|
||||
jsonEncode(event.extensionData.data),
|
||||
'{"test.png":{"source":"test.png","displaySize":{"width":200.0,"height":100.0},"imageSize":{"width":300.0,"height":300.0},"displaySizeInBytes":106666,"decodedSizeInBytes":480000}}',
|
||||
);
|
||||
}, skip: isBrowser); // uses dart:isolate and io
|
||||
|
||||
test('Image painting events - deduplicates across frames', () async {
|
||||
final Completer<Event> completer = Completer<Event>();
|
||||
vmService.onExtensionEvent.first.then(completer.complete);
|
||||
|
||||
const TestImage image = TestImage(width: 300, height: 300);
|
||||
final TestCanvas canvas = TestCanvas();
|
||||
paintImage(
|
||||
canvas: canvas,
|
||||
rect: const Rect.fromLTWH(50.0, 75.0, 200.0, 100.0),
|
||||
image: image,
|
||||
debugImageLabel: 'test.png',
|
||||
);
|
||||
|
||||
paintImage(
|
||||
canvas: canvas,
|
||||
rect: const Rect.fromLTWH(50.0, 75.0, 300.0, 300.0),
|
||||
image: image,
|
||||
debugImageLabel: 'test.png',
|
||||
);
|
||||
await binding.endOfFrame;
|
||||
|
||||
final Event event = await completer.future;
|
||||
expect(event.extensionKind, 'Flutter.ImageSizesForFrame');
|
||||
expect(
|
||||
jsonEncode(event.extensionData.data),
|
||||
'{"test.png":{"source":"test.png","displaySize":{"width":300.0,"height":300.0},"imageSize":{"width":300.0,"height":300.0},"displaySizeInBytes":480000,"decodedSizeInBytes":480000}}',
|
||||
);
|
||||
}, skip: isBrowser); // uses dart:isolate and io
|
||||
}
|
||||
|
||||
class TestImage implements ui.Image {
|
||||
const TestImage({this.height = 0, this.width = 0});
|
||||
@override
|
||||
final int height;
|
||||
@override
|
||||
final int width;
|
||||
|
||||
@override
|
||||
void dispose() {}
|
||||
|
||||
@override
|
||||
Future<ByteData> toByteData(
|
||||
{ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
class TestCanvas implements Canvas {
|
||||
@override
|
||||
void noSuchMethod(Invocation invocation) {}
|
||||
}
|
||||
@ -51,6 +51,7 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
|
||||
codec: _loadAsync(key as NetworkImage, chunkEvents, decode),
|
||||
chunkEvents: chunkEvents.stream,
|
||||
scale: key.scale,
|
||||
debugLabel: key.url,
|
||||
informationCollector: () {
|
||||
return <DiagnosticsNode>[
|
||||
DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
|
||||
|
||||
@ -54,6 +54,7 @@ class NetworkImage
|
||||
chunkEvents: chunkEvents.stream,
|
||||
codec: _loadAsync(key as NetworkImage, decode, chunkEvents),
|
||||
scale: key.scale,
|
||||
debugLabel: key.url,
|
||||
informationCollector: _imageStreamInformationCollector(key));
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
// @dart = 2.8
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:ui' show Size, hashValues;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
@ -31,6 +32,101 @@ typedef HttpClientProvider = HttpClient Function();
|
||||
/// This value is ignored in non-debug builds.
|
||||
HttpClientProvider debugNetworkImageHttpClientProvider;
|
||||
|
||||
typedef PaintImageCallback = void Function(ImageSizeInfo);
|
||||
|
||||
/// Tracks the bytes used by a [ui.Image] compared to the bytes needed to paint
|
||||
/// that image without scaling it.
|
||||
@immutable
|
||||
class ImageSizeInfo {
|
||||
/// Creates an object to track the backing size of a [ui.Image] compared to
|
||||
/// its display size on a [Canvas].
|
||||
///
|
||||
/// This class is used by the framework when it paints an image to a canvas
|
||||
/// to report to `dart:developer`'s [postEvent], as well as to the
|
||||
/// [debugOnPaintImage] callback if it is set.
|
||||
const ImageSizeInfo({this.source, this.displaySize, this.imageSize});
|
||||
|
||||
/// A unique identifier for this image, for example its asset path or network
|
||||
/// URL.
|
||||
final String source;
|
||||
|
||||
/// The size of the area the image will be rendered in.
|
||||
final Size displaySize;
|
||||
|
||||
/// The size the image has been decoded to.
|
||||
final Size imageSize;
|
||||
|
||||
/// The number of bytes needed to render the image without scaling it.
|
||||
int get displaySizeInBytes => _sizeToBytes(displaySize);
|
||||
|
||||
/// The number of bytes used by the image in memory.
|
||||
int get decodedSizeInBytes => _sizeToBytes(imageSize);
|
||||
|
||||
int _sizeToBytes(Size size) {
|
||||
// Assume 4 bytes per pixel and that mipmapping will be used, which adds
|
||||
// 4/3.
|
||||
return (size.width * size.height * 4 * (4/3)).toInt();
|
||||
}
|
||||
|
||||
/// Returns a JSON encodable representation of this object.
|
||||
Map<String, Object> toJson() {
|
||||
return <String, Object>{
|
||||
'source': source,
|
||||
'displaySize': <String, double>{
|
||||
'width': displaySize.width,
|
||||
'height': displaySize.height,
|
||||
},
|
||||
'imageSize': <String, double>{
|
||||
'width': imageSize.width,
|
||||
'height': imageSize.height,
|
||||
},
|
||||
'displaySizeInBytes': displaySizeInBytes,
|
||||
'decodedSizeInBytes': decodedSizeInBytes,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
return other is ImageSizeInfo
|
||||
&& other.source == source
|
||||
&& other.imageSize == imageSize
|
||||
&& other.displaySize == displaySize;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(source, displaySize, imageSize);
|
||||
|
||||
@override
|
||||
String toString() => 'ImageSizeInfo($source, imageSize: $imageSize, displaySize: $displaySize)';
|
||||
}
|
||||
|
||||
/// If not null, called when the framework is about to paint an [Image] to a
|
||||
/// [Canvas] with an [ImageSizeInfo] that contains the decoded size of the
|
||||
/// image as well as its output size.
|
||||
///
|
||||
/// A test can use this callback to detect if images under test are being
|
||||
/// rendered with the appropriate cache dimensions.
|
||||
///
|
||||
/// For example, if a 100x100 image is decoded it takes roughly 53kb in memory
|
||||
/// (including mipmapping overhead). If it is only ever displayed at 50x50, it
|
||||
/// would take only 13kb if the cacheHeight/cacheWidth parameters had been
|
||||
/// specified at that size. This problem becomes more serious for larger
|
||||
/// images, such as a high resolution image from a 12MP camera, which would be
|
||||
/// 64mb when decoded.
|
||||
///
|
||||
/// When using this callback, developers should consider whether the image will
|
||||
/// be panned or scaled up in the application, how many images are being
|
||||
/// displayed, and whether the application will run on multiple devices with
|
||||
/// different resolutions and memory capacities. For example, it should be fine
|
||||
/// to have an image that animates from thumbnail size to full screen be at
|
||||
/// a higher resolution while animating, but it would be problematic to have
|
||||
/// a grid or list of such thumbnails all be at the full resolution at the same
|
||||
/// time.
|
||||
PaintImageCallback debugOnPaintImage;
|
||||
|
||||
/// Returns true if none of the painting library debug variables have been changed.
|
||||
///
|
||||
/// This function is used by the test framework to ensure that debug variables
|
||||
@ -45,7 +141,8 @@ HttpClientProvider debugNetworkImageHttpClientProvider;
|
||||
bool debugAssertAllPaintingVarsUnset(String reason, { bool debugDisableShadowsOverride = false }) {
|
||||
assert(() {
|
||||
if (debugDisableShadows != debugDisableShadowsOverride ||
|
||||
debugNetworkImageHttpClientProvider != null) {
|
||||
debugNetworkImageHttpClientProvider != null ||
|
||||
debugOnPaintImage != null) {
|
||||
throw FlutterError(reason);
|
||||
}
|
||||
return true;
|
||||
|
||||
@ -4,14 +4,17 @@
|
||||
|
||||
// @dart = 2.8
|
||||
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:ui' as ui show Image;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
import 'alignment.dart';
|
||||
import 'basic_types.dart';
|
||||
import 'borders.dart';
|
||||
import 'box_fit.dart';
|
||||
import 'debug.dart';
|
||||
import 'image_provider.dart';
|
||||
import 'image_stream.dart';
|
||||
|
||||
@ -275,6 +278,7 @@ class DecorationImagePainter {
|
||||
canvas: canvas,
|
||||
rect: rect,
|
||||
image: _image.image,
|
||||
debugImageLabel: _image.debugLabel,
|
||||
scale: _details.scale * _image.scale,
|
||||
colorFilter: _details.colorFilter,
|
||||
fit: _details.fit,
|
||||
@ -317,6 +321,25 @@ class DecorationImagePainter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Used by [paintImage] to report image sizes drawn at the end of the frame.
|
||||
Map<String, ImageSizeInfo> _pendingImageSizeInfo = <String, ImageSizeInfo>{};
|
||||
|
||||
/// [ImageSizeInfo]s that were reported on the last frame.
|
||||
///
|
||||
/// Used to prevent duplicative reports from frame to frame.
|
||||
Set<ImageSizeInfo> _lastFrameImageSizeInfo = <ImageSizeInfo>{};
|
||||
|
||||
/// Flushes inter-frame tracking of image size information from [paintImage].
|
||||
///
|
||||
/// Has no effect if asserts are disabled.
|
||||
@visibleForTesting
|
||||
void debugFlushLastFrameImageSizeInfo() {
|
||||
assert(() {
|
||||
_lastFrameImageSizeInfo = <ImageSizeInfo>{};
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
|
||||
/// Paints an image into the given rectangle on the canvas.
|
||||
///
|
||||
/// The arguments have the following meanings:
|
||||
@ -389,6 +412,7 @@ void paintImage({
|
||||
@required Canvas canvas,
|
||||
@required Rect rect,
|
||||
@required ui.Image image,
|
||||
String debugImageLabel,
|
||||
double scale = 1.0,
|
||||
ColorFilter colorFilter,
|
||||
BoxFit fit,
|
||||
@ -431,6 +455,43 @@ void paintImage({
|
||||
// as we apply a nine-patch stretch.
|
||||
assert(sourceSize == inputSize, 'centerSlice was used with a BoxFit that does not guarantee that the image is fully visible.');
|
||||
}
|
||||
|
||||
// Output size is fully calculated.
|
||||
if (!kReleaseMode) {
|
||||
final ImageSizeInfo sizeInfo = ImageSizeInfo(
|
||||
// Some ImageProvider implementations may not have given this.
|
||||
source: debugImageLabel ?? '<Unknown Image(${image.width}×${image.height})>',
|
||||
imageSize: Size(image.width.toDouble(), image.height.toDouble()),
|
||||
displaySize: outputSize,
|
||||
);
|
||||
// Avoid emitting events that are the same as those emitted in the last frame.
|
||||
if (!_lastFrameImageSizeInfo.contains(sizeInfo)) {
|
||||
final ImageSizeInfo existingSizeInfo = _pendingImageSizeInfo[sizeInfo.source];
|
||||
if (existingSizeInfo == null || existingSizeInfo.displaySizeInBytes < sizeInfo.displaySizeInBytes) {
|
||||
_pendingImageSizeInfo[sizeInfo.source] = sizeInfo;
|
||||
}
|
||||
// _pendingImageSizeInfo.add(sizeInfo);
|
||||
if (debugOnPaintImage != null) {
|
||||
debugOnPaintImage(sizeInfo);
|
||||
}
|
||||
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
|
||||
_lastFrameImageSizeInfo = _pendingImageSizeInfo.values.toSet();
|
||||
if (_pendingImageSizeInfo.isEmpty) {
|
||||
return;
|
||||
}
|
||||
developer.postEvent(
|
||||
'Flutter.ImageSizesForFrame',
|
||||
<Object, Object>{
|
||||
for (ImageSizeInfo imageSizeInfo in _pendingImageSizeInfo.values)
|
||||
imageSizeInfo.source: imageSizeInfo.toJson()
|
||||
},
|
||||
);
|
||||
_pendingImageSizeInfo = <String, ImageSizeInfo>{};
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (repeat != ImageRepeat.noRepeat && destinationSize == outputSize) {
|
||||
// There's no need to repeat the image because we're exactly filling the
|
||||
// output rect with the image.
|
||||
|
||||
@ -651,6 +651,7 @@ abstract class AssetBundleImageProvider extends ImageProvider<AssetBundleImageKe
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key, decode),
|
||||
scale: key.scale,
|
||||
debugLabel: key.name,
|
||||
informationCollector: collector
|
||||
);
|
||||
}
|
||||
@ -764,7 +765,11 @@ class ResizeImage extends ImageProvider<_SizeAwareCacheKey> {
|
||||
);
|
||||
return decode(bytes, cacheWidth: width, cacheHeight: height, allowUpscaling: this.allowUpscaling);
|
||||
};
|
||||
return imageProvider.load(key.providerCacheKey, decodeResize);
|
||||
final ImageStreamCompleter completer = imageProvider.load(key.providerCacheKey, decodeResize);
|
||||
if (!kReleaseMode) {
|
||||
completer.debugLabel = '${completer.debugLabel} - Resized(${key.width}×${key.height})';
|
||||
}
|
||||
return completer;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -860,6 +865,7 @@ class FileImage extends ImageProvider<FileImage> {
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key, decode),
|
||||
scale: key.scale,
|
||||
debugLabel: key.file.path,
|
||||
informationCollector: () sync* {
|
||||
yield ErrorDescription('Path: ${file?.path}');
|
||||
},
|
||||
@ -933,6 +939,7 @@ class MemoryImage extends ImageProvider<MemoryImage> {
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key, decode),
|
||||
scale: key.scale,
|
||||
debugLabel: 'MemoryImage(${describeIdentity(key.bytes)})',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,9 @@ class ImageInfo {
|
||||
/// Creates an [ImageInfo] object for the given [image] and [scale].
|
||||
///
|
||||
/// Both the image and the scale must not be null.
|
||||
const ImageInfo({ @required this.image, this.scale = 1.0 })
|
||||
///
|
||||
/// The tag may be used to identify the source of this image.
|
||||
const ImageInfo({ @required this.image, this.scale = 1.0, this.debugLabel })
|
||||
: assert(image != null),
|
||||
assert(scale != null);
|
||||
|
||||
@ -42,11 +44,14 @@ class ImageInfo {
|
||||
/// (e.g. in the arguments given to [Canvas.drawImage]).
|
||||
final double scale;
|
||||
|
||||
@override
|
||||
String toString() => '$image @ ${debugFormatDouble(scale)}x';
|
||||
/// A string used for debugging purpopses to identify the source of this image.
|
||||
final String debugLabel;
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(image, scale);
|
||||
String toString() => '${debugLabel != null ? '$debugLabel ' : ''}$image @ ${debugFormatDouble(scale)}x';
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(image, scale, debugLabel);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
@ -54,7 +59,8 @@ class ImageInfo {
|
||||
return false;
|
||||
return other is ImageInfo
|
||||
&& other.image == image
|
||||
&& other.scale == scale;
|
||||
&& other.scale == scale
|
||||
&& other.debugLabel == debugLabel;
|
||||
}
|
||||
}
|
||||
|
||||
@ -331,6 +337,9 @@ abstract class ImageStreamCompleter with Diagnosticable {
|
||||
ImageInfo _currentImage;
|
||||
FlutterErrorDetails _currentError;
|
||||
|
||||
/// A string identifying the source of the underlying image.
|
||||
String debugLabel;
|
||||
|
||||
/// Whether any listeners are currently registered.
|
||||
///
|
||||
/// Clients should not depend on this value for their behavior, because having
|
||||
@ -623,6 +632,9 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
|
||||
/// The `scale` parameter is the linear scale factor for drawing this frames
|
||||
/// of this image at their intended size.
|
||||
///
|
||||
/// The `tag` parameter is passed on to created [ImageInfo] objects to
|
||||
/// help identify the source of the image.
|
||||
///
|
||||
/// The `chunkEvents` parameter is an optional stream of notifications about
|
||||
/// the loading progress of the image. If this stream is provided, the events
|
||||
/// produced by the stream will be delivered to registered [ImageChunkListener]s
|
||||
@ -630,11 +642,13 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
|
||||
MultiFrameImageStreamCompleter({
|
||||
@required Future<ui.Codec> codec,
|
||||
@required double scale,
|
||||
String debugLabel,
|
||||
Stream<ImageChunkEvent> chunkEvents,
|
||||
InformationCollector informationCollector,
|
||||
}) : assert(codec != null),
|
||||
_informationCollector = informationCollector,
|
||||
_scale = scale {
|
||||
this.debugLabel = debugLabel;
|
||||
codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {
|
||||
reportError(
|
||||
context: ErrorDescription('resolving an image codec'),
|
||||
@ -688,7 +702,7 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
|
||||
if (!hasListeners)
|
||||
return;
|
||||
if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
|
||||
_emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
|
||||
_emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale, debugLabel: debugLabel));
|
||||
_shownTimestamp = timestamp;
|
||||
_frameDuration = _nextFrame.duration;
|
||||
_nextFrame = null;
|
||||
@ -729,7 +743,7 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
|
||||
if (_codec.frameCount == 1) {
|
||||
// This is not an animated image, just return it and don't schedule more
|
||||
// frames.
|
||||
_emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
|
||||
_emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale, debugLabel: debugLabel));
|
||||
return;
|
||||
}
|
||||
_scheduleAppFrame();
|
||||
|
||||
@ -28,6 +28,7 @@ class RenderImage extends RenderBox {
|
||||
/// [alignment] will need resolving or if [matchTextDirection] is true.
|
||||
RenderImage({
|
||||
ui.Image image,
|
||||
this.debugImageLabel,
|
||||
double width,
|
||||
double height,
|
||||
double scale = 1.0,
|
||||
@ -94,6 +95,9 @@ class RenderImage extends RenderBox {
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
/// A string used to identify the source of the image.
|
||||
String debugImageLabel;
|
||||
|
||||
/// If non-null, requires the image to have this width.
|
||||
///
|
||||
/// If null, the image will pick a size that best preserves its intrinsic
|
||||
@ -377,6 +381,7 @@ class RenderImage extends RenderBox {
|
||||
canvas: context.canvas,
|
||||
rect: offset & size,
|
||||
image: _image,
|
||||
debugImageLabel: debugImageLabel,
|
||||
scale: _scale,
|
||||
colorFilter: _colorFilter,
|
||||
fit: _fit,
|
||||
|
||||
@ -5301,6 +5301,7 @@ class RawImage extends LeafRenderObjectWidget {
|
||||
const RawImage({
|
||||
Key key,
|
||||
this.image,
|
||||
this.debugImageLabel,
|
||||
this.width,
|
||||
this.height,
|
||||
this.scale = 1.0,
|
||||
@ -5324,6 +5325,9 @@ class RawImage extends LeafRenderObjectWidget {
|
||||
/// The image to display.
|
||||
final ui.Image image;
|
||||
|
||||
/// A string identifying the source of the image.
|
||||
final String debugImageLabel;
|
||||
|
||||
/// If non-null, require the image to have this width.
|
||||
///
|
||||
/// If null, the image will pick a size that best preserves its intrinsic
|
||||
@ -5443,6 +5447,7 @@ class RawImage extends LeafRenderObjectWidget {
|
||||
assert((!matchTextDirection && alignment is Alignment) || debugCheckHasDirectionality(context));
|
||||
return RenderImage(
|
||||
image: image,
|
||||
debugImageLabel: debugImageLabel,
|
||||
width: width,
|
||||
height: height,
|
||||
scale: scale,
|
||||
@ -5464,6 +5469,7 @@ class RawImage extends LeafRenderObjectWidget {
|
||||
void updateRenderObject(BuildContext context, RenderImage renderObject) {
|
||||
renderObject
|
||||
..image = image
|
||||
..debugImageLabel = debugImageLabel
|
||||
..width = width
|
||||
..height = height
|
||||
..scale = scale
|
||||
|
||||
@ -1205,6 +1205,7 @@ class _ImageState extends State<Image> with WidgetsBindingObserver {
|
||||
|
||||
Widget result = RawImage(
|
||||
image: _imageInfo?.image,
|
||||
debugImageLabel: _imageInfo?.debugLabel,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
scale: _imageInfo?.scale ?? 1.0,
|
||||
|
||||
@ -8,6 +8,7 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' show Codec, FrameInfo;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
@ -219,8 +220,68 @@ void main() {
|
||||
|
||||
debugNetworkImageHttpClientProvider = null;
|
||||
}, skip: isBrowser); // Browser does not resolve images this way.
|
||||
|
||||
Future<Codec> _decoder(Uint8List bytes, {int cacheWidth, int cacheHeight, bool allowUpscaling}) async {
|
||||
return FakeCodec();
|
||||
}
|
||||
|
||||
test('Network image sets tag', () async {
|
||||
const String url = 'http://test.png';
|
||||
const int chunkSize = 8;
|
||||
final List<Uint8List> chunks = <Uint8List>[
|
||||
for (int offset = 0; offset < kTransparentImage.length; offset += chunkSize)
|
||||
Uint8List.fromList(kTransparentImage.skip(offset).take(chunkSize).toList()),
|
||||
];
|
||||
final _MockHttpClientRequest request = _MockHttpClientRequest();
|
||||
final _MockHttpClientResponse response = _MockHttpClientResponse();
|
||||
when(httpClient.getUrl(any)).thenAnswer((_) => Future<HttpClientRequest>.value(request));
|
||||
when(request.close()).thenAnswer((_) => Future<HttpClientResponse>.value(response));
|
||||
when(response.statusCode).thenReturn(HttpStatus.ok);
|
||||
when(response.contentLength).thenReturn(kTransparentImage.length);
|
||||
when(response.listen(
|
||||
any,
|
||||
onDone: anyNamed('onDone'),
|
||||
onError: anyNamed('onError'),
|
||||
cancelOnError: anyNamed('cancelOnError'),
|
||||
)).thenAnswer((Invocation invocation) {
|
||||
final void Function(List<int>) onData = invocation.positionalArguments[0] as void Function(List<int>);
|
||||
final void Function(Object) onError = invocation.namedArguments[#onError] as void Function(Object);
|
||||
final VoidCallback onDone = invocation.namedArguments[#onDone] as VoidCallback;
|
||||
final bool cancelOnError = invocation.namedArguments[#cancelOnError] as bool;
|
||||
|
||||
return Stream<Uint8List>.fromIterable(chunks).listen(
|
||||
onData,
|
||||
onDone: onDone,
|
||||
onError: onError,
|
||||
cancelOnError: cancelOnError,
|
||||
);
|
||||
});
|
||||
|
||||
const NetworkImage provider = NetworkImage(url);
|
||||
|
||||
final MultiFrameImageStreamCompleter completer = provider.load(provider, _decoder) as MultiFrameImageStreamCompleter;
|
||||
|
||||
expect(completer.debugLabel, url);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
class _MockHttpClient extends Mock implements HttpClient {}
|
||||
class _MockHttpClientRequest extends Mock implements HttpClientRequest {}
|
||||
class _MockHttpClientResponse extends Mock implements HttpClientResponse {}
|
||||
|
||||
class FakeCodec implements Codec {
|
||||
@override
|
||||
void dispose() {}
|
||||
|
||||
@override
|
||||
int get frameCount => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<FrameInfo> getNextFrame() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
int get repetitionCount => throw UnimplementedError();
|
||||
}
|
||||
|
||||
@ -6,13 +6,17 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:file/memory.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../rendering/rendering_tester.dart';
|
||||
import 'image_data.dart';
|
||||
import 'mocks_for_image_cache.dart';
|
||||
|
||||
void main() {
|
||||
@ -136,4 +140,70 @@ void main() {
|
||||
|
||||
expect(await error.future, isStateError);
|
||||
});
|
||||
|
||||
Future<Codec> _decoder(Uint8List bytes, {int cacheWidth, int cacheHeight, bool allowUpscaling}) async {
|
||||
return FakeCodec();
|
||||
}
|
||||
|
||||
test('File image sets tag', () async {
|
||||
final MemoryFileSystem fs = MemoryFileSystem();
|
||||
final File file = fs.file('/blue.png')..createSync(recursive: true)..writeAsBytesSync(kBlueRectPng);
|
||||
final FileImage provider = FileImage(file);
|
||||
|
||||
final MultiFrameImageStreamCompleter completer = provider.load(provider, _decoder) as MultiFrameImageStreamCompleter;
|
||||
|
||||
expect(completer.debugLabel, file.path);
|
||||
});
|
||||
|
||||
test('Memory image sets tag', () async {
|
||||
final Uint8List bytes = Uint8List.fromList(kBlueRectPng);
|
||||
final MemoryImage provider = MemoryImage(bytes);
|
||||
|
||||
final MultiFrameImageStreamCompleter completer = provider.load(provider, _decoder) as MultiFrameImageStreamCompleter;
|
||||
|
||||
expect(completer.debugLabel, 'MemoryImage(${describeIdentity(bytes)})');
|
||||
});
|
||||
|
||||
test('Asset image sets tag', () async {
|
||||
const String asset = 'images/blue.png';
|
||||
final ExactAssetImage provider = ExactAssetImage(asset, bundle: _TestAssetBundle());
|
||||
final AssetBundleImageKey key = await provider.obtainKey(ImageConfiguration.empty);
|
||||
final MultiFrameImageStreamCompleter completer = provider.load(key, _decoder) as MultiFrameImageStreamCompleter;
|
||||
|
||||
expect(completer.debugLabel, asset);
|
||||
});
|
||||
|
||||
test('Resize image sets tag', () async {
|
||||
final Uint8List bytes = Uint8List.fromList(kBlueRectPng);
|
||||
final ResizeImage provider = ResizeImage(MemoryImage(bytes), width: 40, height: 40);
|
||||
final MultiFrameImageStreamCompleter completer = provider.load(
|
||||
await provider.obtainKey(ImageConfiguration.empty),
|
||||
_decoder,
|
||||
) as MultiFrameImageStreamCompleter;
|
||||
|
||||
expect(completer.debugLabel, 'MemoryImage(${describeIdentity(bytes)}) - Resized(40×40)');
|
||||
});
|
||||
}
|
||||
|
||||
class FakeCodec implements Codec {
|
||||
@override
|
||||
void dispose() {}
|
||||
|
||||
@override
|
||||
int get frameCount => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<FrameInfo> getNextFrame() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
int get repetitionCount => throw UnimplementedError();
|
||||
}
|
||||
|
||||
class _TestAssetBundle extends CachingAssetBundle {
|
||||
@override
|
||||
Future<ByteData> load(String key) async {
|
||||
return Uint8List.fromList(kBlueRectPng).buffer.asByteData();
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
class TestImageInfo implements ImageInfo {
|
||||
const TestImageInfo(this.value, { this.image, this.scale = 1.0 });
|
||||
const TestImageInfo(this.value, { this.image, this.scale = 1.0, this.debugLabel });
|
||||
|
||||
@override
|
||||
final ui.Image image;
|
||||
@ -21,6 +21,9 @@ class TestImageInfo implements ImageInfo {
|
||||
@override
|
||||
final double scale;
|
||||
|
||||
@override
|
||||
final String debugLabel;
|
||||
|
||||
final int value;
|
||||
|
||||
@override
|
||||
|
||||
@ -8,10 +8,9 @@ import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
import '../flutter_test_alternative.dart';
|
||||
|
||||
class TestImage implements ui.Image {
|
||||
TestImage({ this.width, this.height });
|
||||
|
||||
@ -40,6 +39,10 @@ class TestCanvas implements Canvas {
|
||||
}
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
debugFlushLastFrameImageSizeInfo();
|
||||
});
|
||||
|
||||
test('Cover and align', () {
|
||||
final TestImage image = TestImage(width: 300, height: 300);
|
||||
final TestCanvas canvas = TestCanvas();
|
||||
@ -61,5 +64,114 @@ void main() {
|
||||
expect(command.positionalArguments[2], equals(const Rect.fromLTWH(50.0, 75.0, 200.0, 100.0)));
|
||||
});
|
||||
|
||||
testWidgets('Reports Image painting', (WidgetTester tester) async {
|
||||
ImageSizeInfo imageSizeInfo;
|
||||
int count = 0;
|
||||
debugOnPaintImage = (ImageSizeInfo info) {
|
||||
count += 1;
|
||||
imageSizeInfo = info;
|
||||
};
|
||||
|
||||
final TestImage image = TestImage(width: 300, height: 300);
|
||||
final TestCanvas canvas = TestCanvas();
|
||||
paintImage(
|
||||
canvas: canvas,
|
||||
rect: const Rect.fromLTWH(50.0, 75.0, 200.0, 100.0),
|
||||
image: image,
|
||||
debugImageLabel: 'test.png',
|
||||
);
|
||||
|
||||
expect(count, 1);
|
||||
expect(imageSizeInfo, isNotNull);
|
||||
expect(imageSizeInfo.source, 'test.png');
|
||||
expect(imageSizeInfo.imageSize, const Size(300, 300));
|
||||
expect(imageSizeInfo.displaySize, const Size(200, 100));
|
||||
|
||||
// Make sure that we don't report an identical image size info if we
|
||||
// redraw in the next frame.
|
||||
tester.binding.scheduleForcedFrame();
|
||||
await tester.pump();
|
||||
|
||||
paintImage(
|
||||
canvas: canvas,
|
||||
rect: const Rect.fromLTWH(50.0, 75.0, 200.0, 100.0),
|
||||
image: image,
|
||||
debugImageLabel: 'test.png',
|
||||
);
|
||||
|
||||
expect(count, 1);
|
||||
|
||||
debugOnPaintImage = null;
|
||||
});
|
||||
|
||||
testWidgets('Reports Image painting - change per frame', (WidgetTester tester) async {
|
||||
ImageSizeInfo imageSizeInfo;
|
||||
int count = 0;
|
||||
debugOnPaintImage = (ImageSizeInfo info) {
|
||||
count += 1;
|
||||
imageSizeInfo = info;
|
||||
};
|
||||
|
||||
final TestImage image = TestImage(width: 300, height: 300);
|
||||
final TestCanvas canvas = TestCanvas();
|
||||
paintImage(
|
||||
canvas: canvas,
|
||||
rect: const Rect.fromLTWH(50.0, 75.0, 200.0, 100.0),
|
||||
image: image,
|
||||
debugImageLabel: 'test.png',
|
||||
);
|
||||
|
||||
expect(count, 1);
|
||||
expect(imageSizeInfo, isNotNull);
|
||||
expect(imageSizeInfo.source, 'test.png');
|
||||
expect(imageSizeInfo.imageSize, const Size(300, 300));
|
||||
expect(imageSizeInfo.displaySize, const Size(200, 100));
|
||||
|
||||
// Make sure that we don't report an identical image size info if we
|
||||
// redraw in the next frame.
|
||||
tester.binding.scheduleForcedFrame();
|
||||
await tester.pump();
|
||||
|
||||
paintImage(
|
||||
canvas: canvas,
|
||||
rect: const Rect.fromLTWH(50.0, 75.0, 200.0, 150.0),
|
||||
image: image,
|
||||
debugImageLabel: 'test.png',
|
||||
);
|
||||
|
||||
expect(count, 2);
|
||||
expect(imageSizeInfo, isNotNull);
|
||||
expect(imageSizeInfo.source, 'test.png');
|
||||
expect(imageSizeInfo.imageSize, const Size(300, 300));
|
||||
expect(imageSizeInfo.displaySize, const Size(200, 150));
|
||||
|
||||
debugOnPaintImage = null;
|
||||
});
|
||||
|
||||
testWidgets('Reports Image painting - no debug label', (WidgetTester tester) async {
|
||||
ImageSizeInfo imageSizeInfo;
|
||||
int count = 0;
|
||||
debugOnPaintImage = (ImageSizeInfo info) {
|
||||
count += 1;
|
||||
imageSizeInfo = info;
|
||||
};
|
||||
|
||||
final TestImage image = TestImage(width: 300, height: 200);
|
||||
final TestCanvas canvas = TestCanvas();
|
||||
paintImage(
|
||||
canvas: canvas,
|
||||
rect: const Rect.fromLTWH(50.0, 75.0, 200.0, 100.0),
|
||||
image: image,
|
||||
);
|
||||
|
||||
expect(count, 1);
|
||||
expect(imageSizeInfo, isNotNull);
|
||||
expect(imageSizeInfo.source, '<Unknown Image(300×200)>');
|
||||
expect(imageSizeInfo.imageSize, const Size(300, 200));
|
||||
expect(imageSizeInfo.displaySize, const Size(200, 100));
|
||||
|
||||
debugOnPaintImage = null;
|
||||
});
|
||||
|
||||
// See also the DecorationImage tests in: decoration_test.dart
|
||||
}
|
||||
|
||||
@ -1731,6 +1731,47 @@ void main() {
|
||||
// See https://github.com/flutter/flutter/issues/54292.
|
||||
skip: kIsWeb,
|
||||
);
|
||||
|
||||
testWidgets('Reports image size when painted', (WidgetTester tester) async {
|
||||
ImageSizeInfo imageSizeInfo;
|
||||
int count = 0;
|
||||
debugOnPaintImage = (ImageSizeInfo info) {
|
||||
count += 1;
|
||||
imageSizeInfo = info;
|
||||
};
|
||||
|
||||
final ui.Image image = await tester.runAsync(() => createTestImage(kBlueRectPng));
|
||||
final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter(
|
||||
ImageInfo(
|
||||
image: image,
|
||||
scale: 1.0,
|
||||
debugLabel: 'test.png',
|
||||
),
|
||||
);
|
||||
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter);
|
||||
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
width: 50,
|
||||
child: Image(image: imageProvider),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(count, 1);
|
||||
expect(
|
||||
imageSizeInfo,
|
||||
const ImageSizeInfo(
|
||||
source: 'test.png',
|
||||
imageSize: Size(100, 100),
|
||||
displaySize: Size(50, 50),
|
||||
),
|
||||
);
|
||||
|
||||
debugOnPaintImage = null;
|
||||
});
|
||||
}
|
||||
|
||||
class ImagePainter extends CustomPainter {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user