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; // 速度更新定时器 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() { // 保留占位实现,便于向后兼容 } } }