怀念经典,一步步开发一个ttplayer千千静听,滚动条

皮肤滚动条(SkinScrollBarUI)完整解析

image

一、滚动条在皮肤中的配置方式

千千静听的皮肤中,滚动条并不是通过窗口内部的 <scrollbar> 标签来定义的,而是通过独立的图片资源 + 可选的 XML 配置来实现皮肤化。通常,皮肤包(.skn)中会包含以下图片(命名约定):

  • scroll_buttons.bmp(或 scroll_buttons.png):滚动条两端按钮的精灵图(包含上下/左右按钮,每种按钮有 Normal/Hover/Pressed 三态)。
  • scroll_thumb.bmp:滑块(Thumb)的精灵图(包含 Normal/Hover/Pressed 三态)。
  • scroll_bar.bmp:滚动条轨道背景图(通常是一个纯色渐变或纹理)。

这些图片可以由 Skin.xml 中的 <ScrollBar> 节点指定,也可以直接按照固定文件名约定由上层代码加载。例如,SkinWindowPlayerWindow 在初始化滚动条时,会调用 setSkinImages() 传入对应的字节数据。


二、SkinScrollBarUI 类结构概览

SkinScrollBarUI 继承自 BasicScrollBarUI,重写了滚动条轨道的绘制、滑块的绘制以及两端按钮的创建,用自定义图片替代了 Swing 默认的纯色绘制。

2.1 主要成员变量

private BufferedImage buttonsImg;          // 按钮总图(上下两个按钮水平排列或垂直排列)
private BufferedImage[] thumbFrames;       // 滑块三态帧(normal, hover, pressed)
private BufferedImage barImg;              // 轨道背景图
private int thumbState = 0;                // 滑块状态(0=normal, 1=hover, 2=pressed)
private final Color fallbackTrack;         // 降级轨道颜色(图片加载失败时使用)
private final Color fallbackThumb;         // 降级滑块颜色

三、图片加载与帧分割

3.1 setSkinImages(byte[] buttonsBmp, byte[] thumbBmp, byte[] barBmp)

