张世豪
2025-12-11 6f59464fc6e12a525ba3004864eceb1bc1573a31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
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() {
            // 保留占位实现,便于向后兼容
        }
    }
}