-
Notifications
You must be signed in to change notification settings - Fork 28.7k
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
Fixes FadeInImage
to follow gapless playback
#94601
Conversation
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
I looked at #94540 and I agree that FadeInImage is behaving strangely today when switching out the
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. |
My suspicion is actually that the bug is on this line:
After we have loaded the target image for the first time, |
These lines also look problematic for that: flutter/packages/flutter/lib/src/widgets/fade_in_image.dart Lines 418 to 421 in d063961
Once we have loaded the target once, we shouldn't be resetting the animation, I think. |
I actually suspect we broke this all the way back in #33370. |
I totally missed that. I understand now why |
Does the new image fade in (fading out the old one)? Or does it replace it as soon as the new one is loaded? |
What does |
It updates the image as soon as the new one loads. Will do the same here. |
e102d3d
to
016cef3
Compare
FadeInImage
FadeInImage
to follow gapless playback
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- 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 thewasSynchronouslyLoaded
if statement, so that_AnimatedFadeOutFadeIn
is never disposed. - 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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
c20ff99
to
33b7996
Compare
33b7996
to
d7f7ea4
Compare
@chunhtai @goderbauer PTAL |
There was a problem hiding this 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!)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why remove this logic?
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
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
///
).Bugs needed to be fixed: