fix(player): smooth mini bar cover swap across track change
Audio handler broadcasts MediaItem twice on every track change: once with artUri=null (the new track's bare metadata), then again with artUri pointing at the AlbumCoverCache file once the cover lands on disk. The mini player's cover element was rebuilding in place: previous track's image → slate placeholder → new track's image. That flash is the flicker reported on the v2026.05.13.0 build. AnimatedSwitcher around the cover (180ms crossfade) keyed by the artUri value makes the swap a smooth crossfade instead of a visible snap. The Hero parent stays — its tag is stable across track changes so the mini→full bar expansion animation keeps working unchanged.
This commit is contained in:
@@ -95,14 +95,25 @@ class _TrackInfo extends StatelessWidget {
|
||||
// artwork from this 48dp footprint to the full-screen size rather
|
||||
// than fade-cutting. Tag is stable per-route (not keyed by media.id)
|
||||
// so the transition works regardless of what's playing.
|
||||
Widget cover;
|
||||
//
|
||||
// AnimatedSwitcher around the cover smooths the artUri null→file://
|
||||
// swap that fires on every track change: audio_handler broadcasts
|
||||
// the new MediaItem without artUri immediately, then re-broadcasts
|
||||
// with artUri once AlbumCoverCache resolves the path. Without the
|
||||
// crossfade the cover snaps from the previous track's image to a
|
||||
// slate placeholder to the new image — visible as a flicker.
|
||||
final Widget coverChild;
|
||||
if (media.artUri != null) {
|
||||
// CachedNetworkImageProvider for HTTPS art URIs so the mini bar
|
||||
// hits the same disk cache the rest of the UI uses (ServerImage,
|
||||
// discover thumbnails). FileImage stays for the file:// branch
|
||||
// populated by AlbumCoverCache — bytes are already on disk and a
|
||||
// second cache layer would only burn duplicate space.
|
||||
cover = Image(
|
||||
coverChild = Image(
|
||||
// Key includes the artUri so AnimatedSwitcher detects a swap;
|
||||
// without this it treats successive Image widgets as the same
|
||||
// tree node and skips the transition.
|
||||
key: ValueKey('art-${media.artUri}'),
|
||||
image: media.artUri!.isScheme('file')
|
||||
? FileImage(File.fromUri(media.artUri!)) as ImageProvider
|
||||
: CachedNetworkImageProvider(media.artUri.toString()),
|
||||
@@ -117,8 +128,19 @@ class _TrackInfo extends StatelessWidget {
|
||||
Container(width: 48, height: 48, color: fs.slate),
|
||||
);
|
||||
} else {
|
||||
cover = Container(width: 48, height: 48, color: fs.slate);
|
||||
coverChild = Container(
|
||||
key: const ValueKey('art-placeholder'),
|
||||
width: 48,
|
||||
height: 48,
|
||||
color: fs.slate,
|
||||
);
|
||||
}
|
||||
final cover = AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeOut,
|
||||
child: coverChild,
|
||||
);
|
||||
return Row(
|
||||
// Default centering vertically aligns the like/kebab buttons
|
||||
// against the album art (48dp) — visually they span the title +
|
||||
|
||||
Reference in New Issue
Block a user