import 'dart:math' as math; import 'package:flutter/material.dart'; import 'theme.dart'; /// Drei tanzende Balken — das Zeichen dafür, dass genau dieser Titel läuft. /// /// Bei [laeuft] `false` (pausiert) stehen sie still, statt zu verschwinden: /// die Zeile bleibt als „hier bist du gerade" markiert, zeigt aber, dass /// nichts vorangeht. Sind die Systemanimationen abgeschaltet, stehen sie /// ebenfalls still — eine Endlosschleife ist genau das, was diese /// Einstellung vermeiden soll. class LaufBalken extends StatefulWidget { const LaufBalken({ super.key, required this.laeuft, this.hoehe = 16, this.farbe = Colors.white, }); final bool laeuft; final double hoehe; final Color farbe; @override State createState() => _LaufBalkenState(); } class _LaufBalkenState extends State with SingleTickerProviderStateMixin { static const _anzahl = 3; /// Versatz je Balken, damit sie nicht im Gleichschritt springen. static const _versatz = [0.0, 0.35, 0.7]; late final AnimationController _takt = AnimationController( vsync: this, duration: const Duration(milliseconds: 900), ); // Kein _richteTaktAus() in initState: die Entscheidung hängt an MediaQuery, // und darauf darf man dort noch nicht zugreifen. didChangeDependencies // läuft unmittelbar danach und erledigt es. @override void didUpdateWidget(LaufBalken alt) { super.didUpdateWidget(alt); if (alt.laeuft != widget.laeuft) _richteTaktAus(); } @override void didChangeDependencies() { super.didChangeDependencies(); _richteTaktAus(); } void _richteTaktAus() { final ruhig = MediaQuery.disableAnimationsOf(context); if (widget.laeuft && !ruhig) { if (!_takt.isAnimating) _takt.repeat(); } else { _takt.stop(); } } @override void dispose() { _takt.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return SizedBox( height: widget.hoehe, width: widget.hoehe, child: AnimatedBuilder( animation: _takt, builder: (context, _) => Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ for (var i = 0; i < _anzahl; i++) _Balken( anteil: _anteil(i), hoehe: widget.hoehe, farbe: widget.farbe, ), ], ), ), ); } /// Höhe eines Balkens zwischen 0,3 und 1,0. Stillstehend eine feste, /// ungleiche Form — damit man auch pausiert erkennt, was gemeint ist. double _anteil(int i) { if (!_takt.isAnimating) return const [0.4, 1.0, 0.6][i]; final phase = (_takt.value + _versatz[i]) * 2 * math.pi; return 0.3 + 0.7 * (0.5 + 0.5 * math.sin(phase)); } } class _Balken extends StatelessWidget { const _Balken({ required this.anteil, required this.hoehe, required this.farbe, }); final double anteil; final double hoehe; final Color farbe; @override Widget build(BuildContext context) { return Container( width: hoehe * 0.22, height: hoehe * anteil, decoration: BoxDecoration( color: farbe, borderRadius: BorderRadius.circular(hoehe * 0.11), ), ); } } /// Das Coverbild einer Songzeile mit der Lauf-Markierung darüber. /// /// Die Balken liegen **auf** dem Cover statt daneben: eine vierte Spalte in /// der Zeile würde dem Titel Platz nehmen, und die Zeile spränge beim /// Titelwechsel in der Breite. class LaufMarkierung extends StatelessWidget { const LaufMarkierung({ super.key, required this.markiert, required this.spielt, required this.child, this.radius = MeloRadius.element, }); /// Dieser Titel steht in der Wiedergabe — auch pausiert. final bool markiert; /// Die Wiedergabe läuft tatsächlich (steuert nur die Bewegung). final bool spielt; final Widget child; /// Muss zum Radius des Coverbilds darunter passen, sonst hat der Schleier /// andere Ecken als das Bild. final double radius; @override Widget build(BuildContext context) { if (!markiert) return child; return Stack( alignment: Alignment.center, children: [ child, // Abdunkeln, damit die weißen Balken auch auf hellen Covern stehen. Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.55), borderRadius: BorderRadius.circular(radius), ), ), ), LaufBalken(laeuft: spielt), ], ); } }