Skip to content

Add FadeInImageTransition.fadeInOver to fade the image in over the placeholder - #186246

Merged
auto-submit[bot] merged 5 commits into
flutter:masterfrom
anazr9:feat/fadeinimage-placeholder-transition-control
Sep 2, 2026
Merged

auto-submit[bot] merged 5 commits into
flutter:masterfrom
anazr9:feat/fadeinimage-placeholder-transition-control

Conversation

@anazr9

@anazr9 anazr9 commented May 8, 2026 •

Copy link
Copy Markdown
Contributor

Adds a FadeInImageTransition enum and a transition parameter to FadeInImage,
giving control over how the widget animates from the placeholder to the image:

  • FadeInImageTransition.sequential (default, unchanged behavior): the placeholder fades out, then the image fades in.

  • FadeInImageTransition.fadeInOver: the image fades in on top of the placeholder, which stays at full opacity until the image has fully loaded, so there is no blank gap between the two images.

  • In fadeInOver mode the placeholder is never faded out, so fadeOutDuration and fadeOutCurve have no effect; this is documented on the enum value, the transition property, and both fields.

  • Fixed a pre-existing status listener leak in _AnimatedFadeOutFadeInState: didUpdateTweens() registered a new status listener on every call, which forwards to the persistent AnimationController, so listeners accumulated.

FadeInImageTransition_fadeInOver-2026-05-08_14.53.52.mp4

Fixes #97068

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools.
  • I read the [Tree Hygiene] wiki page, which explains my responsibilities.
  • I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
  • I signed the [CLA].
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is [test-exempt].
  • I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
  • All existing and new tests are passing.

@github-actions github-actions Bot added the framework flutter/packages/flutter repository. See also f: labels. label May 8, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the FadeInImageTransition enum to FadeInImage, allowing users to choose between the default sequential fade and a new fadeInOver behavior where the image fades in directly over the placeholder. The implementation includes updates to the internal _AnimatedFadeOutFadeIn widget to manage animation durations, opacity sequences, and stack ordering based on the selected transition. Feedback suggests refactoring the animation status listener logic to reduce code duplication and improve maintainability.

Comment on lines +617 to +661
if (widget.transition == FadeInImageTransition.fadeInOver) {
// Image fades in on top of the placeholder. The placeholder stays at full
// opacity and is removed from the tree once the fade-in completes.
_targetOpacityAnimation = animation.drive(
_targetOpacity!.chain(CurveTween(curve: widget.fadeInCurve)),
);
_placeholderOpacityAnimation = animation.drive(ConstantTween<double>(1.0))
..addStatusListener((AnimationStatus status) {
if (_placeholderOpacityAnimation!.isCompleted) {
// Need to rebuild to remove placeholder now that it is invisible.
setState(() {});
}
});

_targetOpacityAnimation = animation.drive(
TweenSequence<double>(<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: ConstantTween<double>(0),
weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
),
TweenSequenceItem<double>(
tween: _targetOpacity!.chain(CurveTween(curve: widget.fadeInCurve)),
weight: widget.fadeInDuration.inMilliseconds.toDouble(),
),
]),
);
} else {
_placeholderOpacityAnimation =
animation.drive(
TweenSequence<double>(<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: _placeholderOpacity!.chain(CurveTween(curve: widget.fadeOutCurve)),
weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
),
TweenSequenceItem<double>(
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 invisible.
setState(() {});
}
});

