怀念经典,一步步开发一个ttplayer千千静听,迷你模式窗口

MiniWindow 源码完整深度剖析

image
image
image

一、概述

MiniWindow 是千千静听(TTPlayer)的迷你模式窗口,它继承自 SkinWindow,利用同一份皮肤资源(mini_window 窗口定义)构建出一个紧凑版的播放器界面。迷你窗口通常用于将播放器缩小到屏幕角落,仅保留最核心的控制功能(播放/暂停、上下曲、进度条、音量、歌词开关等),以节省屏幕空间。

与之前分析的 DesktopLyricWindow(桌面歌词)不同,MiniWindow 完全由皮肤 XML 定义,其布局、图片、字体、颜色均来自 .skn 文件中的 <mini_window> 节点。因此,它是典型的“皮肤化”组件,与 TtSkin 解析框架紧密结合。


二、与皮肤 XML 的协作

MiniWindow 构造时,需要传入一个已加载的 TtSkin 对象,并通过 findWindow(skin, "mini_window") 获取对应的 WindowDef。这个 WindowDef 包含了 mini_window 的所有子控件(Ctl)信息,包括它们的位置、图片名、字体、颜色等属性。

buildControls() 方法遍历 def.elements,根据 Ctl.tag 分别创建不同的 Swing 组件(按钮、进度条、标签、可视化面板等),并添加到窗口的内容面板上。


三、成员变量与核心功能

private JLabel infoLabel;                  // 显示歌曲信息(歌名/艺术家等)
private TtButton btnPrev, btnPlay, btnPause, btnNext, btnStop;
private TtButton btnLyric, btnMinimize, btnExit;
private TtButton btnMute;
private TtTrackBar progressBar;            // 进度条
private TtVolumeBar volumeBar;             // 音量条
private TtVisualizer visualizer;           // 频谱可视化
private Runnable onExitMiniMode;           // 退出迷你模式回调
private Runnable onToggleLyric;            // 切换歌词回调
private Runnable onMuteChanged;            // 静音状态变化回调
private java.util.function.IntConsumer onVolumeChanged; // 音量变化回调
private PlayerEngine playerEngine;         // 播放引擎引用

此外,还包含歌曲信息轮播相关的字段(currentTitle, currentArtist 等)以及动画控制字段(animationTimer, marqueeTimer, scrollOffset 等),用于实现标签内容的动态切换(垂直滚动)和长文本的跑马灯效果。


四、构造方法与初始化

public MiniWindow(TtSkin skin, PlayerEngine engine) {
    super(skin, findWindow(skin, "mini_window"), false);
    this.playerEngine = engine;
    setTitle(Messages.get("menu.miniMode"));
    setAlwaysOnTop(true); // 迷你窗口始终置顶
    // 启动进度更新定时器
    Timer updateTimer = new Timer(200, e -> updateProgress());
    updateTimer.start();
    // 添加窗口拖动支持
    MouseAdapter dragAdapter = ...;
    addMouseListener(dragAdapter);
    addMouseMotionListener(dragAdapter);
}
  • 构造函数传入皮肤和引擎,调用父类 SkinWindow 构造,传入 mini_window 的窗口定义。
  • 设置窗口标题(菜单文本),并强制置顶。
  • 启动一个 200ms 的定时器,用于从 PlayerEngine 获取播放进度并更新进度条。
  • 为窗口本身添加鼠标监听,实现拖动(因为迷你窗口通常没有标题栏,用户需要拖动窗口移动位置)。

五、buildControls() —— 控件构建核心

这是整个类最核心的方法,它根据 def.elements 中的每个 Ctl 对象,依据其 tag 创建对应的 UI 组件。

5.1 窗口控制按钮(关闭/最小化/切换模式)

case "close": case "exit": case "normal": case "normalmode":
case "default": case "defaultmode": case "minimode":
case "minimize":
    TtButton btnClose = createButton(c);
    if (btnClose != null) {
        if ("minimize".equals(c.tag)) {
            tip(btnClose, Messages.get("tooltip.minimize"));
            btnClose.addActionListener(e -> setState(Frame.ICONIFIED));
        } else {
            tip(btnClose, Messages.get("tooltip.restoreFull"));
            btnClose.addActionListener(e -> exitMiniMode());
        }
    }
    break;
  • 这些标签对应迷你窗口上的关闭、最小化、切换回完整模式等按钮。createButton() 方法会依据 Ctl.image 从皮肤中加载四帧 BMP 并构造 TtButton
  • minimize 按钮执行窗口最小化。
  • 其他标签(exitnormalminimode 等)均调用 exitMiniMode(),隐藏迷你窗口并触发回调,由外部(通常是主窗口)恢复完整模式。

