怀念经典,一步步开发一个ttplayer千千静听,led绘制

我们先从千千静听(TTPlayer)的 LED 显示机制讲起,包括它在皮肤 XML 中的定义方式,然后对 TtLed 这个类的源码进行逐行、深度剖析
image


第一部分:千千静听的 LED 显示是什么?

1. 视觉风格与用途

在千千静听中,LED 显示指的是主窗口中间那块模拟老式数码管(VFD 荧光显示或 LED 点阵)的区域,主要用于:

  • 播放时间(当前播放位置 / 总时长),格式通常为 MM:SS(如 03:45)或 HH:MM:SS
  • 频谱指示(某些皮肤下显示跳动的柱状图,但那是另一个控件 TtEq,不是 TtLed)。

它的视觉特点是字符与字符之间保持固定间距,背景全黑或透明,字符颜色鲜艳(青绿、橙红等),极具怀旧感。

2. 皮肤文件(.skn)中的定义 —— XML 配置

Skin.xml 文件中,LED 控件通过 <led> 标签定义,TtSkin 解析器会专门处理它。

典型 XML 片段

<main_window position="0,0,275,116" image="main.bmp">
    <!-- 时间显示 LED -->
    <led position="95,72,155,88" image="led_time.bmp" />
    <!-- 或者包含冒号等符号的宽LED -->
</main_window>

TtSkin 解析 led 时的特殊逻辑(参考你之前发的 TtSkin.java):

if ("led".equals(btn.tag)) {
    btn.cols = 12;                          // 强制 12 列
    btn.frameWidth = iw / 12;               // 每帧宽度 = 总宽 / 12
    btn.frameHeight = ih;                   // 高度不变
}
  • 硬编码 12 列:因为 LED 图片必须包含 10 个数字(0-9)+ 冒号(:)+ 短横(-),恰好 12 个符号水平排列。

第二部分:TtLed 源码完整深度剖析

1. 类结构概览

public class TtLed extends javax.swing.JComponent {
    private BufferedImage[] frames = new BufferedImage[12];
    private String text = "00:00";
    private boolean alignRight = true;  // 右对齐,默认 true
    // ... 
}
  • 继承自 JComponent,是一个轻量级 Swing 组件。
  • frames 数组:存储 12 帧图像(索引 0~9 对应字符 '0'~'9',10 对应 ':',11 对应 '-')。
  • text:要显示的字符串,默认 "00:00"(5 个字符)。
  • alignRight:是否右对齐。如果为 true,当显示内容不足 6 个字符时,字符靠右显示(模拟倒计时或时间右对齐效果)。

2. 构造方法(图像加载与切分)

2.1 重载构造

public TtLed(byte[] bmpData) {
    this(bmpData, new Color(255, 0, 255));
}
public TtLed(byte[] bmpData, Color transp) {
    // ... 核心逻辑
}
  • 默认透明色为洋红 #FF00FF,这是 TTPlayer 皮肤通用的透明标记色。

2.2 核心构造逻辑

BufferedImage src = SkinWindow.decodeBmp(bmpData, transp);
if (src != null) {
    int fw = src.getWidth() / 12;  // ⭐ 关键:宽度必须是 12 的整数倍
    int fh = src.getHeight();
    for (int i = 0; i < 12; i++) {
        frames[i] = src.getSubimage(i * fw, 0, fw, fh);
    }
    // 设置首选大小为6个字符宽度
    setPreferredSize(new Dimension(fw * 6, fh));
}
setOpaque(false);
  • 图像切分:从 src 中水平切出 12 等份,每份作为一帧。
  • fw * 6:虽然标准时间 MM:SS 只有 5 个字符(如 12:34),但为了预留安全余量或支持 -MM:SS(负号倒计时),这里固定给定了 6 个字符 的默认宽度。这也与 paintComponent 中的 len = Math.min(display.length(), 6) 相呼应。
  • setOpaque(false):设置背景透明,让皮肤的背景图片透出来。

3. setText(String t)

public void setText(String t) {
    this.text = (t != null) ? t : "00:00";
    repaint();
}
  • 外部(如播放器内核)通过此方法更新时间字符串。
  • 传入 null 时兜底为 "00:00"
  • 调用 repaint() 触发重绘。

