feat: federnde Übergänge und Animationen, Entwürfe je Raum
- Handy: Chat gleitet federnd über die Chatliste (Liste bleibt aufgebaut, Scrollposition erhalten), beim Zurück wieder hinaus; Liste schon während des Hinausgleitens bedienbar. Mitgliederleiste gleitet herein. - Neue/gesendete Nachrichten ploppen weich auf (nur live, nicht beim Öffnen oder Nachladen), Reaktionen ploppen auf und hüpfen beim Hochzählen, Ungelesen-Zähler hüpft, Scroll-nach-unten-Knopf federt (Handy größer), Antwort-Leiste klappt federnd auf/zu, Senden-Pfeil ploppt, Vollansicht zoomt auf. Alles skaliert mit Einstellung „Animationen“ (0 % = aus). - Fix: Beim Raumwechsel blieben Antwort-Ziel, Auswahl und getippter Text am neuen Raum hängen (Antwort auf Raum A konnte in Raum B rausgehen). Jetzt eigenes Eingabefeld je Raum mit Entwurf im Speicher. - Antwort-Leiste: größeres Schließen-X auf dem Handy. - Tests: test/spring_motion_test.dart, UI-Durchlauf mit Übergängen. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1d0ccfde8b
commit
ae6f651f4c
@@ -0,0 +1,150 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pyramid/core/theme.dart';
|
||||
import 'package:pyramid/layout/mobile_page_stack.dart';
|
||||
import 'package:pyramid/widgets/spring_press.dart';
|
||||
|
||||
/// Handy-Übergang Chatliste ↔ Chat und die federnden Einblendungen.
|
||||
void main() {
|
||||
Widget host({required Widget? front, double motion = 1}) {
|
||||
return PyramidTheme(
|
||||
motion: motion,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MobilePageStack(
|
||||
base: const _Counter(key: ValueKey('liste')),
|
||||
front: front,
|
||||
background: const Color(0xFF101010),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('Chatliste behält ihren Zustand, Chat gleitet herein und hinaus',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(host(front: null));
|
||||
await tester.tap(find.byKey(const ValueKey('liste')));
|
||||
await tester.pump();
|
||||
expect(find.text('Zähler 1'), findsOneWidget);
|
||||
|
||||
// Chat öffnen: gleitet von rechts herein.
|
||||
await tester.pumpWidget(host(front: const Text('Chat')));
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
final midX = tester.getTopLeft(find.text('Chat')).dx;
|
||||
expect(midX, greaterThan(0), reason: 'Chat sollte noch hereingleiten');
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getTopLeft(find.text('Chat')).dx, 0);
|
||||
// Verdeckte Liste ist nicht mehr bedienbar, aber noch aufgebaut.
|
||||
expect(find.text('Zähler 1', skipOffstage: false), findsOneWidget);
|
||||
|
||||
// Zurück: Chat gleitet hinaus und wird danach entfernt.
|
||||
await tester.pumpWidget(host(front: null));
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
expect(find.text('Chat'), findsOneWidget, reason: 'noch beim Hinausgleiten');
|
||||
// Schon während des Hinausgleitens ist die Liste wieder bedienbar.
|
||||
await tester.tapAt(const Offset(20, 300));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Zähler 2'), findsOneWidget);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Chat'), findsNothing);
|
||||
// Zustand der Liste ist erhalten geblieben.
|
||||
await tester.tap(find.byKey(const ValueKey('liste')));
|
||||
await tester.pump();
|
||||
expect(find.text('Zähler 3'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Animationen 0 %: sofortiger Wechsel ohne Gleiten', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(host(front: null, motion: 0));
|
||||
await tester.pumpWidget(host(front: const Text('Chat'), motion: 0));
|
||||
await tester.pump();
|
||||
expect(tester.getTopLeft(find.text('Chat')).dx, 0);
|
||||
await tester.pumpWidget(host(front: null, motion: 0));
|
||||
await tester.pump();
|
||||
expect(find.text('Chat'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('PopIn spielt nur, wenn verlangt, und endet unverändert', (
|
||||
tester,
|
||||
) async {
|
||||
Widget pop(bool play) => PyramidTheme(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: PopIn(play: play, child: const Text('Neu')),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(pop(true));
|
||||
await tester.pump(const Duration(milliseconds: 30));
|
||||
final opacity = tester.widget<Opacity>(
|
||||
find.ancestor(of: find.text('Neu'), matching: find.byType(Opacity)),
|
||||
);
|
||||
expect(opacity.opacity, lessThan(1));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester
|
||||
.widget<Opacity>(
|
||||
find.ancestor(of: find.text('Neu'), matching: find.byType(Opacity)),
|
||||
)
|
||||
.opacity,
|
||||
1,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await tester.pumpWidget(pop(false));
|
||||
expect(
|
||||
find.ancestor(of: find.text('Neu'), matching: find.byType(Opacity)),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('SpringBump hüpft nur beim Hochzählen', (tester) async {
|
||||
Widget bump(int v) => PyramidTheme(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(child: SpringBump(value: v, child: Text('$v'))),
|
||||
),
|
||||
);
|
||||
double scale() => tester
|
||||
.widget<Transform>(
|
||||
find.ancestor(
|
||||
of: find.byType(Text),
|
||||
matching: find.byType(Transform),
|
||||
),
|
||||
)
|
||||
.transform
|
||||
.storage[0]; // x-Skalierung (z bleibt bei Transform.scale immer 1)
|
||||
|
||||
await tester.pumpWidget(bump(1));
|
||||
expect(scale(), 1);
|
||||
await tester.pumpWidget(bump(2));
|
||||
await tester.pump(const Duration(milliseconds: 16));
|
||||
expect(scale(), lessThan(1), reason: 'Hochzählen soll hüpfen');
|
||||
await tester.pumpAndSettle();
|
||||
expect(scale(), closeTo(1, 0.001));
|
||||
await tester.pumpWidget(bump(0));
|
||||
await tester.pump(const Duration(milliseconds: 16));
|
||||
expect(scale(), closeTo(1, 0.001), reason: 'Herunterzählen hüpft nicht');
|
||||
});
|
||||
}
|
||||
|
||||
class _Counter extends StatefulWidget {
|
||||
const _Counter({super.key});
|
||||
|
||||
@override
|
||||
State<_Counter> createState() => _CounterState();
|
||||
}
|
||||
|
||||
class _CounterState extends State<_Counter> {
|
||||
int _n = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => setState(() => _n++),
|
||||
child: Center(child: Text('Zähler $_n')),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user