Bernds Feedback 2026-10-07: - Schrift-Standard Handy 110 % (statt 115 %). - PyrIconBtn: im Handy-Layout Symbol 22 px (vorher fest 16), Flaeche 40, Tippflaeche 44; Farben aus dem Farbmodus statt fest verdrahtet. - Eingabeleiste: Anhaengen/GIF/Emoji/Senden auf dem Handy 22-23 px statt 12-13 px. - Chat-Kopf (Handy): Anrufen + Videoanruf sichtbar, Suchen + Mitglieder im Drei-Punkte-Menue. - Aufklappbarer DM-Kopf als Ebenen: Zurueck + Symbole bleiben oben, Profilbereich gleitet darunter und nutzt die volle Breite (vorher Name zu "p..." gekuerzt, Symbole rutschten in die Mitte); Kuerzel im Avatar mittig, skaliert mit. - SpringPress (widgets/spring_press.dart): federnde Druck-Rueckmeldung, aus bei "Animationen 0 %". Co-Authored-By: Claude Opus 5.5 <[email protected]>
72 lines
2.3 KiB
Dart
72 lines
2.3 KiB
Dart
import 'package:flutter/widgets.dart';
|
||
import 'package:pyramid/core/theme.dart';
|
||
|
||
/// Federnde Druck-Rückmeldung: beim Drücken leicht kleiner, beim Loslassen
|
||
/// elastisch zurück (Bernds Wunsch 2026-10-07: mehr „Springiness“).
|
||
///
|
||
/// Hört nur passiv mit (Listener) – Tippen, Halten und Scrollen bleiben beim
|
||
/// Kind bzw. beim umgebenden Scrollbereich. Bewegt sich der Finger (Scrollen),
|
||
/// wird die Rückmeldung sofort abgebrochen. Mit „Animationen: 0 %“ in den
|
||
/// Einstellungen (PyramidTheme.motion) ist sie aus.
|
||
class SpringPress extends StatefulWidget {
|
||
const SpringPress({
|
||
super.key,
|
||
required this.child,
|
||
this.pressedScale = 0.88,
|
||
this.enabled = true,
|
||
});
|
||
|
||
final Widget child;
|
||
final double pressedScale;
|
||
final bool enabled;
|
||
|
||
@override
|
||
State<SpringPress> createState() => _SpringPressState();
|
||
}
|
||
|
||
class _SpringPressState extends State<SpringPress> {
|
||
static const _moveSlop = 8.0;
|
||
bool _pressed = false;
|
||
Offset? _down;
|
||
|
||
void _set(bool pressed) {
|
||
if (_pressed != pressed && mounted) setState(() => _pressed = pressed);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final motion = PyramidTheme.of(context).motion;
|
||
if (!widget.enabled || motion < 0.05) return widget.child;
|
||
return Listener(
|
||
behavior: HitTestBehavior.translucent,
|
||
onPointerDown: (e) {
|
||
_down = e.position;
|
||
_set(true);
|
||
},
|
||
onPointerMove: (e) {
|
||
final d = _down;
|
||
if (d != null && (e.position - d).distance > _moveSlop) _set(false);
|
||
},
|
||
onPointerUp: (_) => _set(false),
|
||
onPointerCancel: (_) => _set(false),
|
||
child: AnimatedScale(
|
||
scale: _pressed ? widget.pressedScale : 1.0,
|
||
duration: _pressed
|
||
? const Duration(milliseconds: 90)
|
||
: Duration(milliseconds: (420 * motion).round().clamp(120, 420)),
|
||
curve: _pressed ? Curves.easeOut : Curves.elasticOut,
|
||
child: widget.child,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Gemeinsame „federnde“ Kurve für Ein-/Ausblendungen (leichtes Überschwingen).
|
||
const springyCurve = Cubic(0.18, 0.89, 0.32, 1.12);
|
||
|
||
/// Dauer, skaliert mit der Einstellung „Animationen“ (0 = sofort).
|
||
Duration motionDuration(BuildContext context, int milliseconds) {
|
||
final motion = PyramidTheme.of(context).motion;
|
||
return Duration(milliseconds: (milliseconds * motion).round());
|
||
}
|