245 lines
6.7 KiB
Dart
245 lines
6.7 KiB
Dart
import 'dart:math';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
enum AxisHint { forwardBackward, leftRight }
|
|
|
|
class JoystickValue {
|
|
final int x; // -100 ~ 100
|
|
final int y; // -100 ~ 100
|
|
JoystickValue(this.x, this.y);
|
|
}
|
|
|
|
class CCJoystick extends StatefulWidget {
|
|
final AxisHint axisHint;
|
|
final Function(JoystickValue) onValueChanged;
|
|
final VoidCallback? onPress;
|
|
final Future<void> Function()? onPanEnd; // 🔥 修复:改为异步回调
|
|
final double radius;
|
|
|
|
const CCJoystick({
|
|
super.key,
|
|
required this.axisHint,
|
|
required this.onValueChanged,
|
|
this.onPress,
|
|
this.onPanEnd, // 🔥 新增
|
|
this.radius = 80,
|
|
});
|
|
|
|
@override
|
|
State<CCJoystick> createState() => _CustomJoystickState();
|
|
}
|
|
|
|
class _CustomJoystickState extends State<CCJoystick>
|
|
with SingleTickerProviderStateMixin {
|
|
Offset _offset = Offset.zero;
|
|
late AnimationController _floatController;
|
|
bool _isPanning = false; // 🔥 跟踪是否正在拖拽
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// 漂浮动画:对应 Compose 的 floatAnim
|
|
_floatController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 800),
|
|
)..repeat(reverse: true);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_floatController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
// 计算输出值并应用死区
|
|
void _updateValue() {
|
|
int x = 0;
|
|
int y = 0;
|
|
|
|
if (widget.axisHint == AxisHint.leftRight) {
|
|
x = ((_offset.dx / widget.radius) * 100).clamp(-100, 100).toInt();
|
|
} else {
|
|
y = ((-_offset.dy / widget.radius) * 100).clamp(-100, 100).toInt();
|
|
}
|
|
|
|
widget.onValueChanged(JoystickValue(_applyDeadZone(x), _applyDeadZone(y)));
|
|
}
|
|
|
|
int _applyDeadZone(int value, {int deadZone = 5}) {
|
|
return value.abs() < deadZone ? 0 : value;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Listener(
|
|
// 🔥 兜底:监听全局指针抬起事件,防止 onPanEnd 丢失
|
|
onPointerUp: (_) async {
|
|
if (_isPanning) {
|
|
debugPrint('🛡️ [兜底] 全局指针抬起,强制归零');
|
|
await _handlePanEnd();
|
|
}
|
|
},
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onPanStart: (_) {
|
|
_isPanning = true;
|
|
widget.onPress?.call();
|
|
},
|
|
onPanUpdate: (details) {
|
|
setState(() {
|
|
Offset newOffset = _offset + details.delta;
|
|
double distance = newOffset.distance;
|
|
// 限制在圆圈内
|
|
if (distance <= widget.radius) {
|
|
_offset = newOffset;
|
|
} else {
|
|
double angle = atan2(newOffset.dy, newOffset.dx);
|
|
_offset = Offset(
|
|
cos(angle) * widget.radius,
|
|
sin(angle) * widget.radius,
|
|
);
|
|
}
|
|
});
|
|
_updateValue();
|
|
},
|
|
onPanEnd: (_) async {
|
|
debugPrint('✋ [正常] onPanEnd 触发');
|
|
await _handlePanEnd();
|
|
},
|
|
onPanCancel: () async {
|
|
// 🔥 新增:处理手势取消(如来电中断)
|
|
debugPrint('⚠️ [异常] onPanCancel 触发');
|
|
await _handlePanEnd();
|
|
},
|
|
child: AnimatedBuilder(
|
|
animation: _floatController,
|
|
builder: (context, child) {
|
|
return CustomPaint(
|
|
size: Size(widget.radius * 2, widget.radius * 2),
|
|
painter: JoystickPainter(
|
|
offset: _offset,
|
|
radius: widget.radius,
|
|
axisHint: widget.axisHint,
|
|
floatValue: _floatController.value * 10 - 5, // -5f 到 5f
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 🔥 统一的松手处理逻辑
|
|
Future<void> _handlePanEnd() async {
|
|
if (!_isPanning) return;
|
|
_isPanning = false;
|
|
setState(() => _offset = Offset.zero); // 归位
|
|
// 🔥 关键修复:不调用 _updateValue(),避免触发 onValueChanged 导致重复发送
|
|
// _updateValue(); // ❌ 注释掉,防止发送错误的停止指令
|
|
await widget.onPanEnd?.call(); // 🔥 等待异步回调完成
|
|
}
|
|
}
|
|
|
|
class JoystickPainter extends CustomPainter {
|
|
final Offset offset;
|
|
final double radius;
|
|
final AxisHint axisHint;
|
|
final double floatValue;
|
|
|
|
JoystickPainter({
|
|
required this.offset,
|
|
required this.radius,
|
|
required this.axisHint,
|
|
required this.floatValue,
|
|
});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final center = Offset(size.width / 2, size.height / 2);
|
|
final paint = Paint();
|
|
|
|
// 1. 绘制底盘 (对应 Color(0x6600BFFF))
|
|
paint
|
|
..color = const Color(0x6600BFFF)
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 1;
|
|
canvas.drawCircle(center, radius + 8, paint);
|
|
|
|
// 2. 绘制箭头 (对应 drawArrow 逻辑)
|
|
paint
|
|
..color = Colors.cyan.withOpacity(0.5)
|
|
..strokeCap = StrokeCap.round
|
|
..strokeWidth = 2;
|
|
|
|
const arrowSize = 10.0;
|
|
if (axisHint == AxisHint.forwardBackward) {
|
|
_drawArrow(
|
|
canvas,
|
|
center + Offset(0, -radius - arrowSize + floatValue),
|
|
"UP",
|
|
arrowSize,
|
|
paint,
|
|
);
|
|
_drawArrow(
|
|
canvas,
|
|
center + Offset(0, radius + arrowSize - floatValue),
|
|
"DOWN",
|
|
arrowSize,
|
|
paint,
|
|
);
|
|
} else {
|
|
_drawArrow(
|
|
canvas,
|
|
center + Offset(-radius - arrowSize + floatValue, 0),
|
|
"LEFT",
|
|
arrowSize,
|
|
paint,
|
|
);
|
|
_drawArrow(
|
|
canvas,
|
|
center + Offset(radius + arrowSize - floatValue, 0),
|
|
"RIGHT",
|
|
arrowSize,
|
|
paint,
|
|
);
|
|
}
|
|
|
|
// 3. 绘制旋钮 (对应 Color(0x8000D4FF))
|
|
paint
|
|
..style = PaintingStyle.fill
|
|
..color = const Color(0x8000D4FF);
|
|
canvas.drawCircle(center + offset, 35, paint);
|
|
}
|
|
|
|
void _drawArrow(
|
|
Canvas canvas,
|
|
Offset pos,
|
|
String direction,
|
|
double size,
|
|
Paint paint,
|
|
) {
|
|
switch (direction) {
|
|
case "UP":
|
|
canvas.drawLine(pos + Offset(-size / 2, size / 2), pos, paint);
|
|
canvas.drawLine(pos + Offset(size / 2, size / 2), pos, paint);
|
|
break;
|
|
case "DOWN":
|
|
canvas.drawLine(pos + Offset(-size / 2, -size / 2), pos, paint);
|
|
canvas.drawLine(pos + Offset(size / 2, -size / 2), pos, paint);
|
|
break;
|
|
case "LEFT":
|
|
canvas.drawLine(pos + Offset(size / 2, -size / 2), pos, paint);
|
|
canvas.drawLine(pos + Offset(size / 2, size / 2), pos, paint);
|
|
break;
|
|
case "RIGHT":
|
|
canvas.drawLine(pos + Offset(-size / 2, -size / 2), pos, paint);
|
|
canvas.drawLine(pos + Offset(-size / 2, size / 2), pos, paint); // 修正坐标
|
|
break;
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant JoystickPainter oldDelegate) => true;
|
|
}
|