public void setSkinImages(byte[] buttonsBmp, byte[] thumbBmp, byte[] barBmp) {
    Color transp = new Color(255, 0, 255);
    if (buttonsBmp != null) {
        buttonsImg = SkinWindow.decodeBmp(buttonsBmp, transp);
    }
    thumbFrames = splitFrames(thumbBmp, transp);
    if (barBmp != null) {
        BufferedImage src = SkinWindow.decodeBmp(barBmp, transp);
        if (src != null) barImg = src;
    }
}
  • 使用洋红(#FF00FF)作为透明色,通过 SkinWindow.decodeBmp() 解码并替换透明色。
  • thumbFramessplitFrames() 方法切分。

3.2 splitFrames(byte[] bmpData, Color transp)splitFramesFromImage(BufferedImage img)

private static BufferedImage[] splitFramesFromImage(BufferedImage img) {
    int sw = img.getWidth();
    int cols;
    if (sw % 3 == 0) cols = 3;
    else if (sw % 2 == 0) cols = 2;
    else cols = 1;
    int fw = sw / cols;
    int fh = img.getHeight();
    BufferedImage[] frames = new BufferedImage[cols];
    for (int i = 0; i < cols; i++) {
        frames[i] = img.getSubimage(i * fw, 0, fw, fh);
    }
    return frames;
}
  • 智能帧数检测:根据图片宽度是否能被 3、2 整除来决定帧数(优先 3 帧,其次 2 帧,否则 1 帧)。
  • 这意味着皮肤制作者可以自由选择提供 1、2 或 3 帧状态图。若提供 3 帧,则按顺序表示 Normal、Hover、Pressed;若提供 2 帧,则可能表示 Normal 和 Hover/Pressed 合并;若提供 1 帧,则所有状态使用同一张图。

3.3 按钮图片分割(splitButtonHalf

private static BufferedImage[] splitButtonHalf(BufferedImage img, int half) {
    int fullH = img.getHeight();
    int halfH = fullH / 2;
    int y = half * halfH;
    BufferedImage halfImg = img.getSubimage(0, y, img.getWidth(), halfH);
    return splitFramesFromImage(halfImg);
}
  • 布局约定buttonsImg 垂直方向被分为上下两半,上半部分为 减少按钮(上/左),下半部分为 增加按钮(下/右)。
  • 每一半又按照水平方向分割成若干帧(1~3 帧),代表该按钮的不同状态。

四、滚动条组件绘制

4.1 轨道绘制(paintTrack

protected void paintTrack(Graphics g, JComponent c, Rectangle r) {
    if (barImg != null) {
        g.drawImage(barImg, r.x, r.y, r.width, r.height, null);
    }
}
  • 若提供了轨道背景图,则直接拉伸绘制到整个轨道区域。

4.2 滑块绘制(paintThumb

protected void paintThumb(Graphics g, JComponent c, Rectangle r) {
    if (r.isEmpty()) return;
    if (thumbFrames != null && thumbState >= 0 && thumbState < thumbFrames.length) {
        BufferedImage frame = thumbFrames[thumbState];
        if (frame != null) {
            g.drawImage(frame, r.x, r.y, r.width, r.height, null);
        }
    }
}
  • 根据当前 thumbState(0/1/2)选择对应的帧绘制滑块,状态由鼠标事件更新。

4.3 滑块最小尺寸(getMinimumThumbSize

protected Dimension getMinimumThumbSize() {
    if (thumbFrames != null && thumbFrames.length > 0 && thumbFrames[0] != null) {
        return new Dimension(thumbFrames[0].getWidth(), thumbFrames[0].getHeight());
    }
    return super.getMinimumThumbSize();
}
  • 滑块尺寸至少为图片原始尺寸,避免太小无法操作。

4.4 滚动条首选尺寸(getPreferredSize

public Dimension getPreferredSize(JComponent c) {
    if (barImg != null) {
        return new Dimension(barImg.getWidth(), c.getHeight());
    }
    return super.getPreferredSize(c);
}
  • 若提供了轨道背景图,则使用其宽度(垂直滚动条)作为滚动条宽度,高度由容器决定。

五、鼠标事件与状态更新

5.1 滑块状态监听

scrollbar.addMouseListener(new MouseAdapter() {
    @Override public void mouseEntered(MouseEvent e) { thumbState = 1; scrollbar.repaint(); }
    @Override public void mouseExited(MouseEvent e)  { thumbState = 0; scrollbar.repaint(); }
    @Override public void mousePressed(MouseEvent e) {
        Rectangle tb = getThumbBounds();
        if (tb != null && tb.contains(e.getPoint())) {
            thumbState = 2;
        }
    }
    @Override public void mouseReleased(MouseEvent e) { thumbState = 1; scrollbar.repaint(); }
});
  • 悬停:鼠标进入滚动条区域 → thumbState=1(Hover)。
  • 离开:→ thumbState=0(Normal)。
  • 按下:仅在鼠标按下滑块本身时 → thumbState=2(Pressed)。
  • 释放:→ thumbState=1(Hover),除非鼠标已离开。

5.2 按钮的状态管理

按钮内部使用 SkinBtn 自定义组件,它有自己的 state(0/1/2)和相应的鼠标监听器,与滑块的监听类似。


六、自定义按钮(SkinBtn

6.1 构造与样式

SkinBtn(BufferedImage[] frames) {
    this.frames = frames;
    setPreferredSize(new Dimension(frames[0].getWidth(), frames[0].getHeight()));
    setRequestFocusEnabled(false);
    setFocusPainted(false);
    setBorderPainted(false);
    setContentAreaFilled(false);
    setOpaque(false);
}
  • 按钮去除了所有默认绘制(边框、背景、焦点高亮),完全由图片绘制。
  • 图片帧数由 splitFramesFromImage 决定(1~3 帧)。

6.2 绘制(paintComponent

protected void paintComponent(Graphics g) {
    if (frames != null && frames.length > state && frames[state] != null) {
        g.drawImage(frames[state], 0, 0, frames[state].getWidth(), frames[state].getHeight(), null);
    }
}
  • 按当前状态绘制对应帧。

七、按钮创建(createDecreaseButton / createIncreaseButton

protected JButton createDecreaseButton(int orientation) {
    if (buttonsImg != null) {
        BufferedImage[] frames = splitButtonHalf(buttonsImg, 0); // 上半部分
        if (frames != null && frames.length > 0) return new SkinBtn(frames);
    }
    return super.createDecreaseButton(orientation);
}
  • orientation 参数虽然传入,但实际未被使用,因为图片本身已经区分了上下半部分。
  • splitButtonHalf 根据参数 half(0 或 1)取出对应的半图,再切分帧。

八、降级策略

若图片加载失败或未提供,则使用 fallbackTrackfallbackThumb 纯色绘制,确保滚动条始终可用。相关代码:

protected void configureScrollBarColors() {
    scrollbar.setOpaque(false);
    if (barImg == null) scrollbar.setBackground(fallbackTrack);
    scrollbar.setForeground(fallbackThumb);
}
  • 这样即使没有皮肤图片,滚动条也能以纯色正常显示。

九、与皮肤系统的集成

9.1 调用流程

  1. 当创建包含滚动条的窗口(如播放列表窗口 PlaylistWindow 或歌词窗口 LyricWindow)时,会实例化 JScrollPane
  2. 在初始化时,通过 SkinLoader 从皮肤包中加载 scroll_buttons.bmpscroll_thumb.bmpscroll_bar.bmp 三个文件(可能通过 TtSkin.getBmp() 获取)。
  3. 创建 SkinScrollBarUI 实例,调用 setSkinImages() 传入图片数据。
  4. 调用 scrollPane.getVerticalScrollBar().setUI(ui)setUI() 应用到滚动条。

9.2 XML 配置(可选)

尽管 SkinScrollBarUI 本身不解析 XML,但在某些扩展实现中,可能从 Skin.xml 中读取 <ScrollBar> 节点以获取图片文件名,例如:

<ScrollBar>
    <Buttons>scroll_buttons.bmp</Buttons>
    <Thumb>scroll_thumb.bmp</Thumb>
    <Track>scroll_bar.bmp</Track>
</ScrollBar>

然后由上层代码解析并调用 setSkinImages()


十、设计与总结

特性 实现方式
三态支持 滑块和按钮均支持 Normal / Hover / Pressed 三态,提升交互反馈
智能帧检测 根据图片宽度自动判断帧数(1/2/3 帧),兼容不同皮肤资源
布局约定 按钮图垂直分为两半,分别对应减少/增加按钮,清晰简单
透明支持 统一使用洋红作为透明色,与 TTPlayer 皮肤标准一致
降级方案 无图片时使用纯色,确保功能不受影响
轻量重绘 仅重写必要的绘制方法,保留 Swing 滚动条的其余逻辑

SkinScrollBarUI 是一个高度可定制的滚动条外观实现,它通过图片替换了 Swing 默认的绘制,完美融入了千千静听的皮肤体系。其设计简洁、扩展性好,是 Swing 中自定义组件外观的优秀范例。

License

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

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

image

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