5.2 播放控制按钮

case "prev":  btnPrev = createButton(c); ... 
case "play":  btnPlay = createButton(c); ...
case "pause": btnPause = createButton(c); ...
case "next":  btnNext = createButton(c); ...
case "stop":  btnStop = createButton(c); ...
  • 每个按钮绑定到 PlayerEngine 的对应方法(previous(), playPause(), next(), stop())。
  • 注意:播放和暂停是两个独立的按钮,它们位于同一位置,通过 setVisible() 切换显示(由 updatePlayState() 控制)。

5.3 歌词按钮与静音按钮

case "lyric": // 切换桌面歌词
    btnLyric = createButton(c);
    btnLyric.addActionListener(e -> { if (onToggleLyric != null) onToggleLyric.run(); });
    break;
case "mute": // 静音切换(带选中状态)
    btnMute = createButton(c);
    btnMute.setSelected(playerEngine != null && playerEngine.isMuted());
    btnMute.addActionListener(e -> { playerEngine.mute(); btnMute.setSelected(playerEngine.isMuted()); ... });
    break;
  • 歌词按钮触发外部回调,通常由 PlayerWindow 控制 DesktopLyricWindow 的显示/隐藏。
  • 静音按钮是自锁开关setSelected),点击后切换引擎静音状态并更新按钮显示。