_targetOpacityAnimation = animation.drive(
TweenSequence<double>(<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: ConstantTween<double>(0),
weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
),
TweenSequenceItem<double>(
tween: _targetOpacity!.chain(CurveTween(curve: widget.fadeInCurve)),
weight: widget.fadeInDuration.inMilliseconds.toDouble(),
),
]),
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for adding a status listener to _placeholderOpacityAnimation is duplicated in both branches of the if statement. This can be refactored to avoid duplication and improve maintainability. Additionally, moving the listener outside the if block allows for a single, unified comment explaining the need for the rebuild.

    if (widget.transition == FadeInImageTransition.fadeInOver) {
      // Image fades in on top of the placeholder. The placeholder stays at full
      // opacity and is removed from the tree once the fade-in completes.
      _targetOpacityAnimation = animation.drive(
        _targetOpacity!.chain(CurveTween(curve: widget.fadeInCurve)),
      );
      _placeholderOpacityAnimation = animation.drive(ConstantTween<double>(1.0));
    } else {
      _placeholderOpacityAnimation =
          animation.drive(
            TweenSequence<double>(<TweenSequenceItem<double>>[
              TweenSequenceItem<double>(
                tween: _placeholderOpacity!.chain(CurveTween(curve: widget.fadeOutCurve)),
                weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
              ),
              TweenSequenceItem<double>(
                tween: ConstantTween<double>(0),
                weight: widget.fadeInDuration.inMilliseconds.toDouble(),
              ),
            ]),
          );

      _targetOpacityAnimation = animation.drive(
        TweenSequence<double>(<TweenSequenceItem<double>>[
          TweenSequenceItem<double>(
            tween: ConstantTween<double>(0),
            weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
          ),
          TweenSequenceItem<double>(
            tween: _targetOpacity!.chain(CurveTween(curve: widget.fadeInCurve)),
            weight: widget.fadeInDuration.inMilliseconds.toDouble(),
          ),
        ]),
      );
    }

    _placeholderOpacityAnimation!.addStatusListener((AnimationStatus status) {
      if (_placeholderOpacityAnimation!.isCompleted) {
        // Need to rebuild to remove placeholder now that it is invisible or no longer needed.
        setState(() {});
      }
    });
References
  1. The style guide encourages simplifying and refactoring code to enhance readability and maintainability. (link)

@anazr9

anazr9 commented May 8, 2026

Copy link
Copy Markdown
Contributor Author

Hi @Piinks, I have made the changes in FadeInImage widget that allows images to appear after loading without the placeholder being gone using enums that specifies how this transition should behave

Comment thread packages/flutter/lib/src/widgets/fade_in_image.dart Outdated
@anazr9 anazr9 changed the title Add FadeInImageTransition enum to control placeholder fade-out behavior Add FadeInImage.fadeInOver constructor to skip placeholder fade-out May 9, 2026
@anazr9

anazr9 commented May 9, 2026

Copy link
Copy Markdown
Contributor Author

I have updated the title and description of this pull request to better reflect the suggested changes.

@victorsanni victorsanni added the CICD Run CI/CD label May 11, 2026

@victorsanni victorsanni left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with constructors though is that they create distinct code paths. Basically a developer cannot create a FadeInImage.memoryNetwork which is also a FadeInImage.fadeInOver.

Is this acceptable?

@anazr9

anazr9 commented May 19, 2026

Copy link
Copy Markdown
Contributor Author

@victorsanni It is a trade-off: memoryNetwork and assetNetwork are purely convenience constructors for building the ImageProviders, and users can achieve the same result with one extra line (MemoryImage(bytes) / NetworkImage(url)) when using FadeInImage.fadeInOver. Adding memoryNetworkFadeInOver / assetNetworkFadeInOver would cover all combinations but leads to a combinatorial explosion as more behaviors are added in the future.

My preference is to accept this limitation and document it on FadeInImage.fadeInOver, but happy to add the two extra constructors if you feel the ergonomics warrant it.

@victorsanni
victorsanni self-requested a review May 19, 2026 21:49
@anazr9

anazr9 commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

hi @victorsanni any changes I need to do on my end for this PR or is it good to go?

@Piinks
Piinks self-requested a review May 26, 2026 22:54
Comment thread packages/flutter/lib/src/widgets/fade_in_image.dart Outdated
Comment thread packages/flutter/lib/src/widgets/fade_in_image.dart Outdated
Comment thread packages/flutter/test/widgets/fade_in_image_test.dart Outdated
@anazr9
anazr9 force-pushed the feat/fadeinimage-placeholder-transition-control branch from d99b585 to 84c01ab Compare June 13, 2026 07:31
@github-actions github-actions Bot removed the CICD Run CI/CD label Jun 13, 2026
@anazr9

anazr9 commented Jun 13, 2026

Copy link
Copy Markdown
Contributor Author

hi @victorsanni I have addressed the requested changes; fadeInOver now sets fadeOutDuration to Duration.zero, documented fadeOutDuration & fadeOutCurve that they are ignored, and updated the test file. Thanks!

@anazr9
anazr9 requested a review from victorsanni June 13, 2026 07:42

@victorsanni victorsanni left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @anazr9, thanks for this PR.

After extensively thinking about this and evaluating the available options (public enum, flag, new constructor, etc), while also considering how the current properties will be used with each option, I think it is better to take the public enum approach. I'm worried about the API asymmetry a new constructor would introduce, as well as how many new constructors (or flags) we may need to introduce if we add another way to fade in/out.

