構建一個以 JSlider 控制 GIF 動畫播放幀率的 Swing 應用,展示滑桿模型配置、事件監聽、Timer 驅動動畫、視窗圖示化暫停/還原、圖片延遲載入與邊界處理,完整示範模型-視圖-控制器分離與執行緒安全設計。
public class SliderDemo extends JPanel implements ActionListener, WindowListener, ChangeListener {
// 滑桿參數常數
static final int FPS_MIN = 0, FPS_MAX = 30, FPS_INIT = 15;
int frameNumber = 0, NUM_FRAMES = 14;
ImageIcon[] images = new ImageIcon[NUM_FRAMES];
int delay; Timer timer; boolean frozen = false;
JLabel picture;
public SliderDemo() {
setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
delay = 1000 / FPS_INIT;
// UI 構建
JLabel sliderLabel = new JLabel("調整滑桿改變播放速度!", JLabel.CENTER);
sliderLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
JSlider fpsSlider = new JSlider(JSlider.HORIZONTAL, FPS_MIN, FPS_MAX, FPS_INIT);
fpsSlider.addChangeListener(this);
fpsSlider.setMajorTickSpacing(10);
fpsSlider.setMinorTickSpacing(1);
fpsSlider.setPaintTicks(true);
fpsSlider.setPaintLabels(true);
fpsSlider.setBorder(BorderFactory.createEmptyBorder(0,0,10,0));
picture = new JLabel();
picture.setHorizontalAlignment(JLabel.CENTER);
picture.setAlignmentX(Component.CENTER_ALIGNMENT);
picture.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLoweredBevelBorder(),
BorderFactory.createEmptyBorder(10,10,10,10)));
updatePicture(0);
add(sliderLabel); add(fpsSlider); add(picture);
setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
// Timer 驅動動畫
timer = new Timer(delay, this);
timer.setInitialDelay(delay * 7);
timer.setCoalesce(true);
}
// 滑桿事件處理
public void stateChanged(ChangeEvent e) {
JSlider source = (JSlider)e.getSource();
if (!source.getValueIsAdjusting()) { // 僅在釋放滑桿時處理
int fps = source.getValue();
if (fps == 0) { if (!frozen) stopAnimation(); }
else {
delay = 1000 / fps;
timer.setDelay(delay);
timer.setInitialDelay(delay * 10);
if (frozen) startAnimation();
}
}
}
// 動畫控制
public void startAnimation() { timer.start(); frozen = false; }
public void stopAnimation() { timer.stop(); frozen = true; }
// Timer 事件:切換幀
public void actionPerformed(ActionEvent e) {
frameNumber = (frameNumber + 1) % NUM_FRAMES;
updatePicture(frameNumber);
if (frameNumber == 0 || frameNumber == NUM_FRAMES/2 - 1) timer.restart();
}
// 延遲載入圖片
protected void updatePicture(int frameNum) {
if (images[frameNum] == null)
images[frameNum] = createImageIcon("images/doggy/T" + frameNum + ".gif");
picture.setIcon(images[frameNum] != null ? images[frameNum] : null);
if (images[frameNum] == null) picture.setText("Image #" + frameNum + " not found");
}
protected static ImageIcon createImageIcon(String path) {
java.net.URL imgURL = SliderDemo.class.getResource(path);
return imgURL != null ? new ImageIcon(imgURL) : null;
}
// 視窗事件:最小化暫停,還原繼續
public void windowIconified(WindowEvent e) { stopAnimation(); }
public void windowDeiconified(WindowEvent e) { startAnimation(); }
// ... 其他 WindowListener 方法留空 ...
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("Slider Demo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setContentPane(new SliderDemo());
frame.pack();
frame.setVisible(true);
});
}
}
| 設計點 | 實作方式 | 效益 |
|---|---|---|
| 滑桿模型配置 | setMajorTickSpacing(10), setPaintTicks(true), setPaintLabels(true) | 直觀視覺回饋 |
| 事件去抖動 | !source.getValueIsAdjusting() 僅釋放時處理 | 避免拖曳時頻繁調整 Timer 造成卡頓 |
| FPS=0 特殊處理 | 停止動畫而非設定無限延遲 | 語義清晰,節省資源 |
| 圖片延遲載入 | 首次顯示時才 createImageIcon() | 啟動快、記憶體友善 |
| 視窗狀態聯動 | WindowListener 最小化暫停、還原繼續 | 符合使用者預期,省電 |
| Timer 合併觸發 | setCoalesce(true) 合併延遲觸發 | 避免系統忙碌時堆疊事件 |
BasicSliderUI 繪製漸層軌道、動態刻度。