在最开始写netbeans插件的时候,就已经开始在想如何实现同步的歌词显示,并且当时也差不多实现了大概的框架,所以YOYOPlayer的歌词显示模块基本上和netbeans插件的歌词显示模块是一样的,只不过一些细节做了一些改进,比如每行歌词的渐入渐出,以后单行歌词实现的卡拉OK效果等等,并把一些设置集成到了整个YOYOPlayer的设置里面去了.
在我实现歌词同步显示的时候,思路是把每首歌的歌词分成一句一句,并且为每一句歌词定义一个对象,叫Sentence,它代表歌词里面的每一句,那么整首歌的对象呢?整首歌的对象我定了一个Lyric对象,它代表的是一首歌的歌词.它里面也包括了自动搜索歌词的实现,当然歌词有了得让它显示出来啊,所以又定义了一个专门用来显示歌词的面板,叫LyricPanel,它是继承自JPanel,为了统一管理,加上边框,所以又加了一个LyricUI类,这个类是直接放置到歌词显示窗口的,而LyricPanel被放置在LyricUI里面,这个时候LyricUI设置一下Border,就实现了无修饰窗口的有修饰效果,至于这一点,我们以后再讲.
各部份的代码如下:
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.hadeslee.yoyoplayer.lyric;
import com.hadeslee.yoyoplayer.playlist.PlayListItem;
import com.hadeslee.yoyoplayer.util.Config;
import com.hadeslee.yoyoplayer.util.Util;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.LinearGradientPaint;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 表示一首歌的歌词对象,它可以以某种方式来画自己
* @author hadeslee
*/
public class Lyric implements Serializable {
private static final long serialVersionUID = 20071125L;
private static Logger log = Logger.getLogger(Lyric.class.getName());
private int width;//表示歌词的显示区域的宽度
private int height;//表示歌词的显示区域的高度
private long time;//表示当前的时间是多少了。以毫秒为单位
private long tempTime;//表示一个暂时的时间,用于拖动的时候,确定应该到哪了
private List<Sentence> list = new ArrayList<Sentence>();//里面装的是所有的句子
private boolean isMoving;//是否正在被拖动
private int currentIndex;//当前正在显示的歌词的下标
private boolean initDone;//是否初始化完毕了
private transient PlayListItem info;//有关于这首歌的信息
private transient File file;//该歌词所存在文件
private boolean enabled = true;//是否起用了该对象,默认是起用的
private long during = Integer.MAX_VALUE;//这首歌的长度
/**
* 用ID3V1标签的字节和歌名来初始化歌词
* 歌词将自动在本地或者网络上搜索相关的歌词并建立关联
* 本地搜索将硬编码为user.home文件夹下面的Lyrics文件夹
* 以后改为可以手动设置.
* @param songName 歌名
* @param data ID3V1的数据
*/
public Lyric(final PlayListItem info) {
this.info = info;
this.during = info.getLength() * 1000;
this.file = info.getLyricFile();
log.info("传进来的歌名是:" + info.toString());
//只要有关联好了的,就不用搜索了直接用就是了
if (file != null) {
log.log(Level.INFO, "不用找了,直接关联到的歌词是:" + file);
init(file);
initDone = true;
return;
} else {
//否则就起一个线程去找了,先是本地找,然后再是网络上找
new Thread() {
public void run() {
doInit(info);
initDone = true;
}
}.start();
}
}
/**
* 读取某个指定的歌词文件,这个构造函数一般用于
* 拖放歌词文件到歌词窗口时调用的,拖放以后,两个自动关联
* @param file 歌词文件
* @param info 歌曲信息
*/
public Lyric(File file, PlayListItem info) {
this.file = file;
this.info = info;
init(file);
initDone = true;
}
/**
* 根据歌词内容和播放项构造一个
* 歌词对象
* @param lyric 歌词内容
* @param info 播放项
*/
public Lyric(String lyric, PlayListItem info) {
this.info = info;
this.init(lyric);
initDone = true;
}
private void doInit(PlayListItem info) {
init(info);
Sentence temp = null;
//这个时候就要去网络上找了
if (list.size() == 1) {
temp = list.remove(0);
try {
String lyric = Util.getLyric(info);
if (lyric != null) {
init(lyric);
saveLyric(lyric, info);
} else {//如果网络也没有找到,就要加回去了
list.add(temp);
}
} catch (IOException ex) {
Logger.getLogger(Lyric.class.getName()).log(Level.SEVERE, null, ex);
//如果抛了任何异常,也要加回去了
list.add(temp);
}
}
}
/**
* 把下载到的歌词保存起来,免得下次再去找
* @param lyric 歌词内容
* @param info 歌的信息
*/
private void saveLyric(String lyric, PlayListItem info) {
try {
//如果歌手不为空,则以歌手名+歌曲名为最好组合
String name = info.getFormattedName() + ".lrc";
// File dir = new File(Config.HOME, "Lyrics" + File.separator);
File dir = Config.getConfig().getSaveLyricDir();
dir.mkdirs();
file = new File(dir, name);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "GBK"));
bw.write(lyric);
bw.close();
info.setLyricFile(file);
log.info("保存完毕,保存在:" + file);
} catch (Exception exe) {
log.log(Level.SEVERE, "保存歌词出错", exe);
}
}
/**
* 设置此歌词是否起用了,否则就不动了
* @param b 是否起用
*/
public void setEnabled(boolean b) {
this.enabled = b;
}
/**
* 得到此歌词保存的地方
* @return 文件
*/
public File getLyricFile() {
return file;
}
/**
* 调整整体的时间,比如歌词统一快多少
* 或者歌词统一慢多少,为正说明要快,为负说明要慢
* @param time 要调的时间,单位是毫秒
*/
public void adjustTime(int time) {
//如果是只有一个显示的,那就说明没有什么效对的意义了,直接返回
if (list.size() == 1) {
return;
}
for (Sentence s : list) {
s.setFromTime(s.getFromTime() - time);
s.setToTime(s.getToTime() - time);
}
}
/**
* 根据一个文件夹,和一个歌曲的信息
* 从本地搜到最匹配的歌词
* @param dir 目录
* @param info 歌曲信息
* @return 歌词文件
*/
private File getMathedLyricFile(File dir, PlayListItem info) {
File matched = null;//已经匹配的文件
File[] fs = dir.listFiles(new FileFilter() {
public boolean accept(File pathname) {
return pathname.getName().toLowerCase().endsWith(".lrc");
}
});
for (File f : fs) {
//全部匹配或者部分匹配都行
if (matchAll(info, f) || matchSongName(info, f)) {
matched = f;
break;
}
}
return matched;
}
/**
* 根据歌的信息去初始化,这个时候
* 可能在本地找到歌词文件,也可能要去网络上搜索了
* @param info 歌曲信息
*/
private void init(PlayListItem info) {
File matched = null;
for (File dir : Config.getConfig().getSearchLyricDirs()) {
log.log(Level.FINE, "正在搜索文件夹:" + dir);
//得到歌曲信息后,先本地搜索,先搜索HOME文件夹
//如果还不存在的话,那建一个目录,然后直接退出不管了
if (!dir.exists()) {
dir.mkdirs();
}
matched = getMathedLyricFile(dir, info);
//当搜索到了,就退出
if (matched != null) {
break;
}
}
log.info("找到的是:" + matched);
if (matched != null) {
info.setLyricFile(matched);
file = matched;
init(matched);
} else {
init("");
}
}
/**
* 根据文件来初始化
* @param file 文件
*/
private void init(File file) {
BufferedReader br = null;
try {
br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "GBK"));
StringBuilder sb = new StringBuilder();
String temp = null;
while ((temp = br.readLine()) != null) {
sb.append(temp).append("\n");
}
init(sb.toString());
} catch (Exception ex) {
Logger.getLogger(Lyric.class.getName()).log(Level.SEVERE, null, ex);
} finally {
try {
br.close();
} catch (Exception ex) {
Logger.getLogger(Lyric.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
/**
* 是否完全匹配,完全匹配是指直接对应到ID3V1的标签,
* 如果一样,则完全匹配了,完全匹配的LRC的文件格式是:
* 阿木 - 有一种爱叫放手.lrc
* @param info 歌曲信息
* @param file 侯选文件
* @return 是否合格
*/
private boolean matchAll(PlayListItem info, File file) {
String name = info.getFormattedName();
String fn = file.getName().substring(0, file.getName().lastIndexOf("."));
if (name.equals(fn)) {
return true;
} else {
return false;
}
}
/**
* 是否匹配了歌曲名
* @param info 歌曲信息
* @param file 侯选文件
* @return 是否合格
*/
private boolean matchSongName(PlayListItem info, File file) {
String name = info.getFormattedName();
String rn = file.getName().substring(0, file.getName().lastIndexOf("."));
if (name.equalsIgnoreCase(rn) || info.getTitle().equalsIgnoreCase(rn)) {
return true;
} else {
return false;
}
}
/**
* 最重要的一个方法,它根据读到的歌词内容
* 进行初始化,比如把歌词一句一句分开并计算好时间
* @param content 歌词内容
*/
private void init(String content) {
//如果歌词的内容为空,则后面就不用执行了
//直接显示歌曲名就可以了
if (content == null || content.trim().equals("")) {
list.add(new Sentence(info.getFormattedName(), Integer.MIN_VALUE, Integer.MAX_VALUE));
return;
}
try {
BufferedReader br = new BufferedReader(new StringReader(content));
String temp = null;
while ((temp = br.readLine()) != null) {
parseLine(temp.trim());
}
br.close();
//读进来以后就排序了
Collections.sort(list, new Comparator<Sentence>() {
public int compare(Sentence o1, Sentence o2) {
return (int) (o1.getFromTime() - o2.getFromTime());
}
});
//处理第一句歌词的起始情况,无论怎么样,加上歌名做为第一句歌词,并把它的
//结尾为真正第一句歌词的开始
if (list.size() == 0) {
list.add(new Sentence(info.getFormattedName(), 0, Integer.MAX_VALUE));
return;
} else {
Sentence first = list.get(0);
list.add(0, new Sentence(info.getFormattedName(), 0, first.getFromTime()));
}
int size = list.size();
for (int i = 0; i < size; i++) {
Sentence next = null;
if (i + 1 < size) {
next = list.get(i + 1);
}
Sentence now = list.get(i);
if (next != null) {
now.setToTime(next.getFromTime() - 1);
}
}
//如果就是没有怎么办,那就只显示一句歌名了
if (list.size() == 1) {
list.get(0).setToTime(Integer.MAX_VALUE);
} else {
Sentence last = list.get(list.size() - 1);
last.setToTime(info == null ? Integer.MAX_VALUE : info.getLength() * 1000 + 1000);
}
} catch (Exception ex) {
Logger.getLogger(Lyric.class.getName()).log(Level.SEVERE, null, ex);
}
}
/**
* 分析这一行的内容,根据这内容
* 以及标签的数量生成若干个Sentence对象
* @param line 这一行
*/
private void parseLine(String line) {
if (line.equals("")) {
return;
}
Matcher m = Pattern.compile("(?<=\\[).*?(?=\\])").matcher(line);
List<String> temp = new ArrayList<String>();
int length = 0;
while (m.find()) {
String s = m.group();
temp.add(s);
length += (s.length() + 2);
}
try {
String content = line.substring(length > line.length() ? line.length() : length);
if (Config.getConfig().isCutBlankChars()) {
content = content.trim();
}
if (content.equals("")) {
return;
}
for (String s : temp) {
long t = parseTime(s);
if (t != -1) {
list.add(new Sentence(content, t));
}
}
} catch (Exception exe) {
}
}
/**
* 把如00:00.00这样的字符串转化成
* 毫秒数的时间,比如
* 01:10.34就是一分钟加上10秒再加上340毫秒
* 也就是返回70340毫秒
* @param time 字符串的时间
* @return 此时间表示的毫秒
*/
private long parseTime(String time) {
String[] ss = time.split("\\:|\\.");
//如果 是两位以后,就非法了
if (ss.length < 2) {
return -1;
} else if (ss.length == 2) {//如果正好两位,就算分秒
try {
int min = Integer.parseInt(ss[0]);
int sec = Integer.parseInt(ss[1]);
if (min < 0 || sec < 0 || sec >= 60) {
throw new RuntimeException("数字不合法!");
}
return (min * 60 + sec) * 1000L;
} catch (Exception exe) {
return -1;
}
} else if (ss.length == 3) {//如果正好三位,就算分秒,十毫秒
try {
int min = Integer.parseInt(ss[0]);
int sec = Integer.parseInt(ss[1]);
int mm = Integer.parseInt(ss[2]);
if (min < 0 || sec < 0 || sec >= 60 || mm < 0 || mm > 99) {
throw new RuntimeException("数字不合法!");
}
return (min * 60 + sec) * 1000L + mm * 10;
} catch (Exception exe) {
return -1;
}
} else {//否则也非法
return -1;
}
}
/**
* 设置其显示区域的高度
* @param height 高度
*/
public void setHeight(int height) {
this.height = height;
}
/**
* 设置其显示区域的宽度
* @param width 宽度
*/
public void setWidth(int width) {
this.width = width;
}
/**
* 设置时间
* @param time 时间
*/
public void setTime(long time) {
if (!isMoving) {
tempTime = this.time = time;
}
}
/**
* 得到是否初始化完成了
* @return 是否完成
*/
public boolean isInitDone() {
return initDone;
}
private void drawKaraoke(Graphics2D gd, Sentence now, int x, int y, long t) {
int nowWidth = now.getContentWidth(gd);
Color gradient = null;
//如果要渐入渐出才去求中间色,否则直接用高亮色画
if (Config.getConfig().isLyricShadow()) {
gradient = now.getBestInColor(Config.getConfig().getLyricHilight(), Config.getConfig().getLyricForeground(), t);
} else {
gradient = Config.getConfig().getLyricHilight();
}
if (Config.getConfig().isKaraoke()) {
float f = (t - now.getFromTime()) * 1.0f / (now.getToTime() - now.getFromTime());
if (f > 0.98f) {
f = 0.98f;
}
if (x == 0) {
x = 1;
}
if (nowWidth == 0) {
nowWidth = 1;
}
gd.setPaint(new LinearGradientPaint(x, y, x + nowWidth, y, new float[]{f, f + 0.01f}, new Color[]{gradient, Config.getConfig().getLyricForeground()}));
} else {
gd.setPaint(gradient);
}
Util.drawString(gd, now.getContent(), x, y);
}
/**
* 自力更生,画出自己在水平方向的方法
* 这个做是为了更方便地把歌词显示在
* 任何想显示的地方
* @param g 画笔
*/
public synchronized void drawH(Graphics g) {
if (!enabled) {
Sentence sen = new Sentence(info.getFormattedName());
int x = (width - sen.getContentWidth(g)) / 2;
int y = (height - sen.getContentHeight(g) + Config.getConfig().getV_SPACE()) / 2;
g.setColor(Config.getConfig().getLyricHilight());
Util.drawString(g, sen.getContent(), x, y);
return;
}
//首先看是不是初始化完毕了
if (!initDone) {
Sentence temp = new Sentence("正在搜索歌词");
int x = (width - temp.getContentWidth(g)) / 2;
int y = (height - temp.getContentHeight(g)) / 2;
g.setColor(Config.getConfig().getLyricHilight());
Util.drawString(g, temp.getContent(), x, y);
return;
}
//如果只存在一句的话,那就不要浪费那么多计算的时候了
//直接画在中间就可以了
if (list.size() == 1) {
Sentence sen = list.get(0);
int x = (width - sen.getContentWidth(g)) / 2;
int y = (height - sen.getContentHeight(g) + Config.getConfig().getV_SPACE()) / 2;
g.setColor(Config.getConfig().getLyricHilight());
Util.drawString(g, sen.getContent(), x, y);
} else {
//取一个time的副本,以防止在一个方法里面产生两种time的情况
long t = tempTime;
Graphics2D gd = (Graphics2D) g;
int index = getNowSentenceIndex(t);
if (!isMoving) {
currentIndex = index;
}
if (index == -1) {
Sentence sen = new Sentence(info.getFormattedName(), Integer.MIN_VALUE, Integer.MAX_VALUE);
int x = (width - sen.getContentWidth(g) - Config.getConfig().getH_SPACE()) / 2;
int y = (height - sen.getContentHeight(g) + Config.getConfig().getV_SPACE()) / 2;
g.setColor(Config.getConfig().getLyricHilight());
Util.drawString(g, sen.getContent(), x, y);
return;
}
Sentence now = list.get(index);
int nowWidth = now.getContentWidth(g) + Config.getConfig().getH_SPACE();
int x = (width) / 2 - now.getHIncrease(g, t);
int y = (height - now.getContentHeight(g)) / 2;
this.drawKaraoke(gd, now, x, y, t);
gd.setPaint(Config.getConfig().getLyricForeground());
int tempX = x;
//画出中间那句之前的句子
for (int i = index - 1; i >= 0; i--) {
Sentence sen = list.get(i);
int wid = sen.getContentWidth(g) + Config.getConfig().getH_SPACE();
tempX = tempX - wid;
if (tempX + wid < 0) {
break;
}
if (Config.getConfig().isLyricShadow()) {
if (i == index - 1) {
gd.setPaint(sen.getBestOutColor(Config.getConfig().getLyricHilight(),
Config.getConfig().getLyricForeground(), time));
} else {
gd.setPaint(Config.getConfig().getLyricForeground());
}
}
Util.drawString(g, sen.getContent(), tempX, y);
}
gd.setPaint(Config.getConfig().getLyricForeground());
tempX = x;
int tempWidth = nowWidth;
//画出中间那句之后的句子
for (int i = index + 1; i < list.size(); i++) {
Sentence sen = list.get(i);
tempX = tempX + tempWidth;
if (tempX > width) {
break;
}
Util.drawString(g, sen.getContent(), tempX, y);
tempWidth = sen.getContentWidth(g) + Config.getConfig().getH_SPACE();
}
}
}
/**
* 得到这批歌词里面,最长的那一句的长度
* @return 最长的长度
*/
public int getMaxWidth(Graphics g) {
int max = 0;
for (Sentence sen : list) {
int w = sen.getContentWidth(g);
if (w > max) {
max = w;
}
}
return max;
}
/**
* 得到一句话的X座标,因为可能对齐方式有
* 多种,针对每种对齐方式,X的座标不一
* 定一样。
* @param g 画笔
* @param sen 要求的句子
* @return 本句的X座标
*/
private int getSentenceX(Graphics g, Sentence sen) {
int x = 0;
int i = Config.getConfig().getLyricAlignMode();
switch (i) {
case Config.LYRIC_CENTER_ALIGN:
x = (width - sen.getContentWidth(g)) / 2;
break;
case Config.LYRIC_LEFT_ALIGN:
x = 0;
break;
case Config.LYRIC_RIGHT_ALIGN:
x = width - sen.getContentWidth(g);
break;
default://默认情况还是中间对齐
x = (width - sen.getContentWidth(g)) / 2;
break;
}
return x;
}
/**
* 画出自己在垂直方向上的过程
* @param g 画笔
*/
public synchronized void drawV(Graphics g) {
if (!enabled) {
Sentence sen = new Sentence(info.getFormattedName());
int x = (width - sen.getContentWidth(g)) / 2;
int y = (height - sen.getContentHeight(g) + Config.getConfig().getV_SPACE()) / 2;
g.setColor(Config.getConfig().getLyricHilight());
Util.drawString(g, sen.getContent(), x, y);
return;
}
//首先看是不是初始化完毕了
if (!initDone) {
Sentence temp = new Sentence("正在搜索歌词");
int x = getSentenceX(g, temp);
int y = (height - temp.getContentHeight(g)) / 2;
g.setColor(Config.getConfig().getLyricHilight());
Util.drawString(g, temp.getContent(), x, y);
return;
}
//如果只存在一句的话,那就不要浪费那么多计算的时候了
//直接画在中间就可以了
if (list.size() == 1) {
Sentence sen = list.get(0);
int x = getSentenceX(g, sen);
int y = (height - sen.getContentHeight(g)) / 2;
g.setColor(Config.getConfig().getLyricHilight());
Util.drawString(g, sen.getContent(), x, y);
} else {
long t = tempTime;
Graphics2D gd = (Graphics2D) g;
int index = getNowSentenceIndex(t);
if (!isMoving) {
currentIndex = index;
}
if (index == -1) {
Sentence sen = new Sentence(info.getFormattedName(), Integer.MIN_VALUE, Integer.MAX_VALUE);
int x = getSentenceX(g, sen);
int y = (height - sen.getContentHeight(g)) / 2;
gd.setPaint(Config.getConfig().getLyricHilight());
Util.drawString(g, sen.getContent(), x, y);
return;
}
Sentence now = list.get(index);
//先求出中间的最基准的纵座标
int y = (height + now.getContentHeight(g)) / 2 - now.getVIncrease(g, t);
int x = getSentenceX(g, now);
this.drawKaraoke(gd, now, x, y, t);
gd.setColor(Config.getConfig().getLyricForeground());
//然后再画上面的部份以及下面的部份
//这样就可以保证正在唱的歌词永远在正中间显示
int tempY = y;
//画出本句之前的句子
for (int i = index - 1; i >= 0; i--) {
Sentence sen = list.get(i);
int x1 = getSentenceX(g, sen);
tempY = tempY - sen.getContentHeight(g) - Config.getConfig().getV_SPACE();
if (tempY + sen.getContentHeight(g) < 0) {
break;
}
if (Config.getConfig().isLyricShadow()) {
if (i == index - 1) {
gd.setColor(sen.getBestOutColor(Config.getConfig().getLyricHilight(),
Config.getConfig().getLyricForeground(), time));
} else {
gd.setColor(Config.getConfig().getLyricForeground());
}
}
Util.drawString(g, sen.getContent(), x1, tempY);
}
gd.setColor(Config.getConfig().getLyricForeground());
tempY = y;
//画出本句之后的句子
for (int i = index + 1; i < list.size(); i++) {
Sentence sen = list.get(i);
int x1 = getSentenceX(g, sen);
tempY = tempY + sen.getContentHeight(g) + Config.getConfig().getV_SPACE();
if (tempY > height) {
break;
}
Util.drawString(g, sen.getContent(), x1, tempY);
}
}
}
/**
* 得到当前正在播放的那一句的下标
* 不可能找不到,因为最开头要加一句
* 自己的句子 ,所以加了以后就不可能找不到了
* @return 下标
*/
private int getNowSentenceIndex(long t) {
for (int i = 0; i < list.size(); i++) {
if (list.get(i).isInTime(t)) {
return i;
}
}
// throw new RuntimeException("竟然出现了找不到的情况!");
return -1;
}
/**
* 水平移动多少个象素,这个方法是给面板调用的
* 移动了这些象素以后,要马上算出这个象素所
* 对应的时间是多少,要注意时间超出的情况
* @param length
* @param g 画笔,因为对于每一个画笔长度不一样的
*/
public void moveH(int length, Graphics g) {
if (list.size() == 1 || !enabled) {
return;
}
//如果长度是大于0的,则说明是正向移动,快进
if (length > 0) {
Sentence now = list.get(currentIndex);
int nowWidth = now.getContentWidth(g);
float f = (time - now.getFromTime()) * 1.0f / (now.getToTime() - now.getFromTime());
//先算出当前的这一句还剩多少长度了
int rest = (int) ((1 - f) * nowWidth);
long timeAdd = 0;//要加多少时间
//如果剩下的长度足够了,那是最好,马上就可以返回了
if (rest > length) {
timeAdd = now.getTimeH(length, g);
} else {
timeAdd = now.getTimeH(rest, g);
for (int i = currentIndex; i < list.size(); i++) {
Sentence sen = list.get(i);
int len = sen.getContentWidth(g);
//如果加上下一句的长度还不够,就把时间再加,继续下一句
if (len + rest < length) {
timeAdd += sen.getDuring();
rest += len;
} else {
timeAdd += sen.getTimeH(length - rest, g);
break;
}
}
}
tempTime = time + timeAdd;
checkTempTime();
} else {//否则就是反向移动,要快退了
length = 0 - length;//取它的正数
Sentence now = list.get(currentIndex);
int nowWidth = now.getContentWidth(g);
float f = (time - now.getFromTime()) * 1.0f / (now.getToTime() - now.getFromTime());
//先算出当前的这一句已经用了多少长度了
int rest = (int) (f * nowWidth);
long timeAdd = 0;//要加多少时间
//如果剩下的长度足够了,那是最好,马上就可以返回了
if (rest > length) {
timeAdd = now.getTimeH(length, g);
} else {
timeAdd = now.getTimeH(rest, g);
for (int i = currentIndex; i > 0; i--) {
Sentence sen = list.get(i);
int len = sen.getContentWidth(g);
//如果加上下一句的长度还不够,就把时间再加,继续下一句
if (len + rest < length) {
timeAdd += sen.getDuring();
rest += len;
} else {
timeAdd += sen.getTimeH(length - rest, g);
break;
}
}
}
tempTime = time - timeAdd;
checkTempTime();
}
}
/**
* 竖直移动多少个象素,这个方法是给面板调用的
* 移动了这些象素以后,要马上算出这个象素所
* 对应的时间是多少,要注意时间超出的情况
* @param length
* @param g 画笔,因为对于每一个画笔长度不一样的
*/
public void moveV(int length, Graphics g) {
if (list.size() == 1 || !enabled) {
return;
}
//如果长度是大于0的,则说明是正向移动,快进
if (length > 0) {
Sentence now = list.get(currentIndex);
int nowHeight = now.getContentHeight(g);
float f = (time - now.getFromTime()) * 1.0f / (now.getToTime() - now.getFromTime());
//先算出当前的这一句还剩多少长度了
int rest = (int) ((1 - f) * nowHeight);
long timeAdd = 0;//要加多少时间
//如果剩下的长度足够了,那是最好,马上就可以返回了
if (rest > length) {
timeAdd = now.getTimeV(length, g);
} else {
timeAdd = now.getTimeV(rest, g);
for (int i = currentIndex; i < list.size(); i++) {
Sentence sen = list.get(i);
int len = sen.getContentHeight(g);
//如果加上下一句的长度还不够,就把时间再加,继续下一句
if (len + rest < length) {
timeAdd += sen.getDuring();
rest += len;
} else {
timeAdd += sen.getTimeV(length - rest, g);
break;
}
}
}
tempTime = time + timeAdd;
checkTempTime();
} else {//否则就是反向移动,要快退了
length = 0 - length;//取它的正数
Sentence now = list.get(currentIndex);
int nowHeight = now.getContentHeight(g);
float f = (time - now.getFromTime()) * 1.0f / (now.getToTime() - now.getFromTime());
//先算出当前的这一句已经用了多少长度了
int rest = (int) (f * nowHeight);
long timeAdd = 0;//要加多少时间
//如果剩下的长度足够了,那是最好,马上就可以返回了
if (rest > length) {
timeAdd = now.getTimeV(length, g);
} else {
timeAdd = now.getTimeV(rest, g);
for (int i = currentIndex; i > 0; i--) {
Sentence sen = list.get(i);
int len = sen.getContentHeight(g);
//如果加上下一句的长度还不够,就把时间再加,继续下一句
if (len + rest < length) {
timeAdd += sen.getDuring();
rest += len;
} else {
timeAdd += sen.getTimeV(length - rest, g);
break;
}
}
}
tempTime = time - timeAdd;
checkTempTime();
}
}
/**
* 是否能拖动,只有有歌词才可以被拖动,否则没有意义了
* @return 能否拖动
*/
public boolean canMove() {
return list.size() > 1 && enabled;
}
/**
* 得到当前的时间,一般是由显示面板调用的
*/
public long getTime() {
return tempTime;
}
/**
* 在对tempTime做了改变之后,检查一下它的
* 值,看是不是在有效的范围之内
*/
private void checkTempTime() {
if (tempTime < 0) {
tempTime = 0;
} else if (tempTime > during) {
tempTime = during;
}
}
/**
* 告诉歌词,要开始移动了,
* 在此期间,所有对歌词的直接的时间设置都不理会
*/
public void startMove() {
isMoving = true;
}
/**
* 告诉歌词拖动完了,这个时候的时间改
* 变要理会,并做更改
*/
public void stopMove() {
isMoving = false;
}
public static void main(String[] args) {
}
}
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.hadeslee.yoyoplayer.lyric;
import com.hadeslee.yoyoplayer.util.Config;
import com.hadeslee.yoyoplayer.util.Util;
import java.awt.Color;
import java.awt.Graphics;
import java.io.Serializable;
/**
* 一个用来表示每一句歌词的类
* 它封装了歌词的内容以及这句歌词的起始时间
* 和结束时间,还有一些实用的方法
* @author hadeslee
*/
public class Sentence implements Serializable {
private static final long serialVersionUID = 20071125L;
private long fromTime;//这句的起始时间,时间是以毫秒为单位
private long toTime;//这一句的结束时间
private String content;//这一句的内容
private final static long DISAPPEAR_TIME = 1000L;//歌词从显示完到消失的时间
public Sentence(String content, long fromTime, long toTime) {
this.content = content;
this.fromTime = fromTime;
this.toTime = toTime;
}
public Sentence(String content, long fromTime) {
this(content, fromTime, 0);
}
public Sentence(String content) {
this(content, 0, 0);
}
public long getFromTime() {
return fromTime;
}
public void setFromTime(long fromTime) {
this.fromTime = fromTime;
}
public long getToTime() {
return toTime;
}
public void setToTime(long toTime) {
this.toTime = toTime;
}
/**
* 检查某个时间是否包含在某句中间
* @param time 时间
* @return 是否包含了
*/
public boolean isInTime(long time) {
return time >= fromTime && time <= toTime;
}
/**
* 得到这一句的内容
* @return 内容
*/
public String getContent() {
return content;
}
/**
* 得到V方向的增量
* @param time 时间
* @return 增量
*/
public int getVIncrease(Graphics g, long time) {
int height = getContentHeight(g);
return (int) ((height + Config.getConfig().getV_SPACE()) * ((time - fromTime) * 1.0 / (toTime - fromTime)));
}
/**
* 得到H向方的增量
* @param time 时间
* @return 增时
*/
public int getHIncrease(Graphics g, long time) {
int width = getContentWidth(g);
return (int) ((width + Config.getConfig().getH_SPACE()) * ((time - fromTime) * 1.0 / (toTime - fromTime)));
}
/**
* 得到内容的宽度
* @param g 画笔
* @return 宽度
*/
public int getContentWidth(Graphics g) {
return (int) g.getFontMetrics().getStringBounds(content, g).getWidth();
}
/**
* 得到这个句子的时间长度,毫秒为单位
* @return 长度
*/
public long getDuring() {
return toTime - fromTime;
}
/**
* 移动这些距离来说,对于这个句子
* 花了多少的时间
* @param length 要移动的距离
* @param g 画笔
* @return 时间长度
*/
public long getTimeH(int length, Graphics g) {
return getDuring() * length / getContentWidth(g);
}
/**
* 对于竖直方向的移动这些象素所代表的时间
* @param length 距离的长度
* @param g 画笔
* @return 时间长度
*/
public long getTimeV(int length, Graphics g) {
return getDuring() * length / getContentHeight(g);
}
/**
* 得到内容的高度
* @param g 画笔
* @return 高度
*/
public int getContentHeight(Graphics g) {
return (int) g.getFontMetrics().getStringBounds(content, g).getHeight() + Config.getConfig().getV_SPACE();
}
/**
* 根据当前指定的时候,得到这个时候应该
* 取渐变色的哪个阶段了,目前的算法是从
* 快到结束的五分之一处开始渐变,这样平缓一些
* @param c1 高亮色
* @param c2 普通色
* @param time 时间
* @return 新的颜色
*/
public Color getBestInColor(Color c1, Color c2, long time) {
float f = (time - fromTime) * 1.0f / getDuring();
if (f > 0.1f) {//如果已经过了十分之一的地方,就直接返高亮色
return c1;
} else {
long dur = getDuring();
f = (time - fromTime) * 1.0f / (dur * 0.1f);
if (f > 1 || f < 0) {
return c1;
}
return Util.getGradientColor(c2, c1, f);
}
}
/**
* 得到最佳的渐出颜色
* @param c1
* @param c2
* @param time
* @return
*/
public Color getBestOutColor(Color c1, Color c2, long time) {
if (isInTime(time)) {
return c1;
}
float f = (time - toTime) * 1.0f / DISAPPEAR_TIME;
if (f > 1f || f <= 0) {//如果时间已经超过了最大的时间了,则直接返回原来的颜色
return c2;
} else {
return Util.getGradientColor(c1, c2, f);
}
}
public String toString() {
return "{" + fromTime + "(" + content + ")" + toTime + "}";
}
}
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.hadeslee.yoyoplayer.lyric;
import com.hadeslee.yoyoplayer.util.Config;
import com.hadeslee.yoyoplayer.util.Playerable;
import com.hadeslee.yoyoplayer.util.Util;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Desktop;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JDialog;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
/**
*
* @author hadeslee
*/
public class LyricPanel extends JPanel implements Runnable, DropTargetListener,
MouseListener, MouseWheelListener, MouseMotionListener {
private static final long serialVersionUID = 20071214L;
private static Logger log = Logger.getLogger(LyricPanel.class.getName());
private DropTarget dt;//一个拖放的目标
private Playerable player;//播放器
private Lyric ly;//表示此歌词面板对应的歌词对象
public static final int V = 0;//表示纵向显示
public static final int H = 1;//表示横向显示
private int state = H;//表示现在是横向还是纵向的
private volatile boolean isPress;//是已经按下,按下就就不滚动歌词了
private volatile boolean isDrag;//是否已经动过了
private int start;//开始的时候座标,在释放的时候,好计算拖了多少
private int end;//现在的座标
private volatile boolean isResized;//是否已经重设大小了
private volatile boolean pause = true;//一个循环的标量
private final Object lock = new Object();
private volatile boolean isOver;//是否手在上面
private Rectangle area = new Rectangle();
private final String logo = "作者:千里冰封";
private boolean isShowLogo = true;
private Config config;//一个全局配置对象
// private Component parent;//它是被加到谁的身上去了
public LyricPanel(Playerable pl) {
this();
this.player = pl;
this.setDoubleBuffered(true);
}
public LyricPanel() {
config = Config.getConfig();
dt = new DropTarget(this, DnDConstants.ACTION_COPY_OR_MOVE, this);
state = config.getLpState();
this.addMouseListener(this);
this.addMouseMotionListener(this);
this.addMouseWheelListener(this);
Thread th = new Thread(this);
th.setDaemon(true);
th.start();
}
public void setConfig(Config config) {
this.config = config;
}
public void setShowLogo(boolean b) {
isShowLogo = b;
}
/**
* 设置播放列表
* @param pl 播放列表
*/
public void setPlayList(Playerable pl) {
this.player = pl;
}
/**
* 设置一个新的歌词对象,此方法可能会被
* PlayList调用
* @param ly 歌词
*/
public void setLyric(Lyric ly) {
this.ly = ly;
isResized = false;
}
public void pause() {
log.log(Level.INFO, "歌词暂停显示了");
pause = true;
}
public void start() {
log.log(Level.INFO, "歌词开始显示了");
pause = false;
synchronized (lock) {
lock.notifyAll();
}
}
protected void paintComponent(Graphics g) {
Graphics2D gd = (Graphics2D) g;
if (config.isTransparency()) {
} else {
super.paintComponent(g);
gd.setColor(config.getLyricBackground());
gd.fillRect(0, 0, getWidth(), getHeight());
}
if (config.isAntiAliasing()) {
gd.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
// gd.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
}
g.setFont(config.getLyricFont());
state = config.getLpState();
if (ly != null) {
//只有要重设大小,并且没有重设大小的时候,才去设,否则就不用理它了
//并且还要不是水平显示,因为水平显示的话,宽度就没有意义了,想多宽就可以多宽
if (config.isAutoResize() && !isResized && ly.isInitDone() && state == V) {
int maxWidth = ly.getMaxWidth(g);
int inset = player.getLyricUI().getInsets().left + player.getLyricUI().getInsets().right;
JDialog win = config.getLrcWindow();
if (win != null) {
win.setSize(maxWidth + inset, win.getHeight());
isResized = true;
}
}
if (isPress && isDrag) {
if (state == H) {
ly.moveH(start - end, g);
} else {
ly.moveV(start - end, g);
}
}
if (state == H) {
ly.drawH(g);
} else {
ly.drawV(g);
}
if (isPress && isDrag) {
if (state == H) {
drawTimeH((int) (ly.getTime() / 1000), g);
} else {
drawTimeV((int) (ly.getTime() / 1000), g);
}
}
} else {
g.setColor(config.getLyricHilight());
int width = Util.getStringWidth(Config.NAME, g);
int height = Util.getStringHeight(Config.NAME, g);
Util.drawString(g, Config.NAME, (getWidth() - width) / 2, (getHeight() - height) / 2);
}
if (isShowLogo) {
drawLogo(g);
}
}
/**
* 画出自己的LOGO
* @param g 画笔
*/
private void drawLogo(Graphics g) {
g.setFont(new Font("Dialog", Font.BOLD, 14));
int width = Util.getStringWidth(logo, g);
int height = Util.getStringHeight(logo, g);
area.x = 5;
area.y = 5;
area.width = width;
area.height = height;
if (isOver) {
g.setColor(Color.RED);
} else {
Color bg = config.getLyricBackground();
int rgb = bg.getRGB();
int xor = ~rgb;
rgb = xor & 0x00ffffff;
Color c = new Color(rgb);
g.setColor(c);
}
Util.drawString(g, "作者:千里冰封", 5, 5);
}
/**
* 得到播放器对象,此方法一般是给
* 在线搜索歌词框用的
* @return 播放器
*/
public Playerable getPlayer() {
return this.player;
}
/**
* 画出正在拖动的时候的时间,以便更好的掌握进度
* 这是画出垂直方向的拖动时间
* @param sec 当前的秒数
* @param g 画笔
*/
private void drawTimeV(int sec, Graphics g) {
String s = Util.secondToString(sec);
int width = getWidth();
int height = getHeight();
int centerY = height / 2;
g.drawLine(3, centerY - 5, 3, centerY + 5);
g.drawLine(width - 3, centerY - 5, width - 3, centerY + 5);
g.drawLine(3, centerY, width - 3, centerY);
g.setFont(new Font("宋体", Font.PLAIN, 14));
g.setColor(Util.getColor(config.getLyricForeground(), config.getLyricHilight()));
Util.drawString(g, s, width - Util.getStringWidth(s, g), (height / 2 - Util.getStringHeight(s, g)));
}
/**
* 画出正在拖动的时候的时间,以便更好的掌握进度
* 这是画出水平方向的拖动时间
* @param sec 当前的秒数
* @param g 画笔
*/
private void drawTimeH(int sec, Graphics g) {
String s = Util.secondToString(sec);
int centerX = getWidth() / 2;
int height = getHeight();
g.drawLine(centerX - 5, 3, centerX + 5, 3);
g.drawLine(centerX - 5, height - 3, centerX + 5, height - 3);
g.drawLine(centerX, 3, centerX, height - 3);
g.setFont(new Font("宋体", Font.PLAIN, 14));
g.setColor(Util.getColor(config.getLyricForeground(), config.getLyricHilight()));
Util.drawString(g, s, centerX, (height - Util.getStringHeight(s, g)));
}
public void run() {
while (true) {
try {
Thread.sleep(config.getRefreshInterval());
if (pause) {
synchronized (lock) {
lock.wait();
}
} else {
if (ly != null) {
ly.setHeight(this.getHeight());
ly.setWidth(this.getWidth());
ly.setTime(player.getTime());
repaint();
}
}
} catch (Exception exe) {
exe.printStackTrace();
}
}
}
public void dragEnter(DropTargetDragEvent dtde) {
dtde.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
}
public void dragOver(DropTargetDragEvent dtde) {
}
public void dropActionChanged(DropTargetDragEvent dtde) {
}
public void dragExit(DropTargetEvent dte) {
}
public void drop(DropTargetDropEvent e) {
try {
//得到操作系统的名字,如果是windows,则接受的是DataFlavor.javaFileListFlavor
//如果是linux则接受的是DataFlavor.stringFlavor
String os = System.getProperty("os.name");
if (os.startsWith("Windows")) {
if (e.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
Transferable tr = e.getTransferable();
e.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
@SuppressWarnings("unchecked")
java.util.List<File> s = (java.util.List<File>) tr.getTransferData(
DataFlavor.javaFileListFlavor);
if (s.size() == 1) {
File f = s.get(0);
if (f.isFile() && player.getCurrentItem() != null) {
ly = new Lyric(f, player.getCurrentItem());
ly.setWidth(this.getWidth());
ly.setHeight(this.getHeight());
player.setLyric(ly);
}
}
e.dropComplete(true);
}
} else if (os.startsWith("Linux")) {
if (e.isDataFlavorSupported(DataFlavor.stringFlavor)) {
e.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
Transferable tr = e.getTransferable();
String[] ss = tr.getTransferData(DataFlavor.stringFlavor).toString().split("\r\n");
if (ss.length == 1) {
File f = new File(new URI(ss[0]));
if (f.isFile() && player.getCurrentItem() != null) {
ly = new Lyric(f, player.getCurrentItem());
ly.setWidth(this.getWidth());
ly.setHeight(this.getHeight());
player.setLyric(ly);
}
}
e.dropComplete(true);
}
} else {
e.rejectDrop();
}
} catch ( Exception io) {
io.printStackTrace();
e.rejectDrop();
}
}
public void setState(int state) {
if (state == H || state == V) {
this.state = state;
}
}
public void setResized(boolean b) {
isResized = b;
}
public void mouseClicked(MouseEvent e) {
// //双击的时候,改变显示风格
// if (e.getClickCount() == 2) {
// if (state == H) {
// state = V;
// } else {
// state = H;
// }
// }
}
public void mousePressed(MouseEvent e) {
if (ly == null) {
return;
}
if (e.getButton() == MouseEvent.BUTTON1) {
if (area != null && area.contains(e.getPoint())) {
try {
Desktop.getDesktop().browse(new URI("http://www.blogjava.net/hadeslee"));
} catch (URISyntaxException ex) {
Logger.getLogger(LyricPanel.class.getName()).log(Level.SEVERE, null, ex);
} catch (IOException ex) {
Logger.getLogger(LyricPanel.class.getName()).log(Level.SEVERE, null, ex);
}
}
if (ly != null && ly.canMove()) {
isPress = true;
isDrag = false;
if (state == V) {
start = e.getY();
} else {
start = e.getX();
}
ly.startMove();
}
}
}
public void mouseReleased(MouseEvent e) {
if (ly == null) {
return;
}
//如果是鼠标左键
if (e.getButton() == MouseEvent.BUTTON1) {
if (ly.canMove() && isDrag) {
if (state == H) {
end = e.getX();
} else {
end = e.getY();
}
long time = ly.getTime();
player.setTime(time);
start = end = 0;
}
ly.stopMove();
isPress = false;
isDrag = false;
//如果是鼠标右键
} else if (e.getButton() == MouseEvent.BUTTON3) {
if (player.getCurrentItem() == null) {
return;
}
JPopupMenu pop = new JPopupMenu();
Util.generateLyricMenu(pop, this);
pop.show(this, e.getX(), e.getY());
}
}
/**
* 隐藏自己
*/
public void hideMe() {
player.setShowLyric(false);
}
public Lyric getLyric() {
return ly;
}
public void mouseEntered(MouseEvent e) {
if (ly != null && ly.canMove()) {
this.setCursor(new Cursor(Cursor.HAND_CURSOR));
} else {
this.setCursor(Cursor.getDefaultCursor());
}
}
public void mouseExited(MouseEvent e) {
this.setCursor(Cursor.getDefaultCursor());
isOver = false;
}
public void mouseWheelMoved(MouseWheelEvent e) {
if (ly == null) {
return;
}
//只有当配置允许鼠标滚动调整时间才可以
if (config.isMouseScrollAjustTime()) {
int adjust = e.getUnitsToScroll() * 100;//每转动一下,移动300毫秒
ly.adjustTime(adjust);
}
}
public void mouseDragged(MouseEvent e) {
if (ly == null) {
return;
}
if (ly.canMove() && isPress) {
isDrag = true;
if (state == H) {
end = e.getX();
} else {
end = e.getY();
}
}
}
public void mouseMoved(MouseEvent e) {
if (area != null && area.contains(e.getPoint())) {
isOver = true;
this.setCursor(new Cursor(Cursor.HAND_CURSOR));
} else {
isOver = false;
mouseEntered(e);
}
}
}
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.hadeslee.yoyoplayer.lyric;
import com.hadeslee.yoyoplayer.util.Config;
import com.hadeslee.yoyoplayer.util.MultiImageBorder;
import com.hadeslee.yoyoplayer.util.Playerable;
import com.hadeslee.yoyoplayer.util.Util;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Insets;
import javax.swing.JPanel;
/**
*
* @author hadeslee
*/
public class LyricUI extends JPanel {
private static final long serialVersionUID = 20071214L;
private Config config;
private LyricPanel lp;//一个实际显示歌词的面板
private Playerable player;
private MultiImageBorder border;//即是边界,又是监听器
public LyricUI() {
super(new BorderLayout());
this.setPreferredSize(new Dimension(285, 465));
this.setMinimumSize(new Dimension(285, 50));
}
public void setPlayer(Playerable player) {
this.player = player;
}
public void setParent(Component parent){
border.setParent(parent);
}
public void setBorderEnabled(boolean b){
if(b){
this.setBorder(border);
}else{
this.setBorder(null);
}
}
public void loadUI(Component parent, Config config) {
this.config = config;
border = new MultiImageBorder(parent, config);
border.setCorner1(Util.getImage("lyric/corner1.png"));
border.setCorner2(Util.getImage("playlist/corner2.png"));
border.setCorner3(Util.getImage("playlist/corner3.png"));
border.setCorner4(Util.getImage("playlist/corner4.png"));
border.setTop(Util.getImage("playlist/top.png"));
border.setBottom(Util.getImage("playlist/bottom.png"));
border.setLeft(Util.getImage("playlist/left.png"));
border.setRight(Util.getImage("playlist/right.png"));
this.setBorder(border);
this.addMouseListener(border);
this.addMouseMotionListener(border);
lp = new LyricPanel(player);
lp.setConfig(config);
this.add(lp, BorderLayout.CENTER);
}
public void setShowLogo(boolean b) {
lp.setShowLogo(b);
}
/**
* 设置播放列表
* @param pl 播放列表
*/
public void setPlayList(Playerable pl) {
lp.setPlayList(player);
}
public LyricPanel getLyricPanel() {
return lp;
}
/**
* 设置一个新的歌词对象,此方法可能会被
* PlayList调用
* @param ly 歌词
*/
public void setLyric(Lyric ly) {
lp.setLyric(ly);
}
public void pause() {
lp.pause();
}
public void start() {
lp.start();
}
}
几乎每个方法都有注释,如果还有什么不太清楚的地方,可以留言,大家共同探讨这方面的话题,以让歌词显示的效率更高一些.
尽管千里冰封
依然拥有晴空
你我共同品味JAVA的浓香.
posted on 2008-01-10 21:52
千里冰封 阅读(7957)
评论(15) 编辑 收藏 所属分类:
JAVASE