最近做了一个android的音乐播放器,下面将介绍歌词同步显示的实现。网上关于这一块的参考实例,多数人推荐了Yoyoplayer。YOYOPlayer是一个用JAVA编写的,跨平台的音乐播放软件.集播放,歌词显示于一体,并且开放源代码,感兴趣的朋友可以自行google。
一、LRC歌词文件的解析
先要了解LRC文件的格式,可以参考:http://baike.baidu.com/view/80650.htm。总体思路是这样的,按行读入歌词文本,忽略每行中的注释,即“[:]”后的内容;再解析标识标签(ID-tags);最后解析出时间标签及其对应的歌词语句。具体实现如下:
1、忽略注释
private String removeComment(String line) { Matcher m = Pattern.compile("\\[:\\]").matcher(line); String str = line; while (m.find()) { if (m.start() == 0)//整行都是注释 str = null; else str = line.substring(0, m.start()); } return str; }
2、解析标识标签
private void getLyricIdTags(String line) { Matcher m = Pattern.compile("\\[((ti)|(ar)|(al)|(by)):.+\\]") .matcher(line); while (m.find()) { String type = m.group().substring(1, 3); if (type.equals("ti")) { title = m.group().substring(4, m.end()-1); } else if (type.equals("ar")) { artist = m.group().substring(4, m.end()-1); } else if (type.equals("al")) { album = m.group().substring(4, m.end()-1); } else if (type.equals("by")) { author = m.group().substring(4, m.end()-1); } } }
3、解析时间标签
这里我用了map结构来存储歌词开始时间及歌词语句的配对关系,value为歌词语句,key值为该歌词开始时间。这里用了一个正则表达式来匹配时间标签,支持[ : ] 、[ : : ] 、[ : . ]三种格式的时间标签。getTimeOfLine()函数的作用是将时间标签转换成以毫秒为单位的时间量。
private void getLyricTimeTags(String line) { Matcher m = Pattern.compile("\\[\\d{1,2}:\\d{1,2}([\\.:]\\d{1,2})?\\]").matcher(line); List<Integer> time = new ArrayList<Integer>(); int begIndex = 0; while (m.find()) { time.add(getTimeOfLine(m.group())); begIndex = m.end(); } for (int i : time) { sortedLyric.put(i, line.substring(begIndex, line.length())); } }
二、歌词语句分行
LRC文件中的单句歌词可能过长,在android屏幕的给定区域中未必能单行显示,从而需要依据屏幕歌词显示区域的宽度进行断句分行。其中寻找分割点的实现如下:
private int getDividePoint(String str, int begIndex, int endIndex) { //not to divide (letter,letter) or (digit,digit) int original = endIndex; while ( (charType(str.charAt(endIndex)) == LETTER && charType(str.charAt(endIndex-1)) == LETTER) || ( charType(str.charAt(endIndex)) == DIGIT && charType(str.charAt(endIndex-1)) == DIGIT) ) { if (endIndex > begIndex) endIndex--; else { endIndex = original; break; } } return endIndex; }
分割过程如下,其中tp为android中TextPaint类型的对象,TextPaint用于在view中绘制文本,它提供了一个测定文本宽度的函数measureText():
int begIndex = 0; for (int i = 0; i <= line.length(); ++i) { String str = line.substring(begIndex, i); if (tp.measureText(str) > width) { int dividePoint = getDividePoint(line, begIndex, i-1); lines.add(line.substring(begIndex, dividePoint)); begIndex = dividePoint; } if (i == line.length())////添加分行结果的最后一部分 lines.add(line.substring(begIndex, i)); }
三、歌词的同步显示
要把歌词同步滚动地显示在手机屏幕上,需要自定义view,这里我选择继承自TextView。主要是重写TextView中的onDrow()函数。主要实现思路是先绘制当前正在播放的歌词,然后绘制当前歌词之上的歌词和当前歌词之下的歌词(指在屏幕上显示的位置)。根据歌曲的播放进度,不断重绘歌词以达到同步滚动效果。
Bundle currentLine = lyric.getLineFromTime(currentTime); float offset = calOffsetOfCurrentLine(); Bundle bundle = drawCurrentLine(canvas, offset, height, currentLine.getStringArrayList("Lines")); drawAboveLines(canvas, bundle.getFloat("PositionAbove"), lineWidth, lyric.valuesOfHeadMap(currentTime, false)); drawBelowLines(canvas, bundle.getFloat("PositionBelow"), lineWidth, height, lyric.valuesOfTailMap(currentTime, false));
其中,函数getLineFromTime()根据当前播放进度获取应该显示的歌词,calOffsetOfCurrentLine()返回当前歌词距离初始显示位置的偏移量,因为一句歌词从进入高亮显示到结束高亮显示有一段时间,这段时间里高亮歌词也是在滚动的,如下图所示:
“敌动我不动 你动我不动”这句开始高亮显示 “敌动我不动 你动我不动”结束高亮显示
其中,calOffsetOfCurrentLine()函数为:
private float calOffsetOfCurrentLine() { Bundle bundle = lyric.getLineFromTime(currentTime); int numOfLines = bundle.getInt("Number"); float fromTime = bundle.getInt("FromTime"); float toTime = bundle.getInt("ToTime"); float offset = (currentTime - fromTime) * 1.0f/(toTime - fromTime) * (30 * numOfLines); return offset; }
四、拖动歌词控制播放进度
这里首先要明确,上下滑动歌词只是一种动作,控制音乐播放进度。往上滑动表示进度往前,往下滑动表示进度往后。自己设定一个合适的滑动距离与时间的转换关系即可。
public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: isMoving = true; targetTime = currentTime; startY = event.getY(); break; case MotionEvent.ACTION_MOVE: targetTime = moveTo(event.getY()-startY); currentTime = targetTime; postInvalidate(); break; case MotionEvent.ACTION_UP: Intent intent = new Intent(getContext(), MusicPlayService.class); intent.setAction(MyIntentAction.SEEK_TO_FROM_LYRIC); intent.putExtra("SeekTo", currentTime); getContext().startService(intent); isMoving = false; break; } return true; }