现在的位置: 首页 > 综合 > 正文

Java Swing制作多行滚动歌词显示控件

2013年10月28日 ⁄ 综合 ⁄ 共 12228字 ⁄ 字号 评论关闭

首先上图一张,为最终制作的效果图,不喜欢或感到失望的朋友可以先行离开

大家已经看到效果图了。那么下面就介绍设计思路和源代码

首先要想显示歌词,就要对歌词文件进行抽象。下面这个类是对某一行歌词文件进行了抽象。

/*
 * 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<LyricStatement>。为了方便测试,以下附上本人自己写的一个歌词文件解释器。

/*
 * 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<LyricStatement> statements = new ArrayList<LyricStatement>();		//歌词
    /* 
     * 实例化一个歌词数据. 歌词数据信息由指定的文件提供. 
     * 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<LyricStatement> 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<str.length; ++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));
//		}
		/*
	 * 测试排序算法
	 */
//		{
//			Vector<Double> vect=new Vector<Double>();
//			vect.add(5.0);
//			vect.add(28.0);
//			vect.add(37.0);
//			vect.add(10.0);
//			vect.add(25.0);
//			vect.add(40.0);
//			vect.add(27.0);
//			vect.add(35.0);
//			vect.add(70.0);
//			vect.add(99.0);
//			vect.add(100.0);
//			
//			for(int i=0;i<vect.size();++i)
//			{
//				System.out.println(vect.elementAt(i));
//			}
//			
//			for(int i=0;i<vect.size()-1;++i)
//			{
//				int index=i;
//				double delta=Double.MAX_VALUE;
//				boolean moveFlag = false;
//				for(int j=i+1;j<vect.size();++j)
//				{
//					double sub;
//					if(0>=(sub=vect.get(i)-vect.get(j)))
//					{
//						continue;
//					}
//					moveFlag=true;
//					if(sub<delta)
//					{
//						delta=sub;
//						index=j+1;
//					}
//				}
//				if(moveFlag)
//				{
//					vect.add(index, vect.elementAt(i));
//					vect.remove(i);	
//					System.out.println("第"+i);
//					--i;
//				}
//			}
//
//			System.out.println("排序后");
//			for(int i=0;i<vect.size();++i)
//			{
//				System.out.println(vect.elementAt(i));
//			}
//		}

	/*
	 * 测试由字符串转化为双精度时间
	 */
//		{
//			String stime="02:03.09";
//			String str[] = stime.split(":|\\.");
//			for(int i=0;i<str.length;++i)
//			{
//				System.out.print("时间"+str[i]+":");
//			}
//			double dtime = Integer.parseInt(str[0])*60+Integer.parseInt(str[1])+Integer.parseInt(str[2])*0.01;
//			System.out.println("time="+dtime);
//		}

	/*
	 * 测试整个类
	 */
	{

	    LyricReader ld = new LyricReader("D:\\music\\海盗.lrc");				//路径\\输入文件名
	    ld.printLrcDate();
	}
    }
}

有了歌词解释器和一个歌词列表,下面就可以进行歌词显示控件的设计了。

由于在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<LyricStatement> 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<LyricStatement> 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,直接把它刷到屏幕上来。

抱歉!评论已关闭.