478 lines
12 KiB
Dart
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;
|
|
}
|
|
} |