简介
之前做 TicktockMusic 音乐播放器,一个必要的需求肯定是歌词,在 github 上找了几个,发现或多或少都有点不满足需求,所以就自己动手写了一个。
先附上项目地址和效果图:
地址:https://github.com/Lauzy/LyricView
效果图:

需求
歌词的需求我想大家都很清楚,简单的话,直接打开一个音乐播放器查看一下。我们打开后分析一下歌词的功能:歌词完整的显示出来、当前歌词变色、可以根据时间而进行定位、可以手动滑动、滑动后显示一个指示器、点击指示器播放进度跳转、滑动时指示器变色等等。OK,我们自己写歌词控件,这些功能也是必不可少的,接下来就逐步分析下实现的过程。
实现
基本实现就是这三个过程,接下来一步步的分析。
歌词解析
首先,我们在网上下载一个歌词,即以 lrc 为后缀的文件。比如海阔天空这首歌的歌词,我们用记事本或者其他工具打开后就可以看到具体的歌词内容,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 
  | [ti: 海阔天空] [ar:黄家驹] [al:乐与怒] [by:mp3.50004.com] [00:00.00]Beyond:海阔天空  [01:40.00][00:16.00]今天我寒夜里看雪飘过  [01:48.00][00:24.00]怀著冷却了的心窝飘远方  [01:53.00][00:29.00]风雨里追赶  ... [00:42.00]多少次迎著冷眼与嘲笑  [00:49.00]从没有放弃过心中的理想  [00:54.00]一刹那恍惚  ... 
  | 
 
可以看到,歌词主要包含歌名、歌手、专辑、作者等头元素,以及歌词的主体内容,我们需要处理的就是主体的歌词内容。首先,歌词是一行一行的文本,其次,每行的文本都包含时间标签和具体的一行歌词,我们首先将歌词解析为一行行的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 
  | InputStreamReader isr = null; BufferedReader br = null; try {     isr = new InputStreamReader(inputStream, CHARSET);     br = new BufferedReader(isr);     String line;     while ((line = br.readLine()) != null) {                           List<Lrc> lrcList = parseLrc(line);         if (lrcList != null && lrcList.size() != 0) {             lrcs.addAll(lrcList);         }     }     sortLrcs(lrcs);     return lrcs; }catch ... 
  | 
 
解析为一行行的文字后,就需要具体的处理单行的文字了,我们可以看到,大部分歌词包含两种格式,即单个时间标签和多个时间标签,这里可以采用正则表达式来匹配文字,正则表达式为 (([\d{2}:\d{2}.\d{2}])+)(.*) 
1 2 3 4 
  | [01:53.00][00:29.00]风雨里追赶    //多个时间标签 [00:42.00]多少次迎著冷眼与嘲笑     //单个时间标签 
  | 
 
接下来根据正则表达式来解析单行歌词
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 
  | private static List<Lrc> parseLrc(String lrcLine) {     if (lrcLine.trim().isEmpty()) {         return null;     }     List<Lrc> lrcs = new ArrayList<>();     Matcher matcher = Pattern.compile(LINE_REGEX).matcher(lrcLine);     if (!matcher.matches()) {         return null;     }     String time = matcher.group(1);     String content = matcher.group(3);     Matcher timeMatcher = Pattern.compile(TIME_REGEX).matcher(time);     while (timeMatcher.find()) {         String min = timeMatcher.group(1);         String sec = timeMatcher.group(2);         String mil = timeMatcher.group(3);         Lrc lrc = new Lrc();         if (content != null && content.length() != 0) {             lrc.setTime(Long.parseLong(min) * 60 * 1000 + Long.parseLong(sec) * 1000                     + Long.parseLong(mil) * 10);             lrc.setText(content);             lrcs.add(lrc);         }     }     return lrcs; } 
  | 
 
这样,第一步就完成了,歌词解析完成后得到歌词的数据集合,每个元素都包括时间和内容。
歌词显示
歌词显示的思路就是将歌词一行行的画出来,我们首先假设屏幕足够大,那么只需要定位第一行歌词的位置,画出来第一行歌词,然后逐行下移一个固定的距离,再画出下一行歌词,依次类推,整个歌词内容就会全部花在画布上了。依照这个思路,我们可以先画出来文字。
1 2 3 4 5 6 7 8 9 10 11 12 
  | float y = getLrcHeight() / 2; float x = getLrcWidth() / 2 + getPaddingLeft(); for (int i = 0; i < getLrcCount(); i++) {       if (i > 0) {           y += textHeight  + mLrcLineSpaceHeight;       }     ...      canvas.drawText(text, x, y, mPaint);  } 
  | 
 
画出来文字的思路就是这样,首先从屏幕的中间开始,然后纵坐标每次增加文字的高度与距离之和,依次画出来每行文字。