怀念经典,一步步开发一个ttplayer千千静听,滚动条
皮肤滚动条(SkinScrollBarUI)完整解析

一、滚动条在皮肤中的配置方式
千千静听的皮肤中,滚动条并不是通过窗口内部的 <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> 节点指定,也可以直接按照固定文件名约定由上层代码加载。例如,SkinWindow 或 PlayerWindow 在初始化滚动条时,会调用 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()解码并替换透明色。 thumbFrames由splitFrames()方法切分。
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)取出对应的半图,再切分帧。
八、降级策略
若图片加载失败或未提供,则使用 fallbackTrack 和 fallbackThumb 纯色绘制,确保滚动条始终可用。相关代码:
protected void configureScrollBarColors() {
scrollbar.setOpaque(false);
if (barImg == null) scrollbar.setBackground(fallbackTrack);
scrollbar.setForeground(fallbackThumb);
}
- 这样即使没有皮肤图片,滚动条也能以纯色正常显示。
九、与皮肤系统的集成
9.1 调用流程
- 当创建包含滚动条的窗口(如播放列表窗口
PlaylistWindow或歌词窗口LyricWindow)时,会实例化JScrollPane。 - 在初始化时,通过
SkinLoader从皮肤包中加载scroll_buttons.bmp、scroll_thumb.bmp、scroll_bar.bmp三个文件(可能通过TtSkin.getBmp()获取)。 - 创建
SkinScrollBarUI实例,调用setSkinImages()传入图片数据。 - 调用
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


浙公网安备 33010602011771号