From d7d48c3594359c29074532193739f922246583d3 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Wed, 15 Apr 2026 11:48:12 +0200 Subject: [PATCH] Refactor TiltPresetPicker to use a circular D-pad for tilt controls; enhance gesture handling for smoother input Signed-off-by: Hans Kokx --- example/lib/main.dart | 124 +++++++++++++++++++++++++++++++++--------- 1 file changed, 98 insertions(+), 26 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 28d7a82..2b5eaf5 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -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 { const _TiltPresetPicker({required this.onChanged}); final ValueChanged 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 Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: FilledButton.tonalIcon( - onPressed: () => onChanged(const Offset(-0.7, 0.0)), - icon: const Icon(Icons.keyboard_double_arrow_left), - label: const Text('Tilt Left'), - ), + 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: [ + Align( + alignment: Alignment.topCenter, + child: IconButton.filledTonal( + tooltip: 'Tilt Up', + onPressed: () => + onChanged(const Offset(0.0, -_tiltAmount)), + icon: const Icon(Icons.keyboard_arrow_up), + ), + ), + Align( + alignment: Alignment.centerLeft, + 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), + icon: const Icon(Icons.adjust), + ), + ), + Align( + alignment: Alignment.centerRight, + child: IconButton.filledTonal( + tooltip: 'Tilt Right', + onPressed: () => + 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), + ), + ), + ], + ), + ), + ); + }, ), - const SizedBox(width: 8), - Expanded( - child: OutlinedButton.icon( - onPressed: () => onChanged(Offset.zero), - icon: const Icon(Icons.restart_alt), - label: const Text('Center'), - ), - ), - const SizedBox(width: 8), - Expanded( - child: FilledButton.tonalIcon( - onPressed: () => onChanged(const Offset(0.7, 0.0)), - icon: const Icon(Icons.keyboard_double_arrow_right), - label: const Text('Tilt Right'), - ), - ), - ], + ), ); } }