Files
flutterApp/packages/cc_ui_kit/lib/src/cc_joystick.dart
2026-04-16 17:39:54 +08:00

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;
}