Items in song_list.dart und artist_list.dart faden/rutschen beim ersten Erscheinen gestaffelt statt abrupt ein — angelehnt an HyperOS' fließende Motion-Sprache. Läuft einmal pro Element, respektiert Reduce-Motion und deckelt die Verzögerung ab Index 12.
92 lines
2.7 KiB
Dart
92 lines
2.7 KiB
Dart
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<EinblendItem> createState() => _EinblendItemState();
|
|
}
|
|
|
|
class _EinblendItemState extends State<EinblendItem>
|
|
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<double> _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,
|
|
);
|
|
}
|
|
}
|