import 'dart:math' as math; import 'package:flutter/material.dart'; import 'theme.dart'; /// Blendet [child] beim ersten Erscheinen leicht gestaffelt ein — statt /// abrupt aufzupoppen, faded und rutscht es ein kleines Stück von unten /// heran. Je weiter hinten in der Liste, desto später beginnt es, damit die /// Liste als Ganzes fließt statt gleichzeitig aufzublitzen. /// /// Läuft genau einmal je Element-Lebenszeit: ein Rebuild (etwa weil sich der /// Song-Titel ändert) startet die Animation nicht erneut. class EinblendItem extends StatefulWidget { const EinblendItem({super.key, required this.child, required this.index}); final Widget child; /// Position in der Liste — bestimmt die Verzögerung vor dem Start. final int index; @override State createState() => _EinblendItemState(); } class _EinblendItemState extends State with SingleTickerProviderStateMixin { /// Ab Index 12 keine zusätzliche Verzögerung mehr — sonst wirkte eine /// lange Liste beim ersten Erscheinen träge, weil die letzten Zeilen erst /// spät anfingen. static const _deckel = 12; static const _versatzProItem = Duration(milliseconds: 25); late final AnimationController _controller = AnimationController( vsync: this, duration: MeloMotion.normal, ); late final Animation _kurve = CurvedAnimation( parent: _controller, curve: MeloMotion.curve, ); bool _gestartet = false; // Kein Start in initState: die Entscheidung hängt an MediaQuery // (Reduce-Motion), und darauf darf man dort noch nicht zugreifen — siehe // LaufBalken. didChangeDependencies läuft unmittelbar danach und ein // _gestartet-Merker verhindert, dass ein späterer Aufruf (z. B. durch // eine geänderte Textgröße) die Animation erneut anstößt. @override void didChangeDependencies() { super.didChangeDependencies(); if (_gestartet) return; _gestartet = true; if (MeloMotion.ruhig(context, MeloMotion.normal) == Duration.zero) { _controller.value = 1; return; } final verzoegerung = _versatzProItem * math.min(widget.index, _deckel); if (verzoegerung == Duration.zero) { _controller.forward(); } else { Future.delayed(verzoegerung, () { if (mounted) _controller.forward(); }); } } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _kurve, builder: (context, child) => Opacity( opacity: _kurve.value, child: Transform.translate( offset: Offset(0, MeloSpace.sm * (1 - _kurve.value)), child: child, ), ), child: widget.child, ); } }