怀念经典,一步步开发一个ttplayer千千静听,迷你模式窗口
MiniWindow 源码完整深度剖析



一、概述
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按钮执行窗口最小化。- 其他标签(
exit、normal、minimode等)均调用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滑块和填充图片。- 它接受
thumbData、fillData和可选的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 自定义绘制
infoLabel 的 paintComponent 重写了绘制逻辑:
- 当
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 方法,在窗口显示前做以下操作:
- 根据皮肤图片的实际尺寸重新设置窗口大小(防止皮肤尺寸与预设不符)。
- 从引擎同步当前音量和静音状态到控件。
- 如果已有歌曲信息,则启动轮播显示。
- 隐藏时停止所有动画,避免后台资源消耗。
十、总结与亮点
MiniWindow 是一个设计精巧的 UI 组件,它完美地融合了皮肤系统、播放引擎和自定义交互,为用户提供了既美观又实用的迷你播放界面。对于想要学习 Swing 皮肤化开发或构建可停靠迷你窗口的开发者来说,这份源码具有很高的参考价值。
License
本项目仅供学习和个人使用。皮肤资源版权归原作者所有。
「获取方式」:后台回复 ttplayer


浙公网安备 33010602011771号