import 'package:flutter/material.dart'; class Joystick2D extends StatefulWidget { final ValueChanged onChanged; const Joystick2D({ super.key, required this.onChanged, }); @override State createState() => _Joystick2DState(); } class _Joystick2DState extends State { Offset _stickPosition = Offset.zero; bool _active = false; void _onPanUpdate(Offset localPosition, Size size) { final center = size.center(Offset.zero); final delta = localPosition - center; final distance = delta.distance; final maxDistance = size.width / 2; final constrainedDelta = distance > maxDistance ? Offset( delta.dx * maxDistance / distance, delta.dy * maxDistance / distance, ) : delta; setState(() { _stickPosition = constrainedDelta; _active = true; }); final normalized = Offset( constrainedDelta.dx / maxDistance, constrainedDelta.dy / maxDistance, ); widget.onChanged(normalized); } void _resetJoystick() { setState(() { _stickPosition = Offset.zero; _active = false; }); widget.onChanged(Offset.zero); } @override Widget build(BuildContext context) { return GestureDetector( behavior: HitTestBehavior.opaque, onPanStart: (details) { final box = context.findRenderObject() as RenderBox; final localPosition = box.globalToLocal(details.globalPosition); _onPanUpdate(localPosition, box.size); }, onPanUpdate: (details) { final box = context.findRenderObject() as RenderBox; final localPosition = box.globalToLocal(details.globalPosition); _onPanUpdate(localPosition, box.size); }, onPanEnd: (_) => _resetJoystick(), onPanCancel: _resetJoystick, child: CustomPaint( painter: JoystickPainter( stickPosition: _stickPosition, active: _active, ), ), ); } } class JoystickPainter extends CustomPainter { final Offset stickPosition; final bool active; JoystickPainter({required this.stickPosition, required this.active}); @override void paint(Canvas canvas, Size size) { final center = size.center(Offset.zero); final radius = size.width / 2; const stickRadius = 20.0; canvas.drawCircle( center, radius, Paint() ..color = active ? Colors.white.withValues(alpha: 0.3) : Colors.white.withValues(alpha: 0.1) ..style = PaintingStyle.fill, ); canvas.drawCircle( center, radius, Paint() ..color = Colors.grey ..style = PaintingStyle.stroke ..strokeWidth = 2.0, ); canvas.drawCircle( center + stickPosition, stickRadius, Paint() ..color = active ? Colors.white : Colors.grey ..style = PaintingStyle.fill, ); } @override bool shouldRepaint(JoystickPainter oldDelegate) { return stickPosition != oldDelegate.stickPosition || active != oldDelegate.active; } } class SquareJoystick extends StatefulWidget { final ValueChanged onChanged; const SquareJoystick({ super.key, required this.onChanged, }); @override State createState() => _SquareJoystickState(); } class _SquareJoystickState extends State { Offset _stickPosition = Offset.zero; bool _active = false; void _onPanUpdate(Offset localPosition, Size size) { final center = size.center(Offset.zero); var dx = localPosition.dx - center.dx; var dy = localPosition.dy - center.dy; final maxDistance = size.width / 2; dx = dx.clamp(-maxDistance, maxDistance); dy = dy.clamp(-maxDistance, maxDistance); setState(() { _stickPosition = Offset(dx, dy); _active = true; }); final normalized = Offset( dx / maxDistance, dy / maxDistance, ); widget.onChanged(normalized); } void _resetJoystick() { setState(() { _stickPosition = Offset.zero; _active = false; }); widget.onChanged(Offset.zero); } @override Widget build(BuildContext context) { return GestureDetector( behavior: HitTestBehavior.opaque, onPanStart: (details) { final box = context.findRenderObject() as RenderBox; final localPosition = box.globalToLocal(details.globalPosition); _onPanUpdate(localPosition, box.size); }, onPanUpdate: (details) { final box = context.findRenderObject() as RenderBox; final localPosition = box.globalToLocal(details.globalPosition); _onPanUpdate(localPosition, box.size); }, onPanEnd: (_) => _resetJoystick(), onPanCancel: _resetJoystick, child: CustomPaint( painter: SquareJoystickPainter( stickPosition: _stickPosition, active: _active, ), ), ); } } class SquareJoystickPainter extends CustomPainter { final Offset stickPosition; final bool active; SquareJoystickPainter({required this.stickPosition, required this.active}); @override @override void paint(Canvas canvas, Size size) { final center = size.center(Offset.zero); const stickRadius = 20.0; final squareRect = Rect.fromCenter( center: center, width: size.width, height: size.height, ); final roundedRect = RRect.fromRectAndRadius( squareRect, Radius.circular(size.width / 8), ); canvas.drawRRect( roundedRect, Paint() ..color = active ? Colors.white.withValues(alpha: 0.3) : Colors.white.withValues(alpha: 0.1) ..style = PaintingStyle.fill, ); canvas.drawRRect( roundedRect, Paint() ..color = Colors.grey ..style = PaintingStyle.stroke ..strokeWidth = 2.0, ); canvas.drawCircle( center + stickPosition, stickRadius, Paint() ..color = active ? Colors.white : Colors.grey ..style = PaintingStyle.fill, ); } @override bool shouldRepaint(SquareJoystickPainter oldDelegate) { return stickPosition != oldDelegate.stickPosition || active != oldDelegate.active; } } class HorizontalJoystick extends StatefulWidget { final ValueChanged onChanged; const HorizontalJoystick({ super.key, required this.onChanged, }); @override State createState() => _HorizontalJoystickState(); } class _HorizontalJoystickState extends State { double _stickPosition = 0.0; bool _active = false; void _onPanUpdate(Offset localPosition, Size size) { final centerX = size.width / 2; final dx = (localPosition.dx - centerX).clamp(-centerX, centerX); setState(() { _stickPosition = dx; _active = true; }); final normalized = dx / centerX; widget.onChanged(normalized); } void _resetJoystick() { setState(() { _stickPosition = 0.0; _active = false; }); widget.onChanged(0.0); } @override Widget build(BuildContext context) { return GestureDetector( behavior: HitTestBehavior.opaque, onPanStart: (details) { final box = context.findRenderObject() as RenderBox; _onPanUpdate(box.globalToLocal(details.globalPosition), box.size); }, onPanUpdate: (details) { final box = context.findRenderObject() as RenderBox; _onPanUpdate(box.globalToLocal(details.globalPosition), box.size); }, onPanEnd: (_) => _resetJoystick(), onPanCancel: _resetJoystick, child: CustomPaint( painter: HorizontalJoystickPainter( stickPosition: _stickPosition, active: _active, ), ), ); } } class HorizontalJoystickPainter extends CustomPainter { final double stickPosition; final bool active; HorizontalJoystickPainter({required this.stickPosition, required this.active}); @override void paint(Canvas canvas, Size size) { final center = size.center(Offset.zero); const stickRadius = 15.0; final trackHeight = size.height / 2; canvas.drawRRect( RRect.fromRectAndRadius( Rect.fromCenter( center: center, width: size.width, height: trackHeight, ), Radius.circular(trackHeight / 2), ), Paint() ..color = active ? Colors.white.withValues(alpha: 0.3) : Colors.white.withValues(alpha: 0.1), ); canvas.drawRRect( RRect.fromRectAndRadius( Rect.fromCenter( center: center, width: size.width, height: trackHeight, ), Radius.circular(trackHeight / 2), ), Paint() ..color = Colors.grey ..style = PaintingStyle.stroke ..strokeWidth = 2.0, ); canvas.drawCircle( Offset(center.dx + stickPosition, center.dy), stickRadius * 1.333, Paint() ..color = active ? Colors.white : Colors.grey ..style = PaintingStyle.fill, ); } @override bool shouldRepaint(HorizontalJoystickPainter oldDelegate) { return stickPosition != oldDelegate.stickPosition || active != oldDelegate.active; } } class VerticalJoystick extends StatefulWidget { final ValueChanged onChanged; const VerticalJoystick({ super.key, required this.onChanged, }); @override State createState() => _VerticalJoystickState(); } class _VerticalJoystickState extends State { double _stickPosition = 0.0; bool _active = false; void _onPanUpdate(Offset localPosition, Size size) { final centerY = size.height / 2; final dy = (localPosition.dy - centerY).clamp(-centerY, centerY); setState(() { _stickPosition = dy; _active = true; }); final normalized = dy / centerY; widget.onChanged(normalized); } void _resetJoystick() { setState(() { _stickPosition = 0.0; _active = false; }); widget.onChanged(0.0); } @override Widget build(BuildContext context) { return GestureDetector( behavior: HitTestBehavior.opaque, onPanStart: (details) { final box = context.findRenderObject() as RenderBox; _onPanUpdate(box.globalToLocal(details.globalPosition), box.size); }, onPanUpdate: (details) { final box = context.findRenderObject() as RenderBox; _onPanUpdate(box.globalToLocal(details.globalPosition), box.size); }, onPanEnd: (_) => _resetJoystick(), onPanCancel: _resetJoystick, child: CustomPaint( painter: VerticalJoystickPainter( stickPosition: _stickPosition, active: _active, ), ), ); } } class VerticalJoystickPainter extends CustomPainter { final double stickPosition; final bool active; VerticalJoystickPainter({required this.stickPosition, required this.active}); @override void paint(Canvas canvas, Size size) { final center = size.center(Offset.zero); const stickRadius = 15.0; final trackWidth = size.width / 2; canvas.drawRRect( RRect.fromRectAndRadius( Rect.fromCenter( center: center, width: trackWidth, height: size.height, ), Radius.circular(trackWidth / 2), ), Paint() ..color = active ? Colors.white.withValues(alpha: 0.3) : Colors.white.withValues(alpha: 0.1), ); canvas.drawRRect( RRect.fromRectAndRadius( Rect.fromCenter( center: center, width: trackWidth, height: size.height, ), Radius.circular(trackWidth / 2), ), Paint() ..color = Colors.grey ..style = PaintingStyle.stroke ..strokeWidth = 2.0, ); canvas.drawCircle( Offset(center.dx, center.dy + stickPosition), stickRadius * 1.333, Paint() ..color = active ? Colors.white : Colors.grey ..style = PaintingStyle.fill, ); } @override bool shouldRepaint(VerticalJoystickPainter oldDelegate) { return stickPosition != oldDelegate.stickPosition || active != oldDelegate.active; } }