首先上图一张,为最终制作的效果图,不喜欢或感到失望的朋友可以先行离开
大家已经看到效果图了。那么下面就介绍设计思路和源代码
首先要想显示歌词,就要对歌词文件进行抽象。下面这个类是对某一行歌词文件进行了抽象。
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package musicbox.model.lyric;
/**
*
* @author Randyzhao
*/
public class LyricStatement {
private long time = 0;//时间, 单位为10ms
private String lyric = "";//歌词
/*
* 获取时间
*/
public long getTime() {
return time;
}
/*
* 设置时间
* time: 被设置成的时间
*/
public void setTime(int time) {
this.time = time;
}
/*
* 设置时间
* time: 被设置成的时间字符串, 格式为mm:ss.ms
*/
public void setTime(String time) {
String str[] = time.split(":|\\.");
this.time = Integer.parseInt(str[0]) * 6000 + Integer.parseInt(str[1]) * 100 +
Integer.parseInt(str[2]);
}
/*
* 获取歌词
*/
public String getLyric() {
return lyric;
}
/*
* 设置歌词
*/
public void setLyric(String lyric) {
this.lyric = lyric;
}
/*
* 打印歌词
*/
public void printLyric() {
System.out.println(time + ": " + lyric);
}
}
特别注意成员变量time表示该行歌词显示的时间,单位是 10ms 这是为了和歌词文件中时间的单位统一。
某一行歌词可以用一个LyricStatement类的实例来表示。那么一个歌词文件就可以解析为一个List。为了方便测试,以下附上本人自己写的一个歌词文件解释器。
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package musicbox.model.lyric;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
* @author Randyzhao
*/
public class LyricReader {
BufferedReader bufferReader = null;//读取文件实例
public String title = "";//歌曲题目
public String artist = "";//演唱者
public String album = "";//专辑
public String lrcMaker = "";//歌词制作者
List statements = new ArrayList();//歌词
/*
* 实例化一个歌词数据. 歌词数据信息由指定的文件提供.
* fileName: 指定的歌词文件.
*/
public LyricReader(String fileName) throws IOException {
//in case the space in the fileName is replaced by the %20
FileInputStream file = new FileInputStream(URLDecoder.decode(fileName, "UTF-8"));
bufferReader = new BufferedReader(new InputStreamReader(file, "GB2312"));
//将文件数据读入内存
readData();
}
public List getStatements() {
return statements;
}
/*
* 读取文件中数据至内存.
*/
private void readData() throws IOException {
statements.clear();
String strLine;
//循环读入所有行
while (null != (strLine = bufferReader.readLine())) {
//判断该行是否为空行
if ("".equals(strLine.trim())) {
continue;
}
//判断该行数据是否表示歌名
if (null == title || "".equals(title.trim())) {
Pattern pattern = Pattern.compile("\\[ti:(.+?)\\]");
Matcher matcher = pattern.matcher(strLine);
if (matcher.find()) {
title = matcher.group(1);
continue;
}
}
//判断该行数据是否表示演唱者
if (null == artist || "".equals(artist.trim())) {
Pattern pattern = Pattern.compile("\\[ar:(.+?)\\]");
Matcher matcher = pattern.matcher(strLine);
if (matcher.find()) {
artist = matcher.group(1);
continue;
}
}
//判断该行数据是否表示专辑
if (null == album || "".equals(album.trim())) {
Pattern pattern = Pattern.compile("\\[al:(.+?)\\]");
Matcher matcher = pattern.matcher(strLine);
if (matcher.find()) {
album = matcher.group(1);
continue;
}
}
//判断该行数据是否表示歌词制作者
if (null == lrcMaker || "".equals(lrcMaker.trim())) {
Pattern pattern = Pattern.compile("\\[by:(.+?)\\]");
Matcher matcher = pattern.matcher(strLine);
if (matcher.find()) {
lrcMaker = matcher.group(1);
continue;
}
}
//读取并分析歌词
int timeNum = 0;//本行含时间个数
String str[] = strLine.split("\\]");//以]分隔
for (int i = 0; i < str.length; ++i) {
String str2[] = str[i].split("\\[");//以[分隔
str[i] = str2[str2.length - 1];
if (isTime(str[i])) {
++timeNum;
}
}
for (int i = 0; i < timeNum; ++i) //处理歌词复用的情况
{
LyricStatement sm = new LyricStatement();
sm.setTime(str[i]);
if (timeNum < str.length) //如果有歌词
{
sm.setLyric(str[str.length - 1]);
}
statements.add(sm);
}
//if(1==str.length)//处理没有歌词的情况
//{
//Statement sm = new Statement();
//sm.setTime(str[0]);
//sm.setLyric("");
//statements.add(sm);
//}
}
//将读取的歌词按时间排序
sortLyric();
}
/*
* 判断给定的字符串是否表示时间.
*/
private boolean isTime(String string) {
String str[] = string.split(":|\\.");
if (3 != str.length) {
return false;
}
try {
for (int i = 0; i < str.length; ++i) {
Integer.parseInt(str[i]);
}
} catch (NumberFormatException e) {
return false;
}
return true;
}
/*
* 将读取的歌词按时间排序.
*/
private void sortLyric() {
for (int i = 0; i < statements.size() - 1; ++i) {
int index = i;
double delta = Double.MAX_VALUE;
boolean moveFlag = false;
for (int j = i + 1; j < statements.size(); ++j) {
double sub;
if (0 >= (sub = statements.get(i).getTime() - statements.get(j).getTime())) {
continue;
}
moveFlag = true;
if (sub < delta) {
delta = sub;
index = j + 1;
}
}
if (moveFlag) {
statements.add(index, statements.get(i));
statements.remove(i);
--i;
}
}
}
/*
* 打印整个歌词文件
*/
private void printLrcDate() {
System.out.println("歌曲名: " + title);
System.out.println("演唱者: " + artist);
System.out.println("专辑名: " + album);
System.out.println("歌词制作: " + lrcMaker);
for (int i = 0; i < statements.size(); ++i) {
statements.get(i).printLyric();
}
}
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
/*
* 测试"[", "]"的ASCII码
*/
//{
//char a='[', b = ']';
//int na = (int)a;
//int nb = (int)b;
//System.out.println("a="+na+", b="+nb+"\n");
//}
/*
* 测试匹配[]. 注: [应用\[表示. 同理]应用\]表示.
*/
//{
//String strLyric = "[02:13.41][02:13.42][02:13.43]错误的泪不想哭却硬要留住";
//String str[] = strLyric.split("\\]");
//for(int i=0; i
//{
//String str2[] = str[i].split("\\[");
//str[i] = str2[str2.length-1];
//System.out.println(str[i]+" ");
//}
//}
/*
* 测试匹配[ti:]. 注: [应用\[表示. 同理]应用\]表示.
*/
//{
//String strLyric = "[ti:Forget]";
//Pattern pattern = Pattern.compile("\\[ti:(.+?)\\]");
//Matcher matcher = pattern.matcher(strLyric);
//if(matcher.find())
// System.out.println(matcher.group(1));
//}
/*
* 测试排序
有了歌词解释器和一个歌词列表,下面就可以进行歌词显示控件的设计了。
由于在Swing框架中设计歌词显示控件,那么最好的选择就是继承一个JPanel控件。当需要刷新屏幕上歌词的时候将多行歌词绘制在一个Image上面,然后重写paint函数。
以下是程序代码。
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package musicbox.view;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.JPanel;
import musicbox.model.lyric.LyricStatement;
/**
* Used to display the lyric
* @author Randyzhao
*/
public class LyricDisplayer extends JPanel {
protected final Color CURRENT_LINE_COLOR = Color.green;
protected final Color OTHER_LINE_COLOR = Color.GRAY;
//the lines other than the current line to be displayed
protected final int UP_DOWN_LINES = 8;
//the list of lyric statements to be displayed
protected List statements;
//the index of next statement to be dispalyed in the statements
protected int index;
protected Image backgroundImage = null;
private String backGroundImagePath = null;
protected Image bufferImage = null;
//the size when the bufferImage is drawn
private Dimension bufferedSize;
public String getBackGroundImagePath() {
return backGroundImagePath;
}
public void setBackGroundImagePath(String backGroundImagePath) {
this.backGroundImagePath = backGroundImagePath;
}
/**
* get ready to display
* @param statements
*/
public void prepareDisplay(List statements) {
this.statements = statements;
this.index = -1;
this.setFont(new Font("微软雅黑", Font.PLAIN, 20));
}
/**
* display a lyric by the index
* @param index
*/
public void displayLyric(int index) {
this.index = index;
this.drawBufferImage();
//System.out.println("draw " + index + " " + this.statements.get(index).getLyric());
this.paint(this.getGraphics());
}
/**
* draw a line of lyric in the middle of the Graphics2D
* @param lyric
* @param g2d
*/
protected void drawLineInMiddle(int height, String lyric, Graphics2D g2d, Color color) {
int width = this.getWidth();
FontMetrics fm = g2d.getFontMetrics();
g2d.setColor(color);
int x = (this.getWidth() - fm.stringWidth(lyric)) / 2;
g2d.drawString(lyric, x, height);
}
/**
* Draw the buffered image. Used to realize the double-buffering.
*/
protected void drawBufferImage() {
Image tempBufferedImage = this.createImage(this.getWidth(), this.getHeight());
this.bufferedSize = this.getSize();
if (this.backgroundImage == null) {
//get background image
URL url = getClass().getResource(this.backGroundImagePath);
try {
backgroundImage = ImageIO.read(url);
//缩放图片
backgroundImage = backgroundImage.getScaledInstance(this.getWidth(), this.getHeight(), 20);
} catch (IOException ex) {
ex.printStackTrace();
}
}
Graphics2D g2d = (Graphics2D) tempBufferedImage.getGraphics();
g2d.setFont(new Font("楷体", Font.PLAIN, 25));
g2d.drawImage(this.backgroundImage, 0, 0, this.getWidth(), this.getHeight(), null);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
if (this.statements != null && this.statements.size() != 0) {
//draw current line
g2d.setFont(new Font("楷体", Font.PLAIN, 35));
this.drawLineInMiddle(this.getHeight() / 2,
this.statements.get(index).getLyric(), g2d, this.CURRENT_LINE_COLOR);
int perHeight = g2d.getFontMetrics().getHeight() + 5;
g2d.setFont(new Font("楷体", Font.PLAIN, 25));
//draw down lines
for (int i = index - UP_DOWN_LINES; i < index; i++) {
if (i < 0) {
continue;
}
if (index - i > UP_DOWN_LINES / 2) {
//set transparance
float ratio = (float) (i - index + UP_DOWN_LINES) / (UP_DOWN_LINES / 2) / 1.2f;
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
ratio));
} else {
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
1.0f));
}
this.drawLineInMiddle(this.getHeight() / 2 - (index - i) * perHeight,
this.statements.get(i).getLyric(), g2d, this.OTHER_LINE_COLOR);
}
//draw up lines
for (int i = index + 1; i < index + UP_DOWN_LINES; i++) {
if (i >= this.statements.size()) {
break;
}
if (i - index > UP_DOWN_LINES / 2) {
//set transparance
float ratio = (float) (index + UP_DOWN_LINES - i) / (UP_DOWN_LINES / 2) / 1.2f;
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
ratio));
} else {
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
1.0f));
}
this.drawLineInMiddle(this.getHeight() / 2 + (i - index) * perHeight,
this.statements.get(i).getLyric(), g2d, this.OTHER_LINE_COLOR);
}
} else {
//statements is empty
this.drawLineInMiddle(this.getHeight() / 2,
"未找到相应的歌词文件", g2d, this.CURRENT_LINE_COLOR);
}
//copyt the buffered image
this.bufferImage = tempBufferedImage;
}
/**
* This method is override in order to display the lyric in the panel
* @param g
*/
@Override
public void paint(Graphics g) {
if (this.isVisible() == false) {
return;
}
super.paint(g);
//draw buffered image
if (this.bufferImage == null || this.getWidth() != this.bufferedSize.getWidth()
|| this.getHeight() != this.bufferedSize.getHeight()) {
this.drawBufferImage();
}
//copy the double buffer
g.drawImage(bufferImage, 0, 0, null);
}
}
下面进行简单的解释。
当LyricDisplayer的实例初始化之后,外部代码应该调用它的prepareDisplay函数。告诉它显示的歌词列表,调用setBackGroundImagePath函数,告诉它歌词背景图片所在的位置。
之后当需要显示某一句歌词的时候,调用displayLyric函数,参数是prepareDisplay函数参数中歌词列表对应歌词的index。此时LyricDisplayer实例会调用自己的drawBufferImage函数来重新绘制Image。
在绘制的时候,
if (this.backgroundImage == null) {
//get background image
URL url = getClass().getResource(this.backGroundImagePath);
try {
backgroundImage = ImageIO.read(url);
//缩放图片
backgroundImage = backgroundImage.getScaledInstance(this.getWidth(), this.getHeight(), 20);
} catch (IOException ex) {
ex.printStackTrace();
}
}
这段代码用于从硬盘中读取背景文件并缩放至JPanel的大小。如果JPanel大小没有变化,而且之前已经初始化过背景图片,那么不要重复初始化。
之后主要就是应用Graphics2D中的drawString函数来将一个字符串绘制在Image上面。
FontMetrics fm = g2d.getFontMetrics();
上面这语句初始化一个FontMerics对象,可以调用它的stringWidth函数来计算它对应的graphics2D对象中的一行字的高度,方便你计算绘制的位置。
在调用drawString函数之前,你可以调用setComposite函数,如以下代码
float ratio = (float) (index + UP_DOWN_LINES - i) / (UP_DOWN_LINES / 2) / 1.2f;
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
ratio));
这样可以设置接下来绘制的字符串的透明度,这样就实现了淡入淡出效果。
绘制完Image后调用paint函数将Image刷到屏幕上。这样的设计相当于实现了一个双缓冲。如果直接在JPanle上绘制那么屏幕一定会闪。
在paint函数中
if (this.bufferImage == null || this.getWidth() != this.bufferedSize.getWidth()
|| this.getHeight() != this.bufferedSize.getHeight()) {
this.drawBufferImage();
}
这句话是判断如果原来已经绘制过Image并且JPanel尺寸和绘制Image的时候相比没有改变,那么不用重新绘制Image,直接把它刷到屏幕上来。