With an enum, adding a new way to fade in/out does not bloat the API surface. I think we can use asserts to ensure fadeOutDuration is zero when fadeInOver is the current enum value etc. We should also call it out in the documentation to mitigate it.

Sorry for directing you towards the new constructor approach in the beginning, feels like we have come full circle.

@anazr9
anazr9 force-pushed the feat/fadeinimage-placeholder-transition-control branch from 84c01ab to d2ed279 Compare June 19, 2026 17:58
@anazr9 anazr9 changed the title Add FadeInImage.fadeInOver constructor to skip placeholder fade-out Add FadeInImageTransition.fadeInOver to fade the image in over the placeholder Jun 19, 2026
@anazr9

anazr9 commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

no worries @victorsanni , I have reverted back to enum, did extensive testing of my changes:

  • FadeInImageTransition is now public, with a transition parameter (default
    sequential) on all three constructors; the FadeInImage.fadeInOver
    constructor is removed.
  • Each constructor asserts fadeOutDuration == Duration.zero when transition
    is fadeInOver, so the ignored fade-out is an explicit error rather than a
    silent no-op, and the docs call it out.

While testing the toggle I also hit a pre-existing crash unrelated to the
enum: a Duration.zero fadeOutDuration in sequential mode builds a
zero-weight TweenSequenceItem, which asserts weight > 0. I fixed that in
this PR — the placeholder now stays visible until the image loads, then is
hidden immediately while the image fades in. Happy to split it into a separate
PR if you'd prefer.

@justinmc
justinmc requested a review from victorsanni June 30, 2026 22:17
Comment thread packages/flutter/lib/src/widgets/fade_in_image.dart Outdated
Comment thread packages/flutter/lib/src/widgets/fade_in_image.dart Outdated
@anazr9
anazr9 force-pushed the feat/fadeinimage-placeholder-transition-control branch from 71f52d1 to 5849bc0 Compare July 21, 2026 16:09
Comment thread packages/flutter/lib/src/widgets/fade_in_image.dart Outdated
Comment thread packages/flutter/lib/src/widgets/fade_in_image.dart Outdated
Comment thread packages/flutter/lib/src/widgets/fade_in_image.dart Outdated
@anazr9
anazr9 force-pushed the feat/fadeinimage-placeholder-transition-control branch from a09809a to 4be681a Compare July 23, 2026 13:23
@anazr9

anazr9 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Addressed all three :

transition doc — restructured to contrast both modes in a preceding paragraph with the default as the closing statement, and dropped the redundant "See [FadeInImageTransition]" line.
fadeOutDuration — each constructor now asserts it (and fadeOutCurve) are left at their defaults when transition is fadeInOver. The untouched default is allowed, so the common case needs nothing, but explicitly passing a value that would be silently ignored now throws. Extracted _defaultFadeOutDuration/_defaultFadeOutCurve so the defaults and the assert share one source of truth, and added tests.
fadeOutCurve — covered by the same assert, and its field doc now notes it must be left at its default in fadeInOver mode.
One heads-up: this means a runtime transition toggle that customizes fadeOutDuration for sequential must reset it to the default before switching to fadeInOver — the intended "loud, not silent" trade-off.

Comment thread packages/flutter/lib/src/widgets/fade_in_image.dart
@justinmc justinmc added the waiting for response The Flutter team cannot make further progress on this issue until the original reporter responds label Jul 28, 2026
@anazr9
anazr9 requested review from Piinks and victorsanni August 26, 2026 16:46

@Piinks Piinks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-LGTM

@Piinks Piinks added CICD Run CI/CD autosubmit Merge PR when tree becomes green via auto submit App labels Aug 26, 2026
@auto-submit

auto-submit Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/186246, because - The status or check suite Mac_arm64_verify_binaries has failed. Please fix the issues identified (or deflake) before re-applying this label.

@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Aug 26, 2026
@victorsanni victorsanni added the autosubmit Merge PR when tree becomes green via auto submit App label Aug 26, 2026
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Aug 26, 2026
@auto-submit

auto-submit Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/186246, because - The status or check suite Mac_arm64_verify_binaries has failed. Please fix the issues identified (or deflake) before re-applying this label.

@justinmc justinmc added the autosubmit Merge PR when tree becomes green via auto submit App label Sep 1, 2026
@auto-submit

