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). /// [ausVollbild] benennt den Aufrufer-Kontext explizit, statt ihn aus dem /// Vorzeichen von [strecke] zu raten (früherer Bug: bei bereits offenem /// Player klappte ein schwaches, versehentliches Aufwärtszittern — negative /// Strecke, weit unter der Schwelle — den Player zu, weil das Vorzeichen /// allein in den ÖFFNEN-Zweig führte, der bei so einer schwachen Geste /// `false` liefert). `false` (Mini-Player, Öffnen-Geste) entscheidet direkt /// über [oeffnetVollbildMitStrecke]; `true` (Vollbild-Player, Schließen- /// Geste) ü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, {required bool ausVollbild}) { final zielIstOffen = ausVollbild ? !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(); } }