怀念经典,一步步开发一个ttplayer千千静听,播放列表

PlaylistWindow 播放列表窗口源码完整分解
PlaylistWindow 是 TTPlayer 中负责管理播放列表的核心窗口,继承自 SkinWindow,因此拥有皮肤背景、九宫格缩放、自定义边框等能力。它包含左侧的播放列表切换列表和右侧的歌曲列表,支持添加、删除、排序、查找、播放模式切换、拖放歌曲、滚动条皮肤化等丰富的交互功能,并与 PlaylistManager、PlayerEngine、歌词窗口、均衡器窗口等组件协同工作。
下面从多个维度对其源码进行详细分解。
1. 类结构与继承关系
public class PlaylistWindow extends SkinWindow
- 继承
SkinWindow,获得皮肤窗口的基础能力(背景绘制、控件定位、边缘拖拽缩放)。 - 包名:
org.ttplayer.ui。 - 实现了一个包含左右双列表(播放列表列表 + 歌曲列表)和工具栏的复杂 UI。
2. 成员变量
2.1 UI 组件
public TtButton btnClose;– 关闭按钮。public TtToolbar toolbar;– 工具栏(包含 7 个按钮,分别对应“添加”、“删除”、“排序”、“查找”、“模式”、“选项”、“主菜单”)。public JPopupMenu addMenu, delMenu, sortMenu, findMenu, modeMenu, optMenu, mainMenu;– 工具栏各按钮对应的弹出菜单。public JPopupMenu playlistMenu;– 左侧播放列表的右键菜单。private JList<String> listLeft;– 左侧播放列表名称列表(使用自定义透明JList)。private org.ttplayer.controls.VirtualList listRight;– 右侧歌曲列表(使用自定义高性能虚拟列表)。private JScrollPane scrollLeft, scrollRight;– 左右列表的滚动面板(自定义透明滚动面板,应用皮肤滚动条)。private TtSkin.Ctl playlistCtl;– 皮肤 XML 中<playlist>控件的定义。private TtToolbar toolbarCtl;– 工具栏控件的皮肤定义。private TtSkin.Ctl scrollbarCtl;– 滚动条皮肤定义。private int toolbarLeft;– 工具栏的左边距(用于布局计算)。
2.2 分割条相关
private JPanel divider;– 左右列表之间的分割条。private double splitRatio = 0.35;– 左侧列表宽度占比(默认 35%)。private int startScreenX;– 拖动分割条时鼠标按下时的屏幕 X 坐标。private int startLeftWidth;– 拖动开始时左侧列表宽度。- 静态常量:
SPLIT_W = 5(分割条宽度),MIN_LEFT = 70,MIN_RIGHT = 70(左右最小宽度)。
2.3 颜色配置
从皮肤 XML 的 <PlayList> 节点读取:
colorText– 默认文字颜色。colorHilight– 高亮颜色。colorSelect– 选中背景色。colorBkgnd– 背景色。colorNumber– 序号颜色。colorDuration– 时长颜色。colorBkgnd2– 交替行背景色。
2.4 业务对象
public PlaylistManager playlistManager;– 播放列表管理器。private PlayerEngine playerEngine;– 播放引擎。private LyricWindow lyricWindow;– 歌词窗口引用。private EqualizerWindow equalizerWindow;– 均衡器窗口引用。private MiniWindow miniWindow;– 迷你窗口引用。private DesktopLyricWindow desktopLyricWindow;– 桌面歌词窗口引用。
2.5 监听器接口
SkinChangeListener– 换肤回调。MiniModeListener– 迷你模式切换回调。LanguageChangeListener– 语言切换回调。
2.6 状态变量
private JCheckBoxMenuItem[] modeItems;– 模式菜单的选项(顺序、循环、单曲、随机、播放后暂停)。private boolean pauseAfterPlay;– 是否启用“播放后暂停”。private int lastFindStart;– 上次查找结果的索引。private final List<Integer> foundIndices;– 查找结果索引列表。private static final String[] AUDIO_EXTS– 支持的音频扩展名列表。
3. 构造与初始化
public PlaylistWindow(TtSkin skin) {
super(skin, findWindow(skin), false);
setTitle(Messages.get("playlist.title"));
TtSkin.WindowDef def = findWindow(skin);
if (def != null && def.width > 0 && def.height > 0) {
setMinSize(def.width, def.height);
} else {
setMinSize(268, 165);
}
}
- 通过
findWindow静态方法获取playlist_window的WindowDef。 - 设置窗口标题(国际化)。
- 设置最小尺寸为皮肤定义尺寸或默认 268×165。
findWindow
static TtSkin.WindowDef findWindow(TtSkin skin) {
return LyricWindow.findWindow(skin, "playlist_window");
}
- 委托给
LyricWindow.findWindow,遍历skin.getWindows()找到名称匹配的窗口定义。
4. 皮肤与样式加载
4.1 buildControls() —— 核心 UI 构建
在 buildControls() 中:
- 若
playlistManager == null,新建一个(确保非空)。 - 先遍历
def.elements找到<toolbar>记录toolbarLeft(用于后续布局)。 - 再次遍历,根据
c.tag分别处理:close:创建关闭按钮,点击隐藏窗口。title:创建标题图标。toolbar:调用createToolbar(c)。playlist:调用createPlaylistArea(c)(创建左右列表区域)。scrollbar:记录scrollbarCtl(用于创建皮肤滚动条)。
4.2 加载 <PlayList> 样式
在 createPlaylistArea() 中,通过 UIUtils.loadSkinXml(skin, "PlayList") 加载皮肤包中的 PlayList.xml 或相应节点,解析出颜色配置,应用到 colorText 等字段。
byte[] xmlData = UIUtils.loadSkinXml(skin, "PlayList");
if (xmlData != null) {
Document doc = UIUtils.parseXml(xmlData);
Element playlist = (Element) doc.getDocumentElement().getElementsByTagName("PlayList").item(0);
if (playlist != null) {
colorText = UIUtils.getColorAttribute(playlist, "Color_Text", colorText);
// ... 其他颜色
}
}
- 这些颜色随后被应用到
listLeft、listRight以及它们的渲染器中。
4.3 字体
使用 FontUtils.getDefaultChineseFont(12) 获取支持中文的字体,大小 12。
5. 左右列表创建(createPlaylistArea)
5.1 布局计算
- 从
playlistCtl获取x, y, w。 - 计算底部边距(从
def.resizeRect中提取bottomMargin)。 - 高度
h = def.height - y - bottomMargin。 - 左侧宽度
leftWidth = (int)(w * 0.35)。
5.2 左侧列表(播放列表切换)
refreshLeftList(); // 初始化 listLeft(JList<String>)
scrollLeft = new TransparentScrollPane(listLeft);
scrollLeft.setBounds(x, y, leftWidth, h);
scrollLeft.setBorder(null);
scrollLeft.getVerticalScrollBar().setUI(createScrollbarUI());
getContentPane().add(scrollLeft);
addControl(scrollLeft, ctl);
refreshLeftList()从playlistManager获取所有播放列表,填充到listLeft模型,并选中当前播放列表。TransparentScrollPane是自定义类,透明背景,无边框。- 滚动条使用
createScrollbarUI()创建皮肤化滚动条。
5.3 右侧列表(歌曲列表)
refreshRightList(); // 初始化 listRight(VirtualList)
scrollRight = new TransparentScrollPane(listRight);
scrollRight.setBounds(x + leftWidth, y, w - leftWidth, h);
scrollRight.getVerticalScrollBar().setUI(createScrollbarUI());
getContentPane().add(scrollRight);
addControl(scrollRight, ctl);
refreshRightList()从当前播放列表获取歌曲数据,构造显示字符串列表(序号 + 歌曲名 + 时长),设置到VirtualList。VirtualList是一个自定义高性能列表组件,支持大量数据、交替行颜色、选择高亮、工具提示等。
5.4 列表交互事件
- 左侧列表:添加
ListSelectionListener,当选择改变时,调用playlistManager.setCurrentPlaylist(index)并刷新右侧列表。 - 右侧列表:通过
setMouseClickListener设置双击播放、右键弹出菜单。 - 右侧工具提示:通过
setToolTipProvider提供歌曲详情 HTML 提示。
5.5 分割条(Divider)
createDivider();
getContentPane().setComponentZOrder(divider, 3);
syncDividerLayout();
createDivider()创建了一个JPanel,宽度 5px,背景半透明黑色,鼠标悬停变为蓝色。- 它实现了鼠标拖拽调整左右比例,更新
splitRatio并调用syncDividerLayout()。 syncDividerLayout()在窗口缩放或分割条拖动时重算左右列表与分割条的位置。
6. 工具栏与菜单系统
6.1 工具栏创建(createToolbar)
byte[] bmp = skin.getBmp(ctl.image);
byte[] hotBmp = ctl.hotImage != null ? skin.getBmp(ctl.hotImage) : null;
toolbar = new TtToolbar(bmp, hotBmp, 7);
TtToolbar是一个水平排列多个按钮的组件,接受image(普通状态)和hotImage(鼠标悬停状态),按钮数量由构造参数(7)指定。- 每个按钮可关联一个
JPopupMenu,点击弹出下拉菜单。 - 随后将 7 个按钮分别关联
addMenu, delMenu, sortMenu, findMenu, modeMenu, optMenu, mainMenu。
6.2 菜单填充
每个菜单都是 JPopupMenu,使用 org.ttplayer.util.UIUtils.createMenuItem 创建带图标和动作的菜单项。部分菜单(如 modeMenu)包含 JCheckBoxMenuItem,用于单选/多选模式。
所有菜单项的动作最终委托给对应的 doXxx() 方法,例如 doAddFile(), doDeleteSelected(), doSortByField() 等。
7. 核心操作实现
7.1 添加文件/文件夹(doAddFile, doAddFolder)
- 使用
JFileChooser选择文件或文件夹。 - 调用
loadSongsAsync(dir, files, false)异步加载。
7.2 loadSongsAsync —— 异步加载歌曲
这是最复杂的方法之一,它支持后台扫描目录、实时更新进度对话框、可取消。
参数:dir(目录)、files(文件列表)、playFirst(是否加载后立即播放第一个)。
流程:
- 创建一个模态(
MODELESS)进度对话框JDialog,包含进度条、标签和取消按钮。 - 使用
SwingWorker在后台执行:- 若
dir != null,递归收集所有音频文件(collectAudioFiles)。 - 遍历文件列表,对每个文件创建
Song对象(调用new Song(filePath),它会在构造时尝试读取元数据)。 - 通过
publish更新进度(当前索引/总数、文件名)。
- 若
- 在
process中更新进度条和标签。 - 在
done中关闭对话框,将解析好的Song列表添加到当前播放列表末尾,刷新右侧列表,保存配置,若playFirst则调用playerEngine.play(firstIdx)。
取消支持:点击取消按钮调用 worker.cancel(true),doInBackground 中的 isCancelled() 会中断循环,保留已加载的部分歌曲。
7.3 删除操作
doDeleteSelected:删除右侧列表选中的歌曲。doDeleteNotSelected:保留选中的歌曲,删除其他所有。doDeleteDuplicates:根据文件路径或标题去重。doClearList:清空当前播放列表。
7.4 排序
doSortByField(String field):按标题、艺术家、文件名、路径排序,使用Comparator比较。doReverse:反转列表。doRandomSort:随机打乱。
7.5 查找
doFind:弹出输入框,在当前播放列表中搜索包含关键词的歌曲(标题+艺术家),将匹配的索引存入foundIndices,并选中第一个。doFindNext:循环定位到下一个匹配项。
7.6 播放模式
doSelectMode(int mi):设置模式(顺序、循环、单曲、随机),更新菜单项选中状态,并弹出提示。doTogglePauseAfterPlay:切换“播放后暂停”状态。
7.7 播放列表管理(左侧列表右键菜单)
doSwitchList:切换到选中的播放列表。doNewList:新建空播放列表。doAddList:导入外部.m3u/.pls文件。doSaveList:导出当前播放列表为.m3u。doSaveAllLists:将所有非空播放列表保存到指定目录下的.m3u文件。doDeleteList:删除选中的播放列表(至少保留一个)。doRenameList:重命名播放列表。doSortPlaylists:按名称排序所有播放列表。
8. 滚动条皮肤化
private SkinScrollBarUI createScrollbarUI() {
SkinScrollBarUI ui = new SkinScrollBarUI(Color.decode("#202020"), Color.decode("#0080ff"));
if (scrollbarCtl != null) {
byte[] btnBmp = scrollbarCtl.buttonsImage != null ? skin.getBmp(scrollbarCtl.buttonsImage) : null;
byte[] thumbBmp = scrollbarCtl.thumbImage != null ? skin.getBmp(scrollbarCtl.thumbImage) : null;
byte[] barBmp = scrollbarCtl.barImage != null ? skin.getBmp(scrollbarCtl.barImage) : null;
ui.setSkinImages(btnBmp, thumbBmp, barBmp);
}
return ui;
}
- 使用
SkinScrollBarUI,传入降级颜色。 - 若皮肤中定义了滚动条图片(
scrollbarCtl中的buttonsImage、thumbImage、barImage),则加载并设置到 UI。
9. 与外部组件交互
setPlayerEngine:注入引擎,并调用syncPlayingHighlight()高亮当前播放歌曲。setLyricWindow、setEqualizerWindow、setMiniWindow、setDesktopLyricWindow:注入其他窗口引用,以便菜单操作切换显示。- 菜单中的“歌词显示”、“均衡器”、“桌面歌词”、“迷你模式”等项调用对应的
doToggleXxx()方法,切换对应窗口的可见性。 - 换肤:调用
doSkin()打开SkinSelectDialog,选中后通过skinChangeListener回调通知主窗口。
10. 界面布局与自适应
- 窗口尺寸变化时,
repositionControls()被调用(SkinWindow的ComponentListener),进而调用syncDividerLayout()重新计算左右列表和分割条的位置。 syncDividerLayout()考虑了playlistCtl的边距,以及splitRatio,计算出实际宽度,并设置scrollLeft、scrollRight和divider的边界。- 分割条拖拽实时更新
splitRatio并刷新布局。
11. 工具提示
- 右侧列表通过
setToolTipProvider提供每行的 HTML 工具提示,包含歌曲名(加粗)和详细信息(时长、艺术家、专辑等)。 - 左侧列表使用
TransparentListCellRenderer,在渲染时通过setToolTipText设置提示。
12. 自定义列表渲染器
TransparentJList:继承JList,设置透明背景。TransparentListCellRenderer:根据是否选中设置背景和前景色,并提供工具提示。- 右侧
VirtualList内部实现了自定义绘制,支持交替行背景色(colorBkgnd/colorBkgnd2)、序号颜色、时长颜色、选中高亮等。
13. 总结
PlaylistWindow 是一个功能完整的播放列表管理窗口,它集成了:
- 皮肤化 UI:完全依赖
SkinWindow和自定义控件(TtToolbar、VirtualList、SkinScrollBarUI)。 - 丰富的播放列表操作:增删改查、排序、导入导出、去重。
- 异步加载:后台扫描文件夹,带进度对话框,可取消。
- 拖放分割条:调整左右列表比例,记忆用户偏好(但未持久化)。
- 与其他窗口协同:通过监听器和引用,控制歌词窗口、均衡器、迷你模式、桌面歌词等。
- 多语言支持:菜单文本使用
Messages国际化,并提供了语言切换菜单项。
License
本项目仅供学习和个人使用。皮肤资源版权归原作者所有。
「获取方式」:后台回复 ttplayer


浙公网安备 33010602011771号