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:
2026-05-14 07:37:42 -04:00
parent 3e7b2582a2
commit 6a08d94255
+25 -3
View File
@@ -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 +