Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Fixes FadeInImage to follow gapless playback #94601

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 6, 2022

Conversation

werainkhatri
Copy link
Member

@werainkhatri werainkhatri commented Dec 3, 2021

FadeInImage does not follow gapless playback, even though it is documented in the last line of its documentation.

This is a regression, caused in #33370.

Fixes #94540

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • 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.
  • All existing and new tests are passing.

Bugs needed to be fixed:

  • When using two images, as in the issue's code sample, one of them is not cached (the first one to be specific), while the other is.

@flutter-dashboard flutter-dashboard bot added the framework flutter/packages/flutter repository. See also f: labels. label Dec 3, 2021
@google-cla google-cla bot added the cla: yes label Dec 3, 2021
@HansMuller HansMuller requested a review from goderbauer December 3, 2021 22:54
Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

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

It is not clear to me what this PR is trying to fix. Both provided test cases appear to be passing on master even without the change. Either the test cases are not covering the change or the existing implementation does already what the tests expect.

@@ -156,7 +156,66 @@ Future<void> main() async {
expect(findFadeInImage(tester).target.opacity, 1);
});

testWidgets('shows a cached image immediately when skipFadeOnSynchronousLoad=true', (WidgetTester tester) async {
testWidgets('re-animates updated uncached image', (WidgetTester tester) async {
Copy link
Member

Choose a reason for hiding this comment

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

This test appears to pass on master even without your change.

Copy link
Member Author

Choose a reason for hiding this comment

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

Improved this test. Now it fails on master, as when the image is changed to an uncached image, the placeholder isn't shown until the animation starts.

@goderbauer
Copy link
Member

I looked at #94540 and I agree that FadeInImage is behaving strangely today when switching out the image. We actually documented what should happen if you do this in https://master-api.flutter.dev/flutter/widgets/FadeInImage-class.html:

When either placeholder or image changes, this widget continues showing the previously loaded image (if any) until the new image provider provides a different image. This is known as "gapless playback" (see also Image.gaplessPlayback).

That used to be the behavior and something broke it. We should restore that behavior. However, as far as I can tell, this PR is not quite doing that. Instead, it is showing the placeholder again.

@goderbauer
Copy link
Member

My suspicion is actually that the bug is on this line:

isTargetLoaded: frame != null,

After we have loaded the target image for the first time, isTargetLoaded is correctly switched to true. But when you replace it with a new image, it switches back to false to show the placeholder again. That's incorrect it should continue to show the target image.

@goderbauer
Copy link
Member

These lines also look problematic for that:

if (wasSynchronouslyLoaded) {
_resetAnimations();
return child;
}

Once we have loaded the target once, we shouldn't be resetting the animation, I think.

@goderbauer
Copy link
Member

I actually suspect we broke this all the way back in #33370.

@werainkhatri
Copy link
Member Author

We actually documented what should happen if you do this in https://master-api.flutter.dev/flutter/widgets/FadeInImage-class.html:

When either placeholder or image changes, this widget continues showing the previously loaded image (if any) until the new image provider provides a different image. This is known as "gapless playback" (see also Image.gaplessPlayback).

I totally missed that. I understand now why ImplicitAnimatedWidget is the correct candidate. I'll try to go over the previous PR and fix this instead.

@werainkhatri
Copy link
Member Author

After we have loaded the target image for the first time, isTargetLoaded is correctly switched to true. But when you replace it with a new image, it switches back to false to show the placeholder again. That's incorrect it should continue to show the target image.

Does the new image fade in (fading out the old one)? Or does it replace it as soon as the new one is loaded?

@goderbauer
Copy link
Member

What does Image.gaplessPlayback do by default? That's what we should do here as well.

@werainkhatri
Copy link
Member Author

What does Image.gaplessPlayback do by default? That's what we should do here as well.

It updates the image as soon as the new one loads. Will do the same here.

@werainkhatri werainkhatri changed the title Renovating FadeInImage Fixes FadeInImage to follow gapless playback Jan 21, 2022
@werainkhatri
Copy link
Member Author

I have fixed the playback bug, but can't seem to understand how to fix the other bug (stated in the issue and this PR's description).

return child;
}
if (_imageAnimation.isCompleted) {
Copy link
Member

Choose a reason for hiding this comment

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

Is all this necessary? I naively expected that we would just need an instance variable (e.g. targetLoaded = false) and set that to true the first time frame != null. Then on line 443, we just pass targetLoaded to isTargetLoaded.

Maybe I am missing something, though?

Copy link
Member Author

Choose a reason for hiding this comment

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

  1. If you make the above-suggested change, run the sample code (attached in the issue), and you change the image (by clicking) a few times, after 4 changes (clicks) the view goes blank, and nothing will happen after any further clicks. This is because, after 3 clicks, the image changes to a loaded image, setting wasSynchronouslyLoaded to true, disposing _AnimatedFadeOutFadeIn and the animations. Now when the 4th click occurs, _AnimatedFadeOutFadeIn is rebuilt, resetting the opacity animations. This can be fixed by removing the wasSynchronouslyLoaded if statement, so that _AnimatedFadeOutFadeIn is never disposed.
  2. IMO the _AnimatedFadeOutFadeIn widget should be added to the tree only when the actual fade-out-fade-in is required, and removed when not. The above change (along with point 1) will cause it to exist on the tree forever.

Copy link
Contributor

Choose a reason for hiding this comment

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

It would be more performant by keeping the subtree the same, otherwise, they may be built from scratch

Copy link
Contributor

Choose a reason for hiding this comment

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

I can't reproduce the issue you mention after 4 clicks. This is the code i use

frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
        if (wasSynchronouslyLoaded) {
          _oldImage = child;
          return child;
        }
        if (frame != null) {
          // This is the first image.
          _oldImage = child;
        }
        return _AnimatedFadeOutFadeIn(
          target: child,
          targetProxyAnimation: _imageAnimation,
          placeholder: _image(
            image: widget.placeholder,
            errorBuilder: widget.placeholderErrorBuilder,
            opacity: _placeholderAnimation,
            fit: widget.placeholderFit ?? widget.fit,
          ),
          placeholderProxyAnimation: _placeholderAnimation,
          isTargetLoaded: _oldImage != null,
          fadeInDuration: widget.fadeInDuration,
          fadeOutDuration: widget.fadeOutDuration,
          fadeInCurve: widget.fadeInCurve,
          fadeOutCurve: widget.fadeOutCurve,
        );
      },

The issue with the current code is that after the image is faded in, it will cause a an unnecessary rebuild to dispose _AnimatedFadeOutFadeIn and rebuild the image widget which is not ideal

Copy link
Member Author

@werainkhatri werainkhatri Feb 20, 2022

Choose a reason for hiding this comment

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

i am able to reproduce it on my side with your code.

The issue with the current code is that after the image is faded in, it will cause a an unnecessary rebuild to dispose _AnimatedFadeOutFadeIn and rebuild the image widget which is not ideal.

this can be fixed if we remove the wasSynchronouslyLoaded if block and let the _AnimatedFadeOutFadeIn return the image in it.

Copy link
Contributor

Choose a reason for hiding this comment

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

disposing _AnimatedFadeOutFadeIn and the animations. Now when the 4th click occurs, _AnimatedFadeOutFadeIn is rebuilt, resetting the opacity animations.

Wheere does the animations is disposed? It looks like we do need to reset the animation when the wasSynchronouslyLoaded is true to prepare for future rebuilds?

Copy link
Member Author

Choose a reason for hiding this comment

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

Where does the animations is disposed?

my bad, animations are not disposed, just the widget.

It looks like we do need to reset the animation when the wasSynchronouslyLoaded is true to prepare for future rebuilds?

As per gapless playback, there won't be any future animations. The old image will just be replaced by the new one after it is loaded.

@werainkhatri werainkhatri force-pushed the renovate-fadeinimage branch from c20ff99 to 33b7996 Compare March 31, 2022 07:04
@werainkhatri werainkhatri force-pushed the renovate-fadeinimage branch from 33b7996 to d7f7ea4 Compare March 31, 2022 07:05
@werainkhatri
Copy link
Member Author

werainkhatri commented Mar 31, 2022

@chunhtai @goderbauer PTAL
I have made some improvements to the code. Now, the _AnimatedFadeInFadeOut widget will never leave the tree (i.e. never dispose) and sync logic is passed down for _AnimatedFadeInFadeOut to handle internally.

Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

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

mostly looks good, left some comment on part of the code that is not clear to me

@@ -534,23 +537,14 @@ class _AnimatedFadeOutFadeInState extends ImplicitlyAnimatedWidgetState<_Animate
weight: widget.fadeInDuration.inMilliseconds.toDouble(),
),
]));
if (!widget.isTargetLoaded && _isValid(_placeholderOpacity!) && _isValid(_targetOpacity!)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

why remove this logic?

Copy link
Member Author

Choose a reason for hiding this comment

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

as the comment suggests, this code jumps back to the placeholder image for the "new" target image.

i.e., old image --[user changes the image]--> jump to placeholder --[wait for new image]--> fade in new image.

but according to image gapless playback, the placeholder will never be shown again, the old image will act as a placeholder and the new image replace the old one w/o "fade in fade out".

Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

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

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

FadeInImage works unexpectedly
3 participants