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; // 速度更新定时器
|
// 摇杆控制定时器:持续发送控制指令
|
private Timer forwardControlTimer; // 前进/后退控制定时器
|
private Timer steeringControlTimer; // 转向控制定时器
|
private int targetForwardSpeed = 0; // 目标前进/后退速度
|
private int targetSteeringSpeed = 0; // 目标转向速度
|
private List<JButton> bladeButtons = new ArrayList<>(); // 存储刀盘控制按钮,用于清理定时器
|
private String bladeUpDefaultText = "刀盘升"; // 刀盘升按钮默认文字
|
private String bladeDownDefaultText = "刀盘降"; // 刀盘降按钮默认文字
|
|
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) +150; // 增加90像素(原40+新增40+再增10)
|
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);
|
Container contentPane = getContentPane();
|
if (contentPane instanceof JComponent) {
|
JComponent jContentPane = (JComponent) contentPane;
|
jContentPane.setBackground(new Color(0, 0, 0, 0)); // 透明背景
|
jContentPane.setOpaque(false);
|
}
|
}
|
|
private void initializeRemoteContent() {
|
JPanel contentPanel = new JPanel(new BorderLayout());
|
contentPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
|
contentPanel.setOpaque(false); // 设置为透明
|
|
// 创建顶部面板,包含按钮和数值显示
|
JPanel topPanel = new JPanel(new BorderLayout());
|
topPanel.setOpaque(false);
|
|
// 创建按钮面板(4个按钮,间隔30像素)
|
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 30, 0));
|
buttonPanel.setOpaque(false);
|
buttonPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 8, 0)); // 底部间距8像素
|
|
// 按钮1:off.png <-> starton.png,文字:启动 <-> 熄火
|
JLabel label1 = new JLabel("启动", SwingConstants.CENTER);
|
label1.setFont(new Font("微软雅黑", Font.PLAIN, 12));
|
label1.setForeground(new Color(40, 40, 40));
|
JButton button1 = createIconButtonWithLabel("image/off.png", "image/starton.png", 27, 27,
|
label1, "启动", "熄火", "POWER");
|
JPanel panel1 = createButtonWithLabel(button1, label1);
|
buttonPanel.add(panel1);
|
|
// 按钮2:dengoff.png <-> dengon.png,文字:开灯 <-> 关灯
|
JLabel label2 = new JLabel("开灯", SwingConstants.CENTER);
|
label2.setFont(new Font("微软雅黑", Font.PLAIN, 12));
|
label2.setForeground(new Color(40, 40, 40));
|
JButton button2 = createIconButtonWithLabel("image/dengoff.png", "image/dengon.png", 30, 30,
|
label2, "开灯", "关灯", "LIGHT");
|
JPanel panel2 = createButtonWithLabel(button2, label2);
|
buttonPanel.add(panel2);
|
|
// 按钮3:xia1.png(固定图标),文字:刀盘升
|
JButton button3 = createBladeControlButton("image/xia1.png", "image/xia20.png", 30, 30, true);
|
JLabel label3 = new JLabel("刀盘升", SwingConstants.CENTER);
|
label3.setFont(new Font("微软雅黑", Font.PLAIN, 12));
|
label3.setForeground(new Color(40, 40, 40));
|
button3.putClientProperty("label", label3);
|
button3.putClientProperty("defaultText", bladeUpDefaultText);
|
bladeButtons.add(button3); // 添加到列表以便清理
|
JPanel panel3 = createButtonWithLabel(button3, label3);
|
buttonPanel.add(panel3);
|
|
// 按钮4:xia2.png(固定图标),文字:刀盘降
|
JButton button4 = createBladeControlButton("image/xia2.png", "image/xia10.png", 30, 30, false);
|
JLabel label4 = new JLabel("刀盘降", SwingConstants.CENTER);
|
label4.setFont(new Font("微软雅黑", Font.PLAIN, 12));
|
label4.setForeground(new Color(40, 40, 40));
|
button4.putClientProperty("label", label4);
|
button4.putClientProperty("defaultText", bladeDownDefaultText);
|
bladeButtons.add(button4); // 添加到列表以便清理
|
JPanel panel4 = createButtonWithLabel(button4, label4);
|
buttonPanel.add(panel4);
|
|
topPanel.add(buttonPanel, BorderLayout.NORTH);
|
|
// 在每个摇杆上方分别显示其状态/数值(分两列)
|
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);
|
topPanel.add(valuesPanel, BorderLayout.CENTER);
|
|
contentPanel.add(topPanel, 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));
|
|
// 更新目标速度
|
targetForwardSpeed = forwardVal;
|
|
if (Math.abs(y) > 0.1) {
|
// 摇杆不在中心位置,启动持续发送定时器
|
startForwardControlTimer();
|
} else {
|
// 摇杆回到中心位置,停止发送
|
stopForwardControlTimer();
|
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));
|
|
// 更新目标速度
|
targetSteeringSpeed = steeringVal;
|
|
if (Math.abs(x) > 0.1) {
|
// 摇杆不在中心位置,启动持续发送定时器
|
startSteeringControlTimer();
|
} else {
|
// 摇杆回到中心位置,停止发送
|
stopSteeringControlTimer();
|
stopSteering();
|
}
|
|
// 更新顶部显示(转向显示当前转向速度,移动显示当前前进速度)
|
int forwardVal = Control03.getCurrentForwardSpeed();
|
updateJoystickValues(forwardVal, steeringVal);
|
}
|
});
|
joystickPanel.add(moveJoystick);
|
joystickPanel.add(turnJoystick);
|
|
// 仅显示摇杆面板(已移除下方的矩形速度显示)
|
contentPanel.add(joystickPanel, BorderLayout.CENTER);
|
|
getContentPane().add(contentPanel);
|
}
|
|
/**
|
* 创建带标签的按钮面板(垂直布局:按钮在上,标签在下)
|
* @param button 按钮
|
* @param label 标签
|
* @return 包含按钮和标签的面板
|
*/
|
private JPanel createButtonWithLabel(JButton button, JLabel label) {
|
JPanel panel = new JPanel(new BorderLayout());
|
panel.setOpaque(false);
|
panel.add(button, BorderLayout.CENTER);
|
panel.add(label, BorderLayout.SOUTH);
|
panel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
|
return panel;
|
}
|
|
/**
|
* 创建可切换图标的按钮(带标签文字切换)
|
* @param defaultIconPath 默认图标路径
|
* @param clickedIconPath 点击后的图标路径
|
* @param width 图标宽度
|
* @param height 图标高度
|
* @param label 标签组件
|
* @param defaultText 默认文字
|
* @param clickedText 点击后的文字
|
* @param controlType 控制指令类型:"POWER"表示启动/熄火,"LIGHT"表示开灯/关灯,null表示无控制指令
|
* @return 配置好的按钮
|
*/
|
private JButton createIconButtonWithLabel(String defaultIconPath, String clickedIconPath,
|
int width, int height, JLabel label,
|
String defaultText, String clickedText, String controlType) {
|
JButton button = new JButton();
|
button.setPreferredSize(new Dimension(width, height));
|
button.setMinimumSize(new Dimension(width, height));
|
button.setMaximumSize(new Dimension(width, height));
|
button.setContentAreaFilled(false);
|
button.setBorderPainted(false);
|
button.setFocusPainted(false);
|
|
// 加载默认图标和点击图标
|
ImageIcon defaultIcon = loadIcon(defaultIconPath, width, height);
|
ImageIcon clickedIcon = loadIcon(clickedIconPath, width, height);
|
|
if (defaultIcon != null) {
|
button.setIcon(defaultIcon);
|
}
|
|
// 使用 clientProperty 来跟踪按钮状态(false=默认图标,true=点击图标)
|
button.putClientProperty("isClicked", false);
|
button.putClientProperty("defaultIcon", defaultIcon);
|
button.putClientProperty("clickedIcon", clickedIcon);
|
button.putClientProperty("label", label);
|
button.putClientProperty("defaultText", defaultText);
|
button.putClientProperty("clickedText", clickedText);
|
button.putClientProperty("controlType", controlType);
|
|
// 添加点击事件,切换图标和文字,并发送控制指令
|
button.addActionListener(e -> {
|
Boolean isClicked = (Boolean) button.getClientProperty("isClicked");
|
ImageIcon defaultIconRef = (ImageIcon) button.getClientProperty("defaultIcon");
|
ImageIcon clickedIconRef = (ImageIcon) button.getClientProperty("clickedIcon");
|
JLabel labelRef = (JLabel) button.getClientProperty("label");
|
String defaultTextRef = (String) button.getClientProperty("defaultText");
|
String clickedTextRef = (String) button.getClientProperty("clickedText");
|
String controlTypeRef = (String) button.getClientProperty("controlType");
|
|
if (isClicked == null || !isClicked) {
|
// 当前是默认图标,切换到点击图标
|
if (clickedIconRef != null) {
|
button.setIcon(clickedIconRef);
|
button.putClientProperty("isClicked", true);
|
}
|
// 更新标签文字
|
if (labelRef != null && clickedTextRef != null) {
|
labelRef.setText(clickedTextRef);
|
}
|
// 发送控制指令(开启)
|
if ("POWER".equals(controlTypeRef)) {
|
boolean success = Control05.sendPowerOnIfDebugSerialOpen();
|
if (!success) {
|
showSerialClosedWarning();
|
}
|
} else if ("LIGHT".equals(controlTypeRef)) {
|
boolean success = Control07.sendLightOnIfDebugSerialOpen();
|
if (!success) {
|
showSerialClosedWarning();
|
}
|
}
|
} else {
|
// 当前是点击图标,切换回默认图标
|
if (defaultIconRef != null) {
|
button.setIcon(defaultIconRef);
|
button.putClientProperty("isClicked", false);
|
}
|
// 更新标签文字
|
if (labelRef != null && defaultTextRef != null) {
|
labelRef.setText(defaultTextRef);
|
}
|
// 发送控制指令(关闭)
|
if ("POWER".equals(controlTypeRef)) {
|
boolean success = Control05.sendPowerOffIfDebugSerialOpen();
|
if (!success) {
|
showSerialClosedWarning();
|
}
|
} else if ("LIGHT".equals(controlTypeRef)) {
|
boolean success = Control07.sendLightOffIfDebugSerialOpen();
|
if (!success) {
|
showSerialClosedWarning();
|
}
|
}
|
}
|
});
|
|
return button;
|
}
|
|
/**
|
* 创建可切换图标的按钮
|
* @param defaultIconPath 默认图标路径
|
* @param clickedIconPath 点击后的图标路径
|
* @param width 图标宽度
|
* @param height 图标高度
|
* @return 配置好的按钮
|
*/
|
private JButton createIconButton(String defaultIconPath, String clickedIconPath, int width, int height) {
|
JButton button = new JButton();
|
button.setPreferredSize(new Dimension(width, height));
|
button.setMinimumSize(new Dimension(width, height));
|
button.setMaximumSize(new Dimension(width, height));
|
button.setContentAreaFilled(false);
|
button.setBorderPainted(false);
|
button.setFocusPainted(false);
|
|
// 加载默认图标和点击图标
|
ImageIcon defaultIcon = loadIcon(defaultIconPath, width, height);
|
ImageIcon clickedIcon = loadIcon(clickedIconPath, width, height);
|
|
if (defaultIcon != null) {
|
button.setIcon(defaultIcon);
|
}
|
|
// 使用 clientProperty 来跟踪按钮状态(false=默认图标,true=点击图标)
|
button.putClientProperty("isClicked", false);
|
button.putClientProperty("defaultIcon", defaultIcon);
|
button.putClientProperty("clickedIcon", clickedIcon);
|
|
// 添加点击事件,切换图标
|
button.addActionListener(e -> {
|
Boolean isClicked = (Boolean) button.getClientProperty("isClicked");
|
ImageIcon defaultIconRef = (ImageIcon) button.getClientProperty("defaultIcon");
|
ImageIcon clickedIconRef = (ImageIcon) button.getClientProperty("clickedIcon");
|
|
if (isClicked == null || !isClicked) {
|
// 当前是默认图标,切换到点击图标
|
if (clickedIconRef != null) {
|
button.setIcon(clickedIconRef);
|
button.putClientProperty("isClicked", true);
|
}
|
} else {
|
// 当前是点击图标,切换回默认图标
|
if (defaultIconRef != null) {
|
button.setIcon(defaultIconRef);
|
button.putClientProperty("isClicked", false);
|
}
|
}
|
});
|
|
return button;
|
}
|
|
/**
|
* 创建固定图标的按钮
|
* @param iconPath 图标路径
|
* @param width 图标宽度
|
* @param height 图标高度
|
* @return 配置好的按钮
|
*/
|
private JButton createFixedIconButton(String iconPath, int width, int height) {
|
JButton button = new JButton();
|
button.setPreferredSize(new Dimension(width, height));
|
button.setMinimumSize(new Dimension(width, height));
|
button.setMaximumSize(new Dimension(width, height));
|
button.setContentAreaFilled(false);
|
button.setBorderPainted(false);
|
button.setFocusPainted(false);
|
|
ImageIcon icon = loadIcon(iconPath, width, height);
|
if (icon != null) {
|
button.setIcon(icon);
|
}
|
|
return button;
|
}
|
|
/**
|
* 创建刀盘升降控制按钮(支持点击和长按)
|
* @param defaultIconPath 默认图标路径
|
* @param clickedIconPath 点击/长按时的图标路径
|
* @param width 图标宽度
|
* @param height 图标高度
|
* @param isUp true表示刀盘升,false表示刀盘降
|
* @return 配置好的按钮
|
*/
|
private JButton createBladeControlButton(String defaultIconPath, String clickedIconPath,
|
int width, int height, boolean isUp) {
|
JButton button = new JButton();
|
button.setPreferredSize(new Dimension(width, height));
|
button.setMinimumSize(new Dimension(width, height));
|
button.setMaximumSize(new Dimension(width, height));
|
button.setContentAreaFilled(false);
|
button.setBorderPainted(false);
|
button.setFocusPainted(false);
|
|
// 加载图标
|
ImageIcon defaultIcon = loadIcon(defaultIconPath, width, height);
|
ImageIcon clickedIcon = loadIcon(clickedIconPath, width, height);
|
|
if (defaultIcon != null) {
|
button.setIcon(defaultIcon);
|
}
|
|
button.putClientProperty("defaultIcon", defaultIcon);
|
button.putClientProperty("clickedIcon", clickedIcon);
|
button.putClientProperty("isUp", isUp);
|
button.putClientProperty("isPressed", false);
|
|
// 长按定时器
|
Timer longPressTimer = new Timer(100, e -> { // 每100ms执行一次
|
if (button.getClientProperty("isPressed") == Boolean.TRUE) {
|
boolean isUpButton = (Boolean) button.getClientProperty("isUp");
|
int currentHeight = Control06.getCurrentBladeHeight();
|
|
// 使用Control06的方法发送指令
|
boolean success;
|
if (isUpButton) {
|
success = Control06.sendBladeUpIfDebugSerialOpen(1);
|
} else {
|
success = Control06.sendBladeDownIfDebugSerialOpen(1);
|
}
|
|
if (!success) {
|
showSerialClosedWarning();
|
} else {
|
// 更新按钮标签显示当前数值
|
updateBladeButtonLabel(button);
|
}
|
}
|
});
|
longPressTimer.setInitialDelay(500); // 长按500ms后开始连续变化
|
button.putClientProperty("longPressTimer", longPressTimer); // 存储定时器引用
|
|
// 鼠标按下事件
|
button.addMouseListener(new MouseAdapter() {
|
@Override
|
public void mousePressed(MouseEvent e) {
|
button.putClientProperty("isPressed", true);
|
|
// 立即切换图标
|
ImageIcon clickedIconRef = (ImageIcon) button.getClientProperty("clickedIcon");
|
if (clickedIconRef != null) {
|
button.setIcon(clickedIconRef);
|
}
|
|
// 点击时立即增加/减少10,使用Control06的方法发送指令
|
boolean isUpButton = (Boolean) button.getClientProperty("isUp");
|
boolean success;
|
if (isUpButton) {
|
success = Control06.sendBladeUpIfDebugSerialOpen(10);
|
} else {
|
success = Control06.sendBladeDownIfDebugSerialOpen(10);
|
}
|
|
if (!success) {
|
showSerialClosedWarning();
|
} else {
|
// 更新按钮标签显示当前数值
|
updateBladeButtonLabel(button);
|
}
|
|
// 启动长按定时器
|
longPressTimer.start();
|
}
|
|
@Override
|
public void mouseReleased(MouseEvent e) {
|
button.putClientProperty("isPressed", false);
|
|
// 停止长按定时器
|
longPressTimer.stop();
|
|
// 恢复默认图标
|
ImageIcon defaultIconRef = (ImageIcon) button.getClientProperty("defaultIcon");
|
if (defaultIconRef != null) {
|
button.setIcon(defaultIconRef);
|
}
|
|
// 恢复默认文字
|
JLabel labelRef = (JLabel) button.getClientProperty("label");
|
String defaultTextRef = (String) button.getClientProperty("defaultText");
|
if (labelRef != null && defaultTextRef != null) {
|
labelRef.setText(defaultTextRef);
|
}
|
}
|
});
|
|
return button;
|
}
|
|
/**
|
* 更新刀盘按钮标签显示当前数值
|
* @param button 按钮
|
*/
|
private void updateBladeButtonLabel(JButton button) {
|
JLabel labelRef = (JLabel) button.getClientProperty("label");
|
if (labelRef != null) {
|
String defaultTextRef = (String) button.getClientProperty("defaultText");
|
int currentHeight = Control06.getCurrentBladeHeight();
|
String displayText = defaultTextRef + " " + currentHeight;
|
labelRef.setText(displayText);
|
}
|
}
|
|
/**
|
* 加载并缩放图标
|
* @param iconPath 图标路径
|
* @param width 目标宽度
|
* @param height 目标高度
|
* @return 缩放后的图标
|
*/
|
private ImageIcon loadIcon(String iconPath, int width, int height) {
|
try {
|
java.net.URL imgURL = getClass().getClassLoader().getResource(iconPath);
|
if (imgURL == null) {
|
// 尝试从文件系统加载
|
java.io.File imgFile = new java.io.File(iconPath);
|
if (imgFile.exists()) {
|
ImageIcon originalIcon = new ImageIcon(imgFile.getAbsolutePath());
|
Image scaledImage = originalIcon.getImage().getScaledInstance(width, height, Image.SCALE_SMOOTH);
|
ImageIcon scaledIcon = new ImageIcon(scaledImage);
|
scaledIcon.setDescription(iconPath);
|
return scaledIcon;
|
}
|
} else {
|
ImageIcon originalIcon = new ImageIcon(imgURL);
|
Image scaledImage = originalIcon.getImage().getScaledInstance(width, height, Image.SCALE_SMOOTH);
|
ImageIcon scaledIcon = new ImageIcon(scaledImage);
|
scaledIcon.setDescription(iconPath);
|
return scaledIcon;
|
}
|
} catch (Exception e) {
|
System.err.println("无法加载图标: " + iconPath + " - " + e.getMessage());
|
}
|
return null;
|
}
|
|
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;
|
}
|
}
|
|
/**
|
* 启动前进/后退控制定时器,持续发送控制指令
|
*/
|
private void startForwardControlTimer() {
|
// 如果定时器已经在运行,先停止它
|
if (forwardControlTimer != null && forwardControlTimer.isRunning()) {
|
forwardControlTimer.stop();
|
}
|
|
// 创建新的定时器,每100ms发送一次指令
|
forwardControlTimer = new Timer(100, new ActionListener() {
|
@Override
|
public void actionPerformed(ActionEvent e) {
|
// 持续发送目标速度的指令
|
applyForwardSpeedContinuously(targetForwardSpeed);
|
}
|
});
|
forwardControlTimer.setInitialDelay(0);
|
forwardControlTimer.start();
|
}
|
|
/**
|
* 停止前进/后退控制定时器
|
*/
|
private void stopForwardControlTimer() {
|
if (forwardControlTimer != null && forwardControlTimer.isRunning()) {
|
forwardControlTimer.stop();
|
}
|
}
|
|
/**
|
* 启动转向控制定时器,持续发送控制指令
|
*/
|
private void startSteeringControlTimer() {
|
// 如果定时器已经在运行,先停止它
|
if (steeringControlTimer != null && steeringControlTimer.isRunning()) {
|
steeringControlTimer.stop();
|
}
|
|
// 创建新的定时器,每100ms发送一次指令
|
steeringControlTimer = new Timer(100, new ActionListener() {
|
@Override
|
public void actionPerformed(ActionEvent e) {
|
// 持续发送目标速度的指令
|
applySteeringSpeedContinuously(targetSteeringSpeed);
|
}
|
});
|
steeringControlTimer.setInitialDelay(0);
|
steeringControlTimer.start();
|
}
|
|
/**
|
* 停止转向控制定时器
|
*/
|
private void stopSteeringControlTimer() {
|
if (steeringControlTimer != null && steeringControlTimer.isRunning()) {
|
steeringControlTimer.stop();
|
}
|
}
|
|
/**
|
* 持续发送前进/后退速度指令
|
*/
|
private void applyForwardSpeedContinuously(int targetSpeed) {
|
int currentSpeed = Control03.getCurrentForwardSpeed();
|
int currentSteeringSpeed = Control03.getCurrentSteeringSpeed();
|
|
// 如果已经达到目标速度,直接发送一次以保持状态
|
if (currentSpeed == targetSpeed) {
|
// 直接发送目标速度指令以保持状态(即使速度相同也要发送)
|
Control03.setAndSendSpeeds(currentSteeringSpeed, targetSpeed);
|
} else {
|
// 逐步调整到目标速度
|
int delta = targetSpeed > currentSpeed ? 10 : -10;
|
Control03.adjustForwardSpeed(delta);
|
}
|
}
|
|
/**
|
* 持续发送转向速度指令
|
*/
|
private void applySteeringSpeedContinuously(int targetSpeed) {
|
int currentSpeed = Control03.getCurrentSteeringSpeed();
|
int currentForwardSpeed = Control03.getCurrentForwardSpeed();
|
|
// 如果已经达到目标速度,直接发送一次以保持状态
|
if (currentSpeed == targetSpeed) {
|
// 直接发送目标速度指令以保持状态(即使速度相同也要发送)
|
Control03.setAndSendSpeeds(targetSpeed, currentForwardSpeed);
|
} else {
|
// 逐步调整到目标速度
|
int delta = targetSpeed > currentSpeed ? 15 : -15;
|
Control03.adjustSteeringSpeed(delta);
|
}
|
}
|
|
// 更新顶部显示的摇杆数值(在 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() {
|
// 停止并清理控制定时器
|
stopForwardControlTimer();
|
stopSteeringControlTimer();
|
if (forwardControlTimer != null) {
|
forwardControlTimer = null;
|
}
|
if (steeringControlTimer != null) {
|
steeringControlTimer = null;
|
}
|
// 前后速度更新定时器
|
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();
|
}
|
// 停止并清理刀盘按钮的定时器
|
for (JButton bladeButton : bladeButtons) {
|
Timer timer = (Timer) bladeButton.getClientProperty("longPressTimer");
|
if (timer != null && timer.isRunning()) {
|
timer.stop();
|
}
|
}
|
bladeButtons.clear();
|
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() {
|
// 保留占位实现,便于向后兼容
|
}
|
}
|
}
|