import 'package:flutter/material.dart'; import '../shared/theme.dart'; import 'wischen.dart'; /// Wie viel Zugstrecke (Pixel) einem vollen Öffnen-Fortschritt entspricht — /// rein darstellerisch, unabhängig von der Öffnen/Schließen-Entscheidung in /// [wischen.dart]. const oeffnenReferenzHoehe = 300.0; /// Optischer Fortschritt zwischen Mini-Player (0) und Vollbild-Player (1). /// /// Trennt bewusst zwei Dinge: WANN geöffnet/geschlossen wird (unverändert /// [oeffnetVollbildMitStrecke] aus wischen.dart) und WIE WEIT der Übergang /// gerade optisch ist (dieser Wert, live an die Zugstrecke gekoppelt). class PlayerExpansionController extends ChangeNotifier { PlayerExpansionController({required TickerProvider vsync}) : _animation = AnimationController(vsync: vsync) { _animation.addListener(notifyListeners); } final AnimationController _animation; double get progress => _animation.value; /// [dy] ist details.delta.dy einer laufenden Wischgeste, unverändertes /// Vorzeichen. Nach oben (negativ) erhöht progress, nach unten (positiv) /// verringert ihn — dieselbe Formel bedient Öffnen- und Schließen-Geste /// symmetrisch, je nachdem, wo progress gerade steht. Bewusst rekursiv auf /// dem bereits geclampten Wert (nicht auf der aufsummierten Rohstrecke): /// so reagiert eine Richtungsumkehr mitten in der Geste sofort, auch wenn /// die Geste vorher schon über [oeffnenReferenzHoehe] hinausgezogen wurde. void dragBy(double dy) { _animation.value = (_animation.value - dy / oeffnenReferenzHoehe).clamp(0.0, 1.0); } /// [strecke]/[geschwindigkeit] wie in wischen.dart (negativ = nach oben). /// Bei einer Öffnen-Geste (strecke <= 0) direkt über /// [oeffnetVollbildMitStrecke] entschieden; bei einer Schließen-Geste /// (strecke >= 0, im Vollbild-Player nach unten gezogen) über dieselbe /// Funktion mit gespiegeltem Vorzeichen — "stark genug nach unten" ist /// spiegelbildlich zu "stark genug nach oben". Fliegt danach mit der /// echten Loslass-Geschwindigkeit als Startimpuls zum entschiedenen Ziel. void dragEnd(BuildContext context, double strecke, double geschwindigkeit) { final zielIstOffen = strecke <= 0 ? oeffnetVollbildMitStrecke(strecke, geschwindigkeit) : !oeffnetVollbildMitStrecke(-strecke, -geschwindigkeit); if (MediaQuery.disableAnimationsOf(context)) { _animation.value = zielIstOffen ? 1.0 : 0.0; return; } // -geschwindigkeit/oeffnenReferenzHoehe, weil dragBy() ein negatives dy // (nach oben) in steigenden progress übersetzt — dieselbe Umrechnung // rückwärts. Zeigt die tatsächliche Geschwindigkeit nicht zum // entschiedenen Ziel (z.B. ein Aufwärtswisch, der am Ende minimal // zurückfedert), erzwingt ein moderater Mindestimpuls die Zielrichtung, // statt fling() mit widersprüchlichem Vorzeichen zu verwirren. final rohimpuls = -geschwindigkeit / oeffnenReferenzHoehe; final zeigtZumZiel = zielIstOffen ? rohimpuls > 0 : rohimpuls < 0; final impuls = zeigtZumZiel ? rohimpuls : (zielIstOffen ? 1.0 : -1.0); _animation.fling(velocity: impuls); } void open(BuildContext context) => _animiereZu(context, 1.0); void close(BuildContext context) => _animiereZu(context, 0.0); /// Für Tap-Auslöser (kein Ziehen, keine Loslass-Geschwindigkeit) — anders /// als [dragEnd] mit fester Kurve/Dauer aus [MeloMotion], nicht als Fling. void _animiereZu(BuildContext context, double ziel) { if (MediaQuery.disableAnimationsOf(context)) { _animation.value = ziel; return; } _animation.animateTo(ziel, duration: MeloMotion.normal, curve: MeloMotion.curve); } @override void dispose() { _animation.dispose(); super.dispose(); } }