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 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 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() { // 保留占位实现,便于向后兼容 } } }