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

image

PlaylistWindow 播放列表窗口源码完整分解

PlaylistWindow 是 TTPlayer 中负责管理播放列表的核心窗口,继承自 SkinWindow,因此拥有皮肤背景、九宫格缩放、自定义边框等能力。它包含左侧的播放列表切换列表和右侧的歌曲列表,支持添加、删除、排序、查找、播放模式切换、拖放歌曲、滚动条皮肤化等丰富的交互功能,并与 PlaylistManagerPlayerEngine、歌词窗口、均衡器窗口等组件协同工作。

下面从多个维度对其源码进行详细分解。


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 = 70MIN_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_windowWindowDef
  • 设置窗口标题(国际化)。
  • 设置最小尺寸为皮肤定义尺寸或默认 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() 中:

  1. playlistManager == null,新建一个(确保非空)。
  2. 先遍历 def.elements 找到 <toolbar> 记录 toolbarLeft(用于后续布局)。
  3. 再次遍历,根据 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);
        // ... 其他颜色
    }
}
  • 这些颜色随后被应用到 listLeftlistRight 以及它们的渲染器中。

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(是否加载后立即播放第一个)。

流程

  1. 创建一个模态(MODELESS)进度对话框 JDialog,包含进度条、标签和取消按钮。
  2. 使用 SwingWorker 在后台执行:
    • dir != null,递归收集所有音频文件(collectAudioFiles)。
    • 遍历文件列表,对每个文件创建 Song 对象(调用 new Song(filePath),它会在构造时尝试读取元数据)。
    • 通过 publish 更新进度(当前索引/总数、文件名)。
  3. process 中更新进度条和标签。
  4. 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 中的 buttonsImagethumbImagebarImage),则加载并设置到 UI。

9. 与外部组件交互

  • setPlayerEngine:注入引擎,并调用 syncPlayingHighlight() 高亮当前播放歌曲。
  • setLyricWindowsetEqualizerWindowsetMiniWindowsetDesktopLyricWindow:注入其他窗口引用,以便菜单操作切换显示。
  • 菜单中的“歌词显示”、“均衡器”、“桌面歌词”、“迷你模式”等项调用对应的 doToggleXxx() 方法,切换对应窗口的可见性。
  • 换肤:调用 doSkin() 打开 SkinSelectDialog,选中后通过 skinChangeListener 回调通知主窗口。

10. 界面布局与自适应

  • 窗口尺寸变化时,repositionControls() 被调用(SkinWindowComponentListener),进而调用 syncDividerLayout() 重新计算左右列表和分割条的位置。
  • syncDividerLayout() 考虑了 playlistCtl 的边距,以及 splitRatio,计算出实际宽度,并设置 scrollLeftscrollRightdivider 的边界。
  • 分割条拖拽实时更新 splitRatio 并刷新布局。

11. 工具提示

  • 右侧列表通过 setToolTipProvider 提供每行的 HTML 工具提示,包含歌曲名(加粗)和详细信息(时长、艺术家、专辑等)。
  • 左侧列表使用 TransparentListCellRenderer,在渲染时通过 setToolTipText 设置提示。

12. 自定义列表渲染器

  • TransparentJList:继承 JList,设置透明背景。
  • TransparentListCellRenderer:根据是否选中设置背景和前景色,并提供工具提示。
  • 右侧 VirtualList 内部实现了自定义绘制,支持交替行背景色(colorBkgnd / colorBkgnd2)、序号颜色、时长颜色、选中高亮等。

13. 总结

PlaylistWindow 是一个功能完整的播放列表管理窗口,它集成了:

  • 皮肤化 UI:完全依赖 SkinWindow 和自定义控件(TtToolbarVirtualListSkinScrollBarUI)。
  • 丰富的播放列表操作:增删改查、排序、导入导出、去重。
  • 异步加载:后台扫描文件夹,带进度对话框,可取消。
  • 拖放分割条:调整左右列表比例,记忆用户偏好(但未持久化)。
  • 与其他窗口协同:通过监听器和引用,控制歌词窗口、均衡器、迷你模式、桌面歌词等。
  • 多语言支持:菜单文本使用 Messages 国际化,并提供了语言切换菜单项。

License

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

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

image

posted @ 2026-08-28 12:25  tomj2ee  阅读(3)  评论(0)    收藏  举报