5.4 进度条(progress

private void createProgressBar(TtSkin.Ctl ctl) {
    byte[] thumbData = skin.getBmp(ctl.thumbImage);
    byte[] fillData = (ctl.fillImage != null) ? skin.getBmp(ctl.fillImage) : null;
    byte[] fillData2 = null; // 某些皮肤有 fill_image2,但 TtTrackBar 暂不支持
    progressBar = new TtTrackBar(thumbData, fillData, fillData2);
    progressBar.setBounds(ctl.left, ctl.top, ctl.right - ctl.left, ctl.bottom - ctl.top);
    getContentPane().add(progressBar);
    progressBar.setTrackListener(seconds -> { if (playerEngine != null) playerEngine.seekTo(seconds); });
}
  • TtTrackBar 是一个自定义进度条,支持 thumb 滑块和填充图片。
  • 它接受 thumbDatafillData 和可选的 fillData2(可能用于缓冲区填充,但当前 TtTrackBar 未使用)。
  • 设置 TrackListener 以便用户拖动时跳转到指定时间。

5.5 音量条(volume

private void createVolumeBar(TtSkin.Ctl ctl) {
    byte[] fillData = skin.getBmp(ctl.fillImage);
    byte[] thumbData = skin.getBmp(ctl.thumbImage);
    volumeBar = new TtVolumeBar(fillData, thumbData);
    volumeBar.setBounds(...);
    volumeBar.setVolumeListener(percent -> { playerEngine.setGainFromPercent(percent); ... });
}
  • 使用之前分析过的 TtVolumeBar 组件,实现音量调节。
  • 当用户拖动音量条时,更新引擎音量并回调外部(主窗口)以同步。

5.6 频谱可视化(visual

private void createMiniVisualizer(TtSkin.Ctl ctl) {
    visualizer = new TtVisualizer();
    visualizer.setBounds(...);
    loadMiniVisualizerSkinStyle(); // 从皮肤 XML 中的 <Visual> 节点读取配色
    visualizer.addMouseListener(...); // 单击切换频谱模式
    getContentPane().add(visualizer);
}
  • 迷你窗口中也可以包含频谱可视化区域(通常在进度条上方或旁边)。
  • 通过 loadMiniVisualizerSkinStyle() 从皮肤包中的 Visual.xml(或 Skin.xml 中的 <Visual> 节点)加载自定义配色(SpectrumTopColor 等),使频谱颜色与皮肤搭配。
  • 单击可视化区域可循环切换频谱模式(由 TtVisualizer.cycleMode() 实现)。

5.7 信息标签(info / song / title

case "info": case "song": case "title":
    infoLabel = createInfo(c);
    break;
  • createInfo() 方法返回一个自定义 JLabel,它支持垂直滚动切换文本水平跑马灯滚动,用于显示歌曲信息(歌名、艺术家、专辑、格式、时长)的轮播。
  • 这个标签的字体、颜色、背景、对齐方式均从 Ctl 中读取,与皮肤风格一致。

六、歌曲信息轮播与动画机制

6.1 数据输入

public void updateSongInfo(String title, String artist, String album, String format, int durationSeconds) {
    // 保存各字段
    // 重置显示索引
    // 若窗口可见,立即开始轮播
}
  • 外部(如 PlayerWindow)在歌曲切换时调用此方法,传入歌曲元数据。
  • 迷你窗口会缓存这些信息,并在窗口可见时启动轮播。

6.2 轮播循环

rotateInfoDisplay() 方法根据 infoDisplayIndex 从 5 种信息(标题、艺术家、专辑、格式、时长)中取出对应文本,然后调用 startTextAnimation() 进行切换动画。

6.3 垂直滚动动画(startTextAnimation

  • 当前显示的文本(currentText)向上滑出,新文本(nextText)从底部滑入。
  • 使用 Timer 每 50ms 增加 scrollOffset,并在自定义 JLabel.paintComponent() 中绘制两行文本的不同位置。
  • 动画完成后,检查新文本宽度是否超出标签宽度,若超出则启动跑马灯(startMarquee),否则停顿 1.5 秒后进入下一轮。

6.4 水平跑马灯(startMarquee

  • 当文本宽度大于标签宽度时,采用从右到左的滚动效果。
  • marqueeOffset 不断增大,在 paintComponent 中绘制文本时,X 坐标 = 标签宽度 - marqueeOffset,实现向左滚动。
  • 滚完一轮(文本完全移出左侧)后停止,停顿后再进入下一轮轮播。

6.5 自定义绘制

infoLabelpaintComponent 重写了绘制逻辑:

  • isAnimating 为 true 时,绘制上下两行(当前和下一行)的垂直位移。
  • isMarquee 为 true 时,绘制水平滚动的文本,并处理循环补尾(如果文本移出后剩余空间,再绘制一份相同文本)。
  • 否则,调用 super.paintComponent() 正常绘制。

这种设计使得标签能在有限区域内展示丰富信息,避免截断。


七、与播放引擎的交互

7.1 进度更新

updateProgress() 由定时器每 200ms 调用:

  • 若播放中且进度条未被用户拖动,则从引擎获取当前播放位置和总时长,更新 progressBar
  • progressBar.setRange(dur) 设置总时长(单位秒),setValue(pos) 设置当前位置。

7.2 音量同步

  • setVolume(int percent) 由外部(主窗口)调用,用于同步音量条,保持主窗口和迷你窗口的音量显示一致。
  • setMuted(boolean m) 同步静音按钮状态。

7.3 播放状态同步

  • updatePlayState(boolean playing) 控制播放/暂停按钮的显隐。
  • 当播放状态变化时,外部会调用此方法切换按钮。

7.4 频谱数据喂入

  • updateVisualizer(byte[] pcmData) 由外部高频调用(例如每帧音频数据),将 PCM 数据传递给 visualizer.updateData(),实现频谱实时绘制。

八、退出迷你模式与回调

private void exitMiniMode() {
    setVisible(false);
    if (onExitMiniMode != null) onExitMiniMode.run();
}
  • 当用户点击“返回完整模式”按钮或关闭迷你窗口时,触发 onExitMiniMode 回调。
  • 通常由主窗口实现该回调,隐藏迷你窗口并重新显示主窗口。

其他回调:

  • onToggleLyric:切换桌面歌词。
  • onVolumeChanged:音量变化时通知主窗口更新滑块。
  • onMuteChanged:静音状态变化时通知主窗口更新按钮。

九、窗口显示时的特殊处理(setVisible

重写 setVisible 方法,在窗口显示前做以下操作:

  1. 根据皮肤图片的实际尺寸重新设置窗口大小(防止皮肤尺寸与预设不符)。
  2. 从引擎同步当前音量和静音状态到控件。
  3. 如果已有歌曲信息,则启动轮播显示。
  4. 隐藏时停止所有动画,避免后台资源消耗。

十、总结与亮点

MiniWindow 是一个设计精巧的 UI 组件,它完美地融合了皮肤系统、播放引擎和自定义交互,为用户提供了既美观又实用的迷你播放界面。对于想要学习 Swing 皮肤化开发或构建可停靠迷你窗口的开发者来说,这份源码具有很高的参考价值。

License

本项目仅供学习和个人使用。皮肤资源版权归原作者所有。

「获取方式」:后台回复 ttplayer

image

posted @ 2026-08-28 14:02  tomj2ee  阅读(5)  评论(0)    收藏  举报