auto-submit Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/186246, because - The status or check suite Mac_arm64_verify_binaries has failed. Please fix the issues identified (or deflake) before re-applying this label.

@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Sep 1, 2026
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Sep 1, 2026
@victorsanni victorsanni added CICD Run CI/CD autosubmit Merge PR when tree becomes green via auto submit App labels Sep 2, 2026
@auto-submit
auto-submit Bot added this pull request to the merge queue Sep 2, 2026
Merged via the queue into flutter:master with commit 644a7c8 Sep 2, 2026
27 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Sep 2, 2026
auto-submit Bot pushed a commit to flutter/packages that referenced this pull request Sep 4, 2026
flutter/flutter@0cbd1a4...70797e1

2026-09-02 [email protected] [analysis] Upgrade package:analyzer to 14.3.0 and enable custom plugin compilation (flutter/flutter#191590)
2026-09-02 [email protected] Roll Dart SDK from d8d7869d8031 to 9a8fbde80f6c (1 revision) (flutter/flutter#192179)
2026-09-02 [email protected] Roll Skia from dec311244db4 to ae22da2e8308 (1 revision) (flutter/flutter#192177)
2026-09-02 [email protected] [tool] Migrate DoctorCommand to modular dependency injection (flutter/flutter#190758)
2026-09-02 [email protected] Roll Skia from 482b7e285244 to dec311244db4 (3 revisions) (flutter/flutter#192161)
2026-09-02 [email protected] Roll Skia from 28447e9fb8d5 to 482b7e285244 (3 revisions) (flutter/flutter#192157)
2026-09-02 [email protected] feat: Add placeholder to DecorationImage (flutter/flutter#191528)
2026-09-02 [email protected] [Android] Do not schedule engine frames on a detached FlutterJNI (flutter/flutter#191204)
2026-09-02 [email protected] Roll Skia from 3a242fb7cdb3 to 28447e9fb8d5 (1 revision) (flutter/flutter#192145)
2026-09-02 [email protected] Roll Dart SDK from 9164def35347 to d8d7869d8031 (2 revisions) (flutter/flutter#192144)
2026-09-02 [email protected] Add FadeInImageTransition.fadeInOver to fade the image in over the placeholder (flutter/flutter#186246)
2026-09-02 [email protected] Roll Skia from 63d13ec4df6d to 3a242fb7cdb3 (9 revisions) (flutter/flutter#192138)
2026-09-02 [email protected] [flutter_tools] Add --no-plugins flag and disable plugins for benchmark (flutter/flutter#192129)
2026-09-02 [email protected] [tool] Migrate ChannelCommand to modular dependency injection (flutter/flutter#190751)
2026-09-01 [email protected] Add mouseCursor to RawScrollbar (flutter/flutter#185750)
2026-09-01 [email protected] Fix self-comparison and assert typos in TestSemantics (flutter/flutter#191938)
2026-09-01 [email protected] [Linux] Wait for frames to be rendered before using them in another context (flutter/flutter#192094)
2026-09-01 [email protected] [wimp] Turn on multithreading for wimp. (flutter/flutter#191747)
2026-09-01 [email protected] Roll Skia from 3911a1fe7f7a to 63d13ec4df6d (6 revisions) (flutter/flutter#192125)
2026-09-01 [email protected] Remove the redundant flutter/shell/platform/android:robolectric_tests build target (flutter/flutter#192117)
2026-09-01 [email protected] [iOS] Preserve semantics parents after reparenting (flutter/flutter#189686)
2026-09-01 [email protected] [flutter_tools] Report preview reload timing analytics in LspPreviewDetector (flutter/flutter#192120)
2026-09-01 [email protected] [flutter_tools] Implement Templates slice and flutter create integration (flutter/flutter#191748)
2026-09-01 [email protected] Uiscene migrate add2app hosts (flutter/flutter#191847)
2026-09-01 [email protected] Roll Packages from d642322 to 7a7912f (8 revisions) (flutter/flutter#192119)
2026-09-01 [email protected] [web] Anchor flt-semantics-host at 0,0 to fix WebKit semantics offset (flutter/flutter#190486)
2026-09-01 [email protected] Replace Shell::WaitForFirstFrame with an asynchronous API that matches the Shell threading model (flutter/flutter#191841)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC [email protected] on the revert to ensure that a human
is aware of the problem.

To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FadeInImage should allow the removal of the "fadeout" of the placeholder

5 participants