WanderndesCover ersetzt Hero-Übergang: Cover wandert live mit progress

This commit is contained in:
Hermes (Server)
2026-08-29 14:50:40 +02:00
parent 9d0cb90a51
commit a1eb5831e0
4 changed files with 161 additions and 12 deletions
+43
View File
@@ -325,6 +325,22 @@ class _HomeShellState extends State<HomeShell>
},
child: const NowPlayingScreen(),
),
StreamBuilder<MediaItem?>(
stream: context.read<MeloAudioHandler>().mediaItem,
builder: (context, snapshot) {
final item = snapshot.data;
if (item == null) return const SizedBox.shrink();
return AnimatedBuilder(
animation: _expansion,
builder: (context, child) => WanderndesCover(
progress: _expansion.progress,
artUri: item.artUri,
miniRect: _miniCoverRect(context),
vollbildRect: _vollbildCoverRect(context),
),
);
},
),
],
),
// Haarlinie darüber: ohne sie geht die Leiste auf schwarzem Grund
@@ -377,4 +393,31 @@ class _HomeShellState extends State<HomeShell>
}
Rect _vollbildRect(BuildContext context) => Offset.zero & MediaQuery.sizeOf(context);
/// Feste 52×52-Größe, linksbündig mit MeloSpace.md-Abstand, vertikal
/// mittig in MiniPlayer.hoehe — Werte aus mini_player.dart übernommen
/// (dortiges Cover entfällt, siehe Task 6 Step 5).
Rect _miniCoverRect(BuildContext context) {
final mini = _miniRect(context);
const seite = 52.0;
final top = mini.top + (MiniPlayer.hoehe - seite) / 2;
return Rect.fromLTWH(mini.left + MeloSpace.md, top, seite, seite);
}
/// Nicht per Formel bestimmbar (Review-Panel-Fund): `_CoverPlatzhalter`
/// sitzt in NowPlayingScreens eigenem responsivem LayoutBuilder — die
/// tatsächliche Position wird stattdessen gemessen, siehe
/// coverPlatzhalterKey.
Rect _vollbildCoverRect(BuildContext context) {
final box = coverPlatzhalterKey.currentContext?.findRenderObject() as RenderBox?;
if (box == null || !box.hasSize) {
// Vor dem ersten Layout (z.B. ganz am Anfang, progress noch 0) —
// Notlösung: Platzhalter mittig im Vollbild-Rechteck annehmen, bis
// der erste Frame mit progress > 0 eine echte Messung liefert.
final voll = _vollbildRect(context);
final seite = voll.width - 48;
return Rect.fromLTWH(voll.left + 24, voll.top + 80, seite, seite);
}
return box.localToGlobal(Offset.zero) & box.size;
}
}
+5 -7
View File
@@ -2,7 +2,6 @@ import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../shared/cover.dart';
import '../shared/theme.dart';
import 'audio_handler.dart';
import 'now_playing_screen.dart';
@@ -136,12 +135,11 @@ class _LeisteState extends State<_Leiste> {
child: Row(
children: [
const SizedBox(width: MeloSpace.md),
// Dasselbe Bild wie im Vollbild: es wandert beim Öffnen
// dorthin, statt dass ein neuer Bildschirm aufspringt.
Hero(
tag: coverHeldenName,
child: CoverImage(artUri: item.artUri, size: 52),
),
// Das Cover rendert WanderndesCover in HomeShell in
// einer eigenen Ebene über allem (deckt exakt diese
// Position bei progress == 0 ab) — hier nur Platz
// freihalten, damit Titel/Bedienung nicht springen.
const SizedBox(width: 52),
const SizedBox(width: MeloSpace.md),
Expanded(
child: Column(
+36 -5
View File
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:ui' show lerpDouble;
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
@@ -7,6 +8,7 @@ import 'package:provider/provider.dart';
import '../library/database.dart';
import '../services/navidrome_service.dart';
import '../shared/cover.dart';
import '../shared/favorite_button.dart';
import '../shared/server_favorite_button.dart';
import '../shared/theme.dart';
@@ -101,11 +103,6 @@ class NowPlayingScreen extends StatelessWidget {
}
}
/// Gemeinsamer Name des Coverbilds in Mini-Player und Vollbild — bis Task 6
/// (WanderndesCover) den bisherigen Hero-Übergang vollständig ersetzt hat,
/// nutzt `mini_player.dart` diesen Namen noch für sein eigenes Hero-Cover.
const coverHeldenName = 'melo-cover';
/// Schlüssel des Cover-Platzhalters — Task 6 misst darüber die tatsächliche
/// Vollbild-Position/-Größe des Covers (`RenderBox.localToGlobal`), statt
/// `NowPlayingScreen`s responsives Layout (Row/Column-Weiche, AspectRatio)
@@ -122,6 +119,40 @@ class _CoverPlatzhalter extends StatelessWidget {
AspectRatio(key: coverPlatzhalterKey, aspectRatio: 1, child: const SizedBox());
}
/// Cover-Bild, das zwischen Mini-Player- und Vollbild-Rechteck wandert.
///
/// Ersetzt die frühere `Hero`-Animation: die feuert nur bei echten
/// Navigator-Transitions, und `NowPlayingScreen` ist seit dem Overlay-Umbau
/// keine Route mehr. Positionierung/Größe kommen direkt aus [progress],
/// live an die Zugstrecke der Wischgeste gekoppelt.
class WanderndesCover extends StatelessWidget {
const WanderndesCover({
super.key,
required this.progress,
required this.artUri,
required this.miniRect,
required this.vollbildRect,
});
final double progress;
final Uri? artUri;
final Rect miniRect;
final Rect vollbildRect;
static const _miniRadius = 6.0;
static const _vollbildRadius = 16.0;
@override
Widget build(BuildContext context) {
final rect = Rect.lerp(miniRect, vollbildRect, progress)!;
final radius = lerpDouble(_miniRadius, _vollbildRadius, progress)!;
return Positioned.fromRect(
rect: rect,
child: CoverImage(artUri: artUri, radius: radius),
);
}
}
/// Die bisherigen AppBar-Actions (Songtext, Sleep-Timer, Warteschlange) als
/// eigene Kopfzeile statt echter AppBar — eine Route/Scaffold gibt es hier
/// nicht mehr.