Layout animated GIFs only once (#143188)

Fixes https://github.com/flutter/flutter/issues/138610.

When `RenderImage` receives a new `Image` it only needs to fire up the layout machinery when the dimensions of the image have changed compared to the previous image. If the dimensions are the same, a repaint is sufficient.
This commit is contained in:
Michael Goderbauer 2024-02-08 17:12:06 -08:00 committed by GitHub
parent 5f1a3c16bd
commit 0aa9b5e17d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 57 additions and 1 deletions

View file

@ -93,10 +93,11 @@ class RenderImage extends RenderBox {
value.dispose();
return;
}
final bool sizeChanged = _image?.width != value?.width || _image?.height != value?.height;
_image?.dispose();
_image = value;
markNeedsPaint();
if (_width == null || _height == null) {
if (sizeChanged && (_width == null || _height == null)) {
markNeedsLayout();
}
}

View file

@ -2065,6 +2065,61 @@ void main() {
: isNot(throwsA(anything)),
);
});
testWidgets('Animated GIFs do not require layout for subsequent frames', (WidgetTester tester) async {
final ui.Codec codec = (await tester.runAsync(() {
return ui.instantiateImageCodec(Uint8List.fromList(kAnimatedGif));
}))!;
Future<ui.Image> nextFrame() async {
final ui.FrameInfo frameInfo = (await tester.runAsync(codec.getNextFrame))!;
return frameInfo.image;
}
final _TestImageStreamCompleter streamCompleter = _TestImageStreamCompleter();
final _TestImageProvider imageProvider = _TestImageProvider(streamCompleter: streamCompleter);
int? lastFrame;
await tester.pumpWidget(
Center(
child: Image(
image: imageProvider,
frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
lastFrame = frame;
return child;
},
),
),
);
expect(tester.getSize(find.byType(Image)), Size.zero);
streamCompleter.setData(imageInfo: ImageInfo(image: await nextFrame()));
await tester.pump();
expect(lastFrame, 0);
expect(tester.allRenderObjects.whereType<RenderImage>().single.debugNeedsLayout, isFalse);
expect(tester.allRenderObjects.whereType<RenderImage>().single.debugNeedsPaint, isFalse);
expect(tester.getSize(find.byType(Image)), const Size(1, 1));
streamCompleter.setData(imageInfo: ImageInfo(image: await nextFrame()));
// We only complete the build phase and expect that it does not mark the
// RenderImage for layout because the new frame has the same dimensions as
// the old one. We only need to repaint.
await tester.pump(null, EnginePhase.build);
expect(lastFrame, 1);
expect(tester.allRenderObjects.whereType<RenderImage>().single.debugNeedsLayout, isFalse);
expect(tester.allRenderObjects.whereType<RenderImage>().single.debugNeedsPaint, isTrue);
expect(tester.getSize(find.byType(Image)), const Size(1, 1));
streamCompleter.setData(imageInfo: ImageInfo(image: await nextFrame()));
await tester.pump();
expect(lastFrame, 2);
expect(tester.allRenderObjects.whereType<RenderImage>().single.debugNeedsLayout, isFalse);
expect(tester.allRenderObjects.whereType<RenderImage>().single.debugNeedsPaint, isFalse);
expect(tester.getSize(find.byType(Image)), const Size(1, 1));
codec.dispose();
});
}
@immutable