MarsCarController/lib/pages/square_joystick.dart
2025-07-03 09:17:39 +08:00

478 lines
12 KiB
Dart

import 'package:flutter/material.dart';
class Joystick2D extends StatefulWidget {
final ValueChanged<Offset> onChanged;
const Joystick2D({
super.key,
required this.onChanged,
});
@override
State<Joystick2D> createState() => _Joystick2DState();
}
class _Joystick2DState extends State<Joystick2D> {
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<Offset> onChanged;
const SquareJoystick({
super.key,
required this.onChanged,
});
@override
State<SquareJoystick> createState() => _SquareJoystickState();
}
class _SquareJoystickState extends State<SquareJoystick> {
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<double> onChanged;
const HorizontalJoystick({
super.key,
required this.onChanged,
});
@override
State<HorizontalJoystick> createState() => _HorizontalJoystickState();
}
class _HorizontalJoystickState extends State<HorizontalJoystick> {
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<double> onChanged;
const VerticalJoystick({
super.key,
required this.onChanged,
});
@override
State<VerticalJoystick> createState() => _VerticalJoystickState();
}
class _VerticalJoystickState extends State<VerticalJoystick> {
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;
}
}