Repository navigation
Add FadeInImageTransition.fadeInOver to fade the image in over the placeholder - #186246
Conversation
There was a problem hiding this comment.
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.
| 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(), | ||
| ), | ||
| ]), | ||
| ); | ||
| } |
There was a problem hiding this comment.
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
- The style guide encourages simplifying and refactoring code to enhance readability and maintainability. (link)
|
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 |
|
I have updated the title and description of this pull request to better reflect the suggested changes. |
victorsanni
left a comment
There was a problem hiding this comment.
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?
|
@victorsanni It is a trade-off: My preference is to accept this limitation and document it on |
|
hi @victorsanni any changes I need to do on my end for this PR or is it good to go? |
d99b585 to
84c01ab
Compare
|
hi @victorsanni I have addressed the requested changes; |
victorsanni
left a comment
There was a problem hiding this comment.
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.
84c01ab to
d2ed279
Compare
|
no worries @victorsanni , I have reverted back to enum, did extensive testing of my changes:
While testing the toggle I also hit a pre-existing crash unrelated to the |
71f52d1 to
5849bc0
Compare
a09809a to
4be681a
Compare
|
Addressed all three :
|
|
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. |
|
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. |
|
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. |
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
Adds a
FadeInImageTransitionenum and atransitionparameter toFadeInImage,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
fadeInOvermode the placeholder is never faded out, sofadeOutDurationandfadeOutCurvehave no effect; this is documented on the enum value, thetransitionproperty, 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 persistentAnimationController, so listeners accumulated.FadeInImageTransition_fadeInOver-2026-05-08_14.53.52.mp4
Fixes #97068
Pre-launch Checklist
///).