Avoid including a potentially animated invisible image (#50842)

* Avoid including a potentially animated invisible image
diff --git a/packages/flutter/lib/src/widgets/fade_in_image.dart b/packages/flutter/lib/src/widgets/fade_in_image.dart
index 5cbc0b2..feae786 100644
--- a/packages/flutter/lib/src/widgets/fade_in_image.dart
+++ b/packages/flutter/lib/src/widgets/fade_in_image.dart
@@ -448,7 +448,13 @@
         tween: ConstantTween<double>(0),
         weight: widget.fadeInDuration.inMilliseconds.toDouble(),
       ),
-    ]));
+    ]))..addStatusListener((AnimationStatus status) {
+      if (_placeholderOpacityAnimation.isCompleted) {
+        // Need to rebuild to remove placeholder now that it is invisibile.
+        setState(() {});
+      }
+    });
+
     _targetOpacityAnimation = animation.drive(TweenSequence<double>(<TweenSequenceItem<double>>[
       TweenSequenceItem<double>(
         tween: ConstantTween<double>(0),
@@ -472,6 +478,15 @@
 
   @override
   Widget build(BuildContext context) {
+    final Widget target = FadeTransition(
+      opacity: _targetOpacityAnimation,
+      child: widget.target,
+    );
+
+    if (_placeholderOpacityAnimation.isCompleted) {
+      return target;
+    }
+
     return Stack(
       fit: StackFit.passthrough,
       alignment: AlignmentDirectional.center,
@@ -479,10 +494,7 @@
       // but it allows the Stack to avoid a call to Directionality.of()
       textDirection: TextDirection.ltr,
       children: <Widget>[
-        FadeTransition(
-          opacity: _targetOpacityAnimation,
-          child: widget.target,
-        ),
+        target,
         FadeTransition(
           opacity: _placeholderOpacityAnimation,
           child: widget.placeholder,
diff --git a/packages/flutter/test/widgets/fade_in_image_test.dart b/packages/flutter/test/widgets/fade_in_image_test.dart
index 97aaec2..5058326 100644
--- a/packages/flutter/test/widgets/fade_in_image_test.dart
+++ b/packages/flutter/test/widgets/fade_in_image_test.dart
@@ -209,6 +209,28 @@
       expect(findFadeInImage(tester).state, same(state));
     });
 
+    testWidgets('does not keep the placeholder in the tree if it is invisible', (WidgetTester tester) async {
+      final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
+      final TestImageProvider imageProvider = TestImageProvider(targetImage);
+
+      await tester.pumpWidget(FadeInImage(
+        placeholder: placeholderProvider,
+        image: imageProvider,
+        fadeOutDuration: animationDuration,
+        fadeInDuration: animationDuration,
+        excludeFromSemantics: true,
+      ));
+
+      placeholderProvider.complete();
+      await tester.pumpAndSettle();
+
+      expect(find.byType(Image), findsNWidgets(2));
+
+      imageProvider.complete();
+      await tester.pumpAndSettle();
+      expect(find.byType(Image), findsOneWidget);
+    });
+
     testWidgets('re-fades in the image when the target image is updated', (WidgetTester tester) async {
       final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
       final TestImageProvider imageProvider = TestImageProvider(targetImage);
@@ -285,7 +307,7 @@
       expect(findFadeInImage(tester).target.opacity, moreOrLessEquals(1));
     });
 
-    group(ImageProvider, () {
+    group('ImageProvider', () {
 
       testWidgets('memory placeholder cacheWidth and cacheHeight is passed through', (WidgetTester tester) async {
         final Uint8List testBytes = Uint8List.fromList(kTransparentImage);