package yaokong;
|
|
import ui.UIConfig;
|
|
import javax.swing.*;
|
import java.awt.*;
|
import java.awt.event.*;
|
import java.util.ArrayList;
|
import java.util.List;
|
|
public class RemoteControlDialog extends JDialog {
|
private final Color THEME_COLOR;
|
private boolean serialWarningShown;
|
private final List<ButtonInteraction> buttonInteractions = new ArrayList<>();
|
private ModernJoystickComponent moveJoystick; // 移动摇杆(控制前进后退)
|
private ModernJoystickComponent turnJoystick; // 转向摇杆(控制左右转向)
|
private JLabel speedLabel; // 首页的速度标签引用
|
private JLabel joystickValuesLabel; // 旧的顶部显示(保留以防兼容),但主要使用下面两个标签
|
// 分别显示两个摇杆的数值/状态,位于各自摇杆的正上方
|
private JLabel moveJoystickValueLabel;
|
private JLabel turnJoystickValueLabel;
|
private Timer speedUpdateTimer; // 速度更新定时器
|
|
public RemoteControlDialog(Component parent, Color themeColor, JLabel speedLabel) {
|
super(parent != null ? (JFrame) SwingUtilities.getWindowAncestor(parent) : null,
|
"遥控操作", false);
|
this.THEME_COLOR = themeColor;
|
this.speedLabel = speedLabel;
|
setModalityType(ModalityType.MODELESS);
|
setAlwaysOnTop(true);
|
Control03.resetSpeeds();
|
Control03.sendNeutralCommandIfDebugSerialOpen();
|
int dialogWidth = UIConfig.DIALOG_WIDTH; // 与首页宽度一致
|
int dialogHeight = (int) (UIConfig.DIALOG_HEIGHT / 3.0 * 0.7) + 50 + 40; // 增加40像素
|
initializeDialog(dialogWidth, dialogHeight);
|
initializeRemoteContent();
|
startSpeedUpdateTimer(); // 启动速度更新定时器
|
if (parent == null) {
|
setLocationRelativeTo(null); // 居中显示
|
}
|
}
|
|
private void startSpeedUpdateTimer() {
|
// 不再在首页显示“行进/转向”内容,隐藏并清空速度标签
|
if (speedLabel != null) {
|
speedLabel.setVisible(false);
|
speedLabel.setText("");
|
// 不创建速度更新定时器,避免在首页显示行进/转向信息
|
speedUpdateTimer = null;
|
}
|
}
|
|
private void initializeDialog(int width, int height) {
|
setSize(width, height);
|
setLocationRelativeTo(getOwner());
|
setResizable(false);
|
getContentPane().setBackground(new Color(26, 26, 46)); // 深色背景
|
}
|
|
private void initializeRemoteContent() {
|
JPanel contentPanel = new JPanel(new BorderLayout());
|
contentPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
|
contentPanel.setBackground(Color.WHITE);
|
|
// 在每个摇杆上方分别显示其状态/数值(分两列)
|
JPanel valuesPanel = new JPanel(new GridLayout(1, 2, 20, 0));
|
valuesPanel.setOpaque(false);
|
|
moveJoystickValueLabel = new JLabel("前后", SwingConstants.CENTER);
|
moveJoystickValueLabel.setFont(new Font("微软雅黑", Font.BOLD, 14));
|
moveJoystickValueLabel.setForeground(new Color(40, 40, 40));
|
moveJoystickValueLabel.setBorder(BorderFactory.createEmptyBorder(8, 4, 8, 4));
|
moveJoystickValueLabel.setOpaque(false);
|
|
turnJoystickValueLabel = new JLabel("左右", SwingConstants.CENTER);
|
turnJoystickValueLabel.setFont(new Font("微软雅黑", Font.BOLD, 14));
|
turnJoystickValueLabel.setForeground(new Color(40, 40, 40));
|
turnJoystickValueLabel.setBorder(BorderFactory.createEmptyBorder(8, 4, 8, 4));
|
turnJoystickValueLabel.setOpaque(false);
|
|
valuesPanel.add(moveJoystickValueLabel);
|
valuesPanel.add(turnJoystickValueLabel);
|
contentPanel.add(valuesPanel, BorderLayout.NORTH);
|
|
// 创建摇杆面板
|
JPanel joystickPanel = new JPanel(new GridLayout(1, 2, 20, 0));
|
joystickPanel.setOpaque(false);
|
|
// 移动摇杆(绿色主题)
|
moveJoystick = new ModernJoystickComponent("前进/后退",
|
new Color(46, 204, 113), true); // 绿色主题
|
moveJoystick.setJoystickListener(new JoystickListener() {
|
@Override
|
public void onJoystickMoved(double x, double y) {
|
// 只使用Y轴控制前进后退,向上(北)为正
|
// 计算并四舍五入到整数速度值,正为前进,负为后退
|
int forwardVal = (int) Math.round(-y * 100.0);
|
// 限制在 [-100, 100]
|
forwardVal = Math.max(-100, Math.min(100, forwardVal));
|
|
if (Math.abs(y) > 0.1) {
|
applyForwardSpeed(forwardVal);
|
} else {
|
// 前后
|
stopForward();
|
}
|
|
// 更新顶部显示(移动显示当前前进/后退速度,转向取当前转向速度作为参考)
|
int steeringVal = Control03.getCurrentSteeringSpeed();
|
updateJoystickValues(forwardVal, steeringVal);
|
}
|
});
|
// 转向摇杆(蓝色主题)
|
turnJoystick = new ModernJoystickComponent("左转/右转",
|
new Color(52, 152, 219), false); // 蓝色主题
|
turnJoystick.setJoystickListener(new JoystickListener() {
|
@Override
|
public void onJoystickMoved(double x, double y) {
|
// 只使用X轴控制左右转向,向右为正
|
// 计算并四舍五入到整数转向值,正为右转,负为左转
|
int steeringVal = (int) Math.round(x * 100.0);
|
steeringVal = Math.max(-100, Math.min(100, steeringVal));
|
|
if (Math.abs(x) > 0.1) {
|
applySteeringSpeed(steeringVal);
|
} else {
|
// 左右
|
stopSteering();
|
}
|
|
// 更新顶部显示(转向显示当前转向速度,移动显示当前前进速度)
|
int forwardVal = Control03.getCurrentForwardSpeed();
|
updateJoystickValues(forwardVal, steeringVal);
|
}
|
});
|
joystickPanel.add(moveJoystick);
|
joystickPanel.add(turnJoystick);
|
|
// 仅显示摇杆面板(已移除下方的矩形速度显示)
|
contentPanel.add(joystickPanel, BorderLayout.CENTER);
|
|
getContentPane().add(contentPanel);
|
}
|
|
private JPanel createStatusPanel(String label, String value, Color color) {
|
JPanel panel = new JPanel(new BorderLayout(0, 5));
|
panel.setOpaque(false);
|
|
JLabel titleLabel = new JLabel(label, SwingConstants.CENTER);
|
titleLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
|
titleLabel.setForeground(new Color(255, 255, 255, 180));
|
|
JLabel valueLabel = new JLabel(value, SwingConstants.CENTER);
|
valueLabel.setFont(new Font("微软雅黑", Font.BOLD, 24));
|
valueLabel.setForeground(color);
|
valueLabel.setOpaque(true);
|
valueLabel.setBackground(new Color(0, 0, 0, 30));
|
valueLabel.setBorder(BorderFactory.createCompoundBorder(
|
BorderFactory.createLineBorder(new Color(255, 255, 255, 30), 2),
|
BorderFactory.createEmptyBorder(10, 20, 10, 20)
|
));
|
valueLabel.setPreferredSize(new Dimension(120, 60));
|
|
panel.add(titleLabel, BorderLayout.NORTH);
|
panel.add(valueLabel, BorderLayout.CENTER);
|
|
return panel;
|
}
|
|
private void showSerialClosedWarning() {
|
if (serialWarningShown) {
|
return;
|
}
|
serialWarningShown = true;
|
JOptionPane.showMessageDialog(this, "请先打开系统调试串口", "提示", JOptionPane.WARNING_MESSAGE);
|
}
|
|
private void applyForwardSpeed(int speed) {
|
// 计算目标速度(范围-100到100)
|
int targetSpeed = Math.max(-100, Math.min(100, speed));
|
|
// 获取当前速度
|
int currentSpeed = Control03.getCurrentForwardSpeed();
|
|
// 如果差异较大,逐步调整
|
if (Math.abs(targetSpeed - currentSpeed) > 5) {
|
int delta = targetSpeed > currentSpeed ? 10 : -10;
|
boolean success = Control03.adjustForwardSpeed(delta);
|
if (!success) {
|
showSerialClosedWarning();
|
return;
|
}
|
serialWarningShown = false;
|
}
|
}
|
|
private void stopForward() {
|
if (Control03.getCurrentForwardSpeed() != 0) {
|
boolean success = Control03.approachForwardSpeedToZero(20);
|
if (!success) {
|
showSerialClosedWarning();
|
return;
|
}
|
serialWarningShown = false;
|
}
|
}
|
|
private void applySteeringSpeed(int speed) {
|
// 计算目标转向速度(范围-100到100)
|
int targetSpeed = Math.max(-100, Math.min(100, speed));
|
|
// 获取当前转向速度
|
int currentSpeed = Control03.getCurrentSteeringSpeed();
|
|
// 如果差异较大,逐步调整
|
if (Math.abs(targetSpeed - currentSpeed) > 5) {
|
int delta = targetSpeed > currentSpeed ? 15 : -15;
|
boolean success = Control03.adjustSteeringSpeed(delta);
|
if (!success) {
|
showSerialClosedWarning();
|
return;
|
}
|
serialWarningShown = false;
|
}
|
}
|
|
private void stopSteering() {
|
if (Control03.getCurrentSteeringSpeed() != 0) {
|
boolean success = Control03.approachSteeringSpeedToZero(25);
|
if (!success) {
|
showSerialClosedWarning();
|
return;
|
}
|
serialWarningShown = false;
|
}
|
}
|
|
// 更新顶部显示的摇杆数值(在 EDT 上调用),文字根据数值映射为方向描述
|
private void updateJoystickValues(int forwardVal, int steeringVal) {
|
// 计算移动/转向描述文本
|
// 移动方向:正为前进,负为后退,0 为前后
|
String moveText;
|
if (forwardVal > 0) {
|
moveText = String.format("前进 %d", forwardVal);
|
} else if (forwardVal < 0) {
|
moveText = String.format("后退 %d", Math.abs(forwardVal));
|
} else {
|
moveText = "前后";
|
}
|
|
// 转向方向:正为右转,负为左转,0 为左右
|
String steerText;
|
if (steeringVal > 0) {
|
steerText = String.format("右转 %d", steeringVal);
|
} else if (steeringVal < 0) {
|
steerText = String.format("左转 %d", Math.abs(steeringVal));
|
} else {
|
steerText = "左右";
|
}
|
|
// 分别设置两个摇杆上方的文字(同时兼容旧的顶部合并标签)
|
final String moveTextFinal = moveText;
|
final String steerTextFinal = steerText;
|
// 大部分回调来自 EDT(鼠标事件或 Swing Timer),但为安全使用 invokeLater
|
SwingUtilities.invokeLater(() -> {
|
if (moveJoystickValueLabel != null) moveJoystickValueLabel.setText(moveTextFinal);
|
if (turnJoystickValueLabel != null) turnJoystickValueLabel.setText(steerTextFinal);
|
// 兼容:同时更新旧的合并显示(如果存在)
|
if (joystickValuesLabel != null) joystickValuesLabel.setText(String.format("%s %s", moveTextFinal, steerTextFinal));
|
});
|
}
|
|
/**
|
* 显示对话框并仅显示移动摇杆(隐藏转向摇杆)。
|
* 这个方法用于兼容旧调用点,确保外部可以请求只显示移动控制部分。
|
*/
|
public void showOnlyMoveJoystick() {
|
if (moveJoystick != null) {
|
moveJoystick.setVisible(true);
|
}
|
if (turnJoystick != null) {
|
turnJoystick.setVisible(false);
|
}
|
// 重新布局并刷新显示
|
revalidate();
|
repaint();
|
// 不再显示首页速度标签(已移除“行进/转向”内容)
|
// 显示对话框
|
setVisible(true);
|
// 尝试让对话框获得焦点
|
toFront();
|
requestFocus();
|
}
|
|
/**
|
* 仅显示转向(左右)摇杆,隐藏移动摇杆。
|
*/
|
public void showOnlyTurnJoystick() {
|
if (moveJoystick != null) {
|
moveJoystick.setVisible(false);
|
}
|
if (turnJoystick != null) {
|
turnJoystick.setVisible(true);
|
}
|
revalidate();
|
repaint();
|
// 不再显示首页速度标签(已移除“行进/转向”内容)
|
setVisible(true);
|
toFront();
|
requestFocus();
|
}
|
|
/**
|
* 同时显示移动摇杆与转向摇杆(恢复默认视图)。
|
*/
|
public void showBothJoysticks() {
|
if (moveJoystick != null) {
|
moveJoystick.setVisible(true);
|
}
|
if (turnJoystick != null) {
|
turnJoystick.setVisible(true);
|
}
|
revalidate();
|
repaint();
|
// 不再显示首页速度标签(已移除“行进/转向”内容)
|
setVisible(true);
|
toFront();
|
requestFocus();
|
}
|
|
@Override
|
public void dispose() {
|
// 前后速度更新定时器
|
if (speedUpdateTimer != null) {
|
speedUpdateTimer.stop();
|
speedUpdateTimer = null;
|
}
|
// 隐藏速度标签
|
if (speedLabel != null) {
|
speedLabel.setVisible(false);
|
speedLabel.setText("");
|
}
|
for (ButtonInteraction interaction : buttonInteractions) {
|
interaction.shutdown();
|
}
|
if (moveJoystick != null) {
|
moveJoystick.dispose();
|
}
|
if (turnJoystick != null) {
|
turnJoystick.dispose();
|
}
|
super.dispose();
|
}
|
|
// 摇杆监听器接口
|
private interface JoystickListener {
|
void onJoystickMoved(double x, double y);
|
}
|
// 现代化摇杆组件(参考1.html风格)
|
private class ModernJoystickComponent extends JPanel {
|
private final String title;
|
private final Color themeColor;
|
private final boolean isVertical;
|
private JoystickListener listener;
|
private double joystickX = 0;
|
private double joystickY = 0;
|
// 视觉坐标:用于回中动画(不影响对外发送的逻辑数值)
|
private double visualX = 0;
|
private double visualY = 0;
|
private boolean isDragging = false;
|
// 缩小摇杆尺寸,使其在窗口中完整显示
|
private final int joystickSize = 140; // 从150缩小到100
|
private final int handleSize = 60; // 从60缩小到40
|
private final int maxMoveDistance;
|
private Timer returnToCenterTimer;
|
|
public ModernJoystickComponent(String title, Color themeColor, boolean isVertical) {
|
this.title = title;
|
this.themeColor = themeColor;
|
this.isVertical = isVertical;
|
this.maxMoveDistance = (joystickSize - handleSize) / 2 - 10;
|
|
setOpaque(false);
|
// 调整组件整体大小,使摇杆更紧凑
|
setPreferredSize(new Dimension(joystickSize + 40, joystickSize + 80));
|
|
// 初始化回中定时器
|
returnToCenterTimer = new Timer(20, new ActionListener() {
|
private static final double RETURN_SPEED = 0.08;
|
|
@Override
|
public void actionPerformed(ActionEvent e) {
|
// 视觉回中:仅改变 visualX/visualY,不影响对外的逻辑值 joystickX/joystickY
|
if (!isDragging && (Math.abs(visualX) > 0.001 || Math.abs(visualY) > 0.001)) {
|
// 平滑回中视觉坐标
|
visualX = applyReturnForce(visualX, RETURN_SPEED);
|
visualY = applyReturnForce(visualY, RETURN_SPEED);
|
|
// 不在视觉回中期间向外发送非零数值(逻辑值已在释放时置0)
|
repaint();
|
|
// 如果视觉坐标已经接近中心,停止动画并确保为精确 0
|
if (Math.abs(visualX) < 0.001 && Math.abs(visualY) < 0.001) {
|
visualX = 0;
|
visualY = 0;
|
returnToCenterTimer.stop();
|
}
|
}
|
}
|
});
|
returnToCenterTimer.setInitialDelay(0);
|
|
addMouseListener(new MouseAdapter() {
|
@Override
|
public void mousePressed(MouseEvent e) {
|
isDragging = true;
|
if (returnToCenterTimer.isRunning()) {
|
returnToCenterTimer.stop();
|
}
|
updateJoystickPosition(e.getX(), e.getY());
|
// 视觉坐标随逻辑坐标同步
|
visualX = joystickX;
|
visualY = joystickY;
|
repaint();
|
}
|
|
@Override
|
public void mouseReleased(MouseEvent e) {
|
isDragging = false;
|
// 立即把对外逻辑数值置为 0 并通知监听器,保证数值立刻为 0
|
joystickX = 0;
|
joystickY = 0;
|
if (listener != null) {
|
listener.onJoystickMoved(0, 0);
|
}
|
|
// 启动视觉回中动画(仅影响 visualX/visualY,不再发送监听器事件)
|
if (!returnToCenterTimer.isRunning()) {
|
returnToCenterTimer.start();
|
}
|
}
|
});
|
|
addMouseMotionListener(new MouseMotionAdapter() {
|
@Override
|
public void mouseDragged(MouseEvent e) {
|
if (isDragging) {
|
updateJoystickPosition(e.getX(), e.getY());
|
// 拖动时视觉坐标与逻辑坐标同步
|
visualX = joystickX;
|
visualY = joystickY;
|
repaint();
|
}
|
}
|
});
|
}
|
|
private double applyReturnForce(double value, double speed) {
|
if (value > 0) {
|
return Math.max(0, value - speed);
|
} else if (value < 0) {
|
return Math.min(0, value + speed);
|
}
|
return 0;
|
}
|
|
private void updateJoystickPosition(int mouseX, int mouseY) {
|
int centerX = joystickSize / 2 + 20;
|
// 将摇杆绘制中心在组件内向下平移 5 像素(整体上移 5px)
|
int centerY = joystickSize / 2 + 25;
|
|
int rawDx = mouseX - centerX;
|
int rawDy = mouseY - centerY;
|
|
// 计算在圆形范围内的距离(使用原始整数位移计算距离)
|
double distance = Math.sqrt((double)rawDx * rawDx + (double)rawDy * rawDy);
|
|
// 使用 double 进行限制与缩放,避免整型截断导致最大值达不到 1.0 的问题
|
double scaledDx = rawDx;
|
double scaledDy = rawDy;
|
if (distance > maxMoveDistance && distance > 0) {
|
double factor = maxMoveDistance / distance;
|
scaledDx = rawDx * factor;
|
scaledDy = rawDy * factor;
|
}
|
|
// 转换为-1到1的范围(使用 double,避免精度丢失)
|
joystickX = maxMoveDistance > 0 ? scaledDx / (double)maxMoveDistance : 0;
|
joystickY = maxMoveDistance > 0 ? scaledDy / (double)maxMoveDistance : 0;
|
|
// 根据摇杆方向调整
|
if (isVertical) {
|
joystickX = 0; // 竖向摇杆不处理X轴
|
} else {
|
joystickY = 0; // 横向摇杆不处理Y轴
|
}
|
|
if (listener != null) {
|
listener.onJoystickMoved(joystickX, joystickY);
|
}
|
}
|
|
public void setJoystickListener(JoystickListener listener) {
|
this.listener = listener;
|
}
|
|
|
protected void paintComponent(Graphics g) {
|
super.paintComponent(g);
|
Graphics2D g2d = (Graphics2D) g;
|
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
|
int centerX = joystickSize / 2 + 20;
|
// 与 updateJoystickPosition 保持一致:将绘制中心向下平移 5 像素(整体上移 5px)
|
int centerY = joystickSize / 2 + 25;
|
|
// 绘制标题
|
if (title != null && !title.isEmpty()) {
|
g2d.setColor(new Color(255, 255, 255, 180));
|
g2d.setFont(new Font("微软雅黑", Font.BOLD, 16));
|
FontMetrics fm = g2d.getFontMetrics();
|
int titleWidth = fm.stringWidth(title);
|
g2d.drawString(title, centerX - titleWidth / 2, 30);
|
}
|
|
// 绘制摇杆底座(渐变圆形)
|
RadialGradientPaint baseGradient = new RadialGradientPaint(
|
centerX, centerY, joystickSize / 2,
|
new float[]{0.0f, 1.0f},
|
new Color[]{
|
new Color(44, 62, 80), // 深蓝灰
|
new Color(26, 37, 47) // 更深的蓝灰
|
}
|
);
|
g2d.setPaint(baseGradient);
|
g2d.fillOval(centerX - joystickSize / 2, centerY - joystickSize / 2,
|
joystickSize, joystickSize);
|
|
// 绘制底座边框
|
g2d.setColor(new Color(255, 255, 255, 20));
|
g2d.setStroke(new BasicStroke(6));
|
g2d.drawOval(centerX - joystickSize / 2, centerY - joystickSize / 2,
|
joystickSize, joystickSize);
|
|
// 绘制内部阴影
|
g2d.setColor(new Color(0, 0, 0, 80));
|
for (int i = 1; i <= 15; i++) {
|
g2d.drawOval(centerX - joystickSize / 2 + i, centerY - joystickSize / 2 + i,
|
joystickSize - 2 * i, joystickSize - 2 * i);
|
}
|
|
// 绘制方向指示线
|
g2d.setColor(new Color(255, 255, 255, 80));
|
g2d.setStroke(new BasicStroke(2));
|
|
// 垂直指示线
|
g2d.drawLine(centerX, centerY - joystickSize / 4,
|
centerX, centerY + joystickSize / 4);
|
// 水平指示线
|
g2d.drawLine(centerX - joystickSize / 4, centerY,
|
centerX + joystickSize / 4, centerY);
|
|
// 绘制方向箭头
|
g2d.setColor(new Color(255, 255, 255, 100));
|
g2d.setFont(new Font("微软雅黑", Font.BOLD, 12));
|
|
if (isVertical) {
|
// 竖向箭头
|
g2d.drawString("↑", centerX - 4, centerY - joystickSize / 2 + 20);
|
g2d.drawString("↓", centerX - 4, centerY + joystickSize / 2 - 8);
|
} else {
|
// 横向箭头
|
g2d.drawString("←", centerX - joystickSize / 2 + 12, centerY + 4);
|
g2d.drawString("→", centerX + joystickSize / 2 - 16, centerY + 4);
|
}
|
|
// 计算摇杆手柄位置(使用视觉坐标 visualX/visualY,这样回中动画只影响视觉)
|
int handleX = centerX + (int)(visualX * maxMoveDistance);
|
int handleY = centerY + (int)(visualY * maxMoveDistance);
|
|
// 绘制手柄阴影
|
g2d.setColor(new Color(0, 0, 0, 60));
|
g2d.fillOval(handleX - handleSize / 2 + 3, handleY - handleSize / 2 + 3,
|
handleSize, handleSize);
|
|
// 绘制手柄(渐变圆形)
|
RadialGradientPaint handleGradient = new RadialGradientPaint(
|
handleX, handleY, handleSize / 2,
|
new float[]{0.0f, 0.8f, 1.0f},
|
new Color[]{
|
themeColor.brighter(),
|
themeColor,
|
themeColor.darker()
|
}
|
);
|
g2d.setPaint(handleGradient);
|
g2d.fillOval(handleX - handleSize / 2, handleY - handleSize / 2,
|
handleSize, handleSize);
|
|
// 绘制手柄高光
|
g2d.setColor(new Color(255, 255, 255, 100));
|
g2d.fillOval(handleX - handleSize / 4, handleY - handleSize / 4,
|
handleSize / 2, handleSize / 2);
|
|
// 绘制手柄中心点
|
g2d.setColor(Color.WHITE);
|
g2d.fillOval(handleX - 6, handleY - 6, 12, 12);
|
g2d.setColor(themeColor.darker());
|
g2d.drawOval(handleX - 6, handleY - 6, 12, 12);
|
|
|
}
|
|
public void dispose() {
|
// 清理资源
|
listener = null;
|
if (returnToCenterTimer != null) {
|
returnToCenterTimer.stop();
|
returnToCenterTimer = null;
|
}
|
}
|
}
|
|
// 保留原有的ButtonInteraction类,但不再使用
|
private final class ButtonInteraction extends MouseAdapter {
|
// ... 保持原有代码不变 ...
|
|
public void shutdown() {
|
// 保留占位实现,便于向后兼容
|
}
|
}
|
}
|