diff --git a/packages/flutter/lib/src/painting/image_stream.dart b/packages/flutter/lib/src/painting/image_stream.dart index a0b27b7e0b..3b04f3a11c 100644 --- a/packages/flutter/lib/src/painting/image_stream.dart +++ b/packages/flutter/lib/src/painting/image_stream.dart @@ -974,7 +974,7 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter { @override void addListener(ImageStreamListener listener) { - if (!hasListeners && _codec != null) + if (!hasListeners && _codec != null && (_currentImage == null || _codec!.frameCount > 1)) _decodeNextFrameAndSchedule(); super.addListener(listener); } diff --git a/packages/flutter/test/painting/fake_codec.dart b/packages/flutter/test/painting/fake_codec.dart index 7b9553146f..d15be738f4 100644 --- a/packages/flutter/test/painting/fake_codec.dart +++ b/packages/flutter/test/painting/fake_codec.dart @@ -19,6 +19,7 @@ class FakeCodec implements ui.Codec { final int _repetitionCount; final List _frameInfos; int _nextFrame = 0; + int _numFramesAsked = 0; /// Creates a FakeCodec from encoded image data. /// @@ -38,8 +39,11 @@ class FakeCodec implements ui.Codec { @override int get repetitionCount => _repetitionCount; + int get numFramesAsked => _numFramesAsked; + @override Future getNextFrame() { + _numFramesAsked += 1; final SynchronousFuture result = SynchronousFuture(_frameInfos[_nextFrame]); _nextFrame = (_nextFrame + 1) % _frameCount; diff --git a/packages/flutter/test/painting/image_stream_test.dart b/packages/flutter/test/painting/image_stream_test.dart index 607d8a587f..274fd32a00 100644 --- a/packages/flutter/test/painting/image_stream_test.dart +++ b/packages/flutter/test/painting/image_stream_test.dart @@ -3,12 +3,16 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:typed_data'; import 'dart:ui'; import 'package:flutter/painting.dart'; import 'package:flutter/scheduler.dart' show timeDilation, SchedulerBinding; import 'package:flutter_test/flutter_test.dart'; +import '../image_data.dart'; +import 'fake_codec.dart'; + class FakeFrameInfo implements FrameInfo { const FakeFrameInfo(this._duration, this._image); @@ -780,6 +784,32 @@ void main() { handle.dispose(); }); + test('MultiFrameImageStreamCompleter - one frame image should only be decoded once', () async { + final FakeCodec oneFrameCodec = await FakeCodec.fromData(Uint8List.fromList(kTransparentImage)); + final Completer codecCompleter = Completer(); + final Completer decodeCompleter = Completer(); + final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter( + codec: codecCompleter.future, + scale: 1.0, + ); + final ImageStreamListener imageListener = ImageStreamListener((ImageInfo info, bool syncCall) { + decodeCompleter.complete(); + }); + + imageStream.keepAlive(); // do not dispose + imageStream.addListener(imageListener); + codecCompleter.complete(oneFrameCodec); + await decodeCompleter.future; + + imageStream.removeListener(imageListener); + expect(oneFrameCodec.numFramesAsked, 1); + + // Adding a new listener for decoded imageSteam, the one frame image should + // not be decoded again. + imageStream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {})); + expect(oneFrameCodec.numFramesAsked, 1); + }); // https://github.com/flutter/flutter/issues/82532 + // TODO(amirh): enable this once WidgetTester supports flushTimers. // https://github.com/flutter/flutter/issues/30344 // testWidgets('remove and add listener before a delayed frame is scheduled', (WidgetTester tester) async {