4. paintComponent(Graphics g) —— 核心绘制逻辑

这是整个类最复杂的部分,我们逐段分解。

4.1 空帧检查与尺寸获取

if (frames[0] == null) return;
int w = getWidth();
int h = getHeight();
int fw = frames[0].getWidth();
int fh = frames[0].getHeight();
  • 如果没有加载到图片,直接返回(避免 NullPointerException)。

4.2 字符串截取与字符数量计算

String display = text;
int len = Math.min(display.length(), 6);
  • 最多显示 6 个字符。如果传入 "123:45"(6个字符)显示完整;如果传入 "12:34:56"(8个字符),只会显示前 6 个 "12:34:"(但通常播放器不会传超过 6 个字符给这个控件)。

4.3 动态计算字符宽度(自适应缩放)

int actualCharWidth = fw;
if (len > 0) {
    actualCharWidth = Math.min(fw, w / len);
}
int totalW = len * actualCharWidth;
  • 设计意图:当组件的实际宽度小于原始图片宽度时,按比例压缩每个字符,确保所有字符都能完整显示在控件内。
  • Math.min(fw, w / len):取“原始帧宽”和“可用宽度/字符数”中的较小值。如果控件宽度很宽,也不会超过原始帧宽(避免放大导致锯齿)。
  • totalW:所有字符占据的总宽度。

4.4 对齐计算(默认右对齐)

int startX = 0;
if (alignRight) {
    if (totalW <= w) {
        startX = w - totalW;
    }
}
int y = (h - fh) / 2;  // 垂直居中
  • 右对齐逻辑:如果总宽度小于控件宽度,将起点定在 (宽度 - 总宽) 处,字符向右靠拢。
  • 垂直居中:将帧绘制在控件垂直中央。

4.5 字符映射与逐帧绘制

for (int i = 0; i < len; i++) {
    char c = display.charAt(i);
    int idx;
    if (c >= '0' && c <= '9') idx = c - '0';
    else if (c == ':') idx = 10;
    else if (c == '-') idx = 11;
    else continue;  // 不支持其他字符,跳过

    if (idx >= 0 && idx < 12 && frames[idx] != null) {
        int drawX = startX + i * actualCharWidth;
        g.drawImage(frames[idx], drawX, y, actualCharWidth, fh, null);
    }
}
  • 映射表
    • '0'~'9' → 索引 0~9
    • ':' → 索引 10
    • '-' → 索引 11
  • 不支持空格、字母等,直接跳过(不绘制)。
  • 绘制:调用 drawImage 将帧拉伸到 (actualCharWidth, fh)。注意这里垂直方向没有拉伸,保持了原始高度,只有宽度动态变化。

5. 对齐控制 Setter

public void setAlignRight(boolean r) { alignRight = r; }
  • 允许外部动态切换对齐方式(某些皮肤可能希望左对齐)。

第三部分:

✅ 设计亮点

  1. 高度复刻经典:12 帧精灵图完美模拟了老式数码管显示,怀旧感十足。
  2. 自适应宽度:通过动态计算 actualCharWidth,组件可以缩放而不变形(仅水平拉伸字符),适合不同 DPI 或窗口缩放。
  3. 默认右对齐:符合音乐播放器时间显示的习惯(时间向右对齐,便于快速定位时长)。

第四部分:与 TtSkin / TtButton 的联动关系

图片切分方式 帧数 用途
TtButton 宽度 / 4 4 帧 正常、悬停、按下、禁用
TtLed 宽度 / 12 12 帧 0-9、冒号、短横

两者都依赖 SkinWindow.decodeBmp 解码 BMP,并遵循相同的透明色约定。在 UI 构建时,TtSkin 解析出 <led> 标签,将 image 属性指向的图片数据传给 TtLed 构造函数,完成实例化。


总结

TtLed 是一个专精于数字符号显示的轻量组件,它通过 12 帧精灵图 + 字符映射,完美还原了千千静听标志性的数码管时间显示。

License

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

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

image

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