Refactor TiltPresetPicker to use a circular D-pad for tilt controls; enhance gesture handling for smoother input

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-04-15 11:48:12 +02:00
parent 76069020de
commit d7d48c3594
+90 -18
View File
@@ -349,40 +349,112 @@ class _SparkleShapePicker extends StatelessWidget {
} }
} }
/// Preset controls for feeding an external tilt stream. /// Circular D-pad control for feeding the external tilt stream.
class _TiltPresetPicker extends StatelessWidget { class _TiltPresetPicker extends StatelessWidget {
const _TiltPresetPicker({required this.onChanged}); const _TiltPresetPicker({required this.onChanged});
final ValueChanged<Offset> onChanged; final ValueChanged<Offset> onChanged;
static const double _tiltAmount = 0.7;
static const double _deadZoneRatio = 0.18;
void _emitDragTilt(Offset localPosition, Size size) {
final Offset center = Offset(size.width / 2.0, size.height / 2.0);
final double radius = size.shortestSide / 2.0;
final Offset delta = localPosition - center;
final double distance = delta.distance;
if (radius <= 0 || distance <= radius * _deadZoneRatio) {
onChanged(Offset.zero);
return;
}
Offset normalized = delta / radius;
final double normalizedDistance = normalized.distance;
if (normalizedDistance > 1.0) {
normalized = normalized / normalizedDistance;
}
onChanged(normalized * _tiltAmount);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Center(
child: SizedBox(
width: 176,
height: 176,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final Size size = constraints.biggest;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (DragDownDetails details) {
_emitDragTilt(details.localPosition, size);
},
onPanUpdate: (DragUpdateDetails details) {
_emitDragTilt(details.localPosition, size);
},
onPanEnd: (_) => onChanged(Offset.zero),
onPanCancel: () => onChanged(Offset.zero),
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
),
child: Stack(
children: <Widget>[ children: <Widget>[
Expanded( Align(
child: FilledButton.tonalIcon( alignment: Alignment.topCenter,
onPressed: () => onChanged(const Offset(-0.7, 0.0)), child: IconButton.filledTonal(
icon: const Icon(Icons.keyboard_double_arrow_left), tooltip: 'Tilt Up',
label: const Text('Tilt Left'), onPressed: () =>
onChanged(const Offset(0.0, -_tiltAmount)),
icon: const Icon(Icons.keyboard_arrow_up),
), ),
), ),
const SizedBox(width: 8), Align(
Expanded( alignment: Alignment.centerLeft,
child: OutlinedButton.icon( child: IconButton.filledTonal(
tooltip: 'Tilt Left',
onPressed: () =>
onChanged(const Offset(-_tiltAmount, 0.0)),
icon: const Icon(Icons.keyboard_arrow_left),
),
),
Align(
alignment: Alignment.center,
child: IconButton.outlined(
tooltip: 'Center Tilt',
onPressed: () => onChanged(Offset.zero), onPressed: () => onChanged(Offset.zero),
icon: const Icon(Icons.restart_alt), icon: const Icon(Icons.adjust),
label: const Text('Center'),
), ),
), ),
const SizedBox(width: 8), Align(
Expanded( alignment: Alignment.centerRight,
child: FilledButton.tonalIcon( child: IconButton.filledTonal(
onPressed: () => onChanged(const Offset(0.7, 0.0)), tooltip: 'Tilt Right',
icon: const Icon(Icons.keyboard_double_arrow_right), onPressed: () =>
label: const Text('Tilt Right'), onChanged(const Offset(_tiltAmount, 0.0)),
icon: const Icon(Icons.keyboard_arrow_right),
),
),
Align(
alignment: Alignment.bottomCenter,
child: IconButton.filledTonal(
tooltip: 'Tilt Down',
onPressed: () =>
onChanged(const Offset(0.0, _tiltAmount)),
icon: const Icon(Icons.keyboard_arrow_down),
), ),
), ),
], ],
),
),
);
},
),
),
); );
} }
} }