这次的内容是歌词处理模块LyricAdapter类。这个类的主要功能有
1.歌词文件的解析
2.对外提供歌词访问服务(歌词数取得,歌词内容,时间的取得等)
3.根据播放位置检索对应的歌词。
4.在歌词文件取得后和当前歌词变化以后通过登录的LyricListener进行通知。
 
先看看LyricAdapter类在整个软件中的位置。

Android歌词秀设计思路(2)歌词处理

从图中可以看出,LyricAdapter类和SafeTimer类一样,归LyricPlayerService管理,并位置提供服务。
 
接下来在说明LyricAdapter的功能之前,先让我们看看我们的处理对象,歌词文件的内容。打开一个歌词文件(*.lrc)可以看到以下内容。

  1. [ti:δ֪]   
  2. [ar:]   
  3. [al:Family Album, U.S.A.]   
  4. [by:SPJ]   
  5. [00:00.97]EPISODE 12    You're Tops   
  6. [00:06.20]ACT II   
  7. [00:10.52]Sam, would you come in, please?   
  8. [00:16.62]You sound like something's bothering you, Susan.   
  9. [00:19.15]The sketches for the cover of the new doll book?   
  10. [00:21.74]That's not it.   
  11. [00:23.35]Please sit down.   
  12. [00:24.56]Sure.   
  13. [00:30.31]I need your advice on a personsal matter,   
  14. [00:32.53]but it's not about me.   
  15. [00:35.12]You need my advice on a personal matter,   
  16. [00:36.89]and it's not about you. OK.   
  17. [00:40.01]It's about my grandfather.   
  18. [00:42.62]What's the problem?   
  19. [00:45.17]It won't sound like a big deal,  
 

除了前面的几个特殊的ti,ar,al,by等关键字以外的每一句歌词都是有包含在中括号中的时间和后面的歌词组成的。歌词处理模块的功能就是解析歌词文件并按照歌曲播放的时间选择合适的歌词表示就可以了。

 

下面来看一看今天的主角和配角吧。

Android歌词秀设计思路(2)歌词处理

LyricAdapter类主要提供以下功能

  1. 解析歌词文件并管理得到的信息。
  2. 提供访问歌词的接口(歌词语句数,取得特定歌词信息等)
  3. 根据提供的时间选择合适的歌词并将结果通知给LyricPlayerServie类。

 

SafetyTimer类主要提供以下功能

  1. 负责定时启动从MediaPlayer取得播放的当前时间并传达给LyricAdapter

 

LyricPlayerService类主要提供以下功能

  1. 负责控制LyricAdapter,SafetyTimer的创建
  2. 建立LyricPlayerService,LyricAdapter,SafetyTimer之间的联系。
  3. 控制LyricAdapter,SafetyTimer的动作
  4. 处理传出的LyricAdapter通知

 

以下是时序图

Android歌词秀设计思路(2)歌词处理

在这个时序图中,为了说明LyricAdapter的功能,我们省略了许多细节。下面是时序图的说明。

1.创建对象并建立联系
1-1 LyricPlayerService创建MediaPlayer对象
1-2 LyricPlayerService创建LyricAdapter对象
1-3 LyricPlayerService创建SafetyTimer.OnTimerListener的匿名内嵌派生类
1-4 LyricPlayerService创建SafetyTimer并指定前一步创建的SafetyTimer.OnTimerListener
1-5 LyricPlayerService将自己指定成LyricAdapter的listener.
 
2.解析歌词
2-1 LyricPlayerService调用LyricAdapter.LoadLyric方法,参数为字幕文件的文件路径+文件名。
2 -2 LyricAdapter在LoadLyric的最后会调用LyricPlayerService.onLyricLoaded方法进行通知。
 
3.启动Timer并处理播放时间通知
3-1 在启动播放器的同时启动SafetyTimer
3-2 当定时时间到了已有,SafetyTimer会调用在1-3中创建的OnTimeListener对象的OnTimer方法。
3-3 在OnTimer方法中,OnTimeListener首先从MediaPlayer取得现在的播放位置,然后调用LyricAdapter的notifyTime方法将位置传递个LyricAdapter。
3-4 LyricAdapter根据播放位置取得当前的歌词并通知LyricPlayerService进行相应的处理。
 
 
需要补充一点,在这里我们定义了一个LyricListener接口实现了,既可以将消息通知给LyricPlayerService有避免了LyricAdapter对LyricPlayerService的依赖关系。这一点和Andorid歌词秀设计思路(1)SafetyTimer中用到的是一样的方法。

以下是LyricAdapter的代码,请参考。很简单的。


  1. package LyricPlayer.xwg;  
  2.  
  3. import java.io.BufferedReader;  
  4. import java.io.FileNotFoundException;  
  5. import java.io.FileReader;  
  6. import java.io.IOException;  
  7. import java.util.ArrayList;  
  8. import java.util.Collections;  
  9. import java.util.Comparator;  
  10.  
  11. import android.util.Log;  
  12.  
  13. public class LyricAdapter{  
  14.     private ArrayList<LyricLine> mLyricLines= null;  
  15.     private LyricListener mListener = null//歌词载入,变化Listener  
  16.     private int mCurrentLyric = 0;   //当前的歌词  
  17.     private int mLyricOffset = -300//为了解决播放滞后的问题设置的调整时间,单位是毫秒  
  18.     private static final String TAG = new String("LyricAdapter");  
  19.  
  20.     //用于向外通知歌词载入,变化的Listener  
  21.     public interface LyricListener{  
  22.         public void onLyricChanged(int lyric_index);  
  23.         public void onLyricLoaded();  
  24.     }  
  25.       
  26.     //歌词信息  
  27.     private class LyricLine{  
  28.         long mLyricTime;  //in milliseconds  
  29.         String mLyricText;  
  30.         LyricLine(long time, String lyric){  
  31.             mLyricTime = time;  
  32.             mLyricText = lyric;  
  33.         }  
  34.     }  
  35.       
  36.     //将歌词的时间字符串转化成毫秒数的Utility类,如果参数是00:01:23.45  
  37.     private static class TimeParser{  
  38.         //@return value in milliseconds.  
  39.         static long parse(String strTime){  
  40.             String beforeDot = new String("00:00:00");  
  41.             String afterDot = new String("0");  
  42.               
  43.             //将字符串按小数点拆分成整秒部分和小数部分。  
  44.             int dotIndex = strTime.indexOf(".");  
  45.             if(dotIndex < 0){  
  46.                 beforeDot = strTime;  
  47.             }else if(dotIndex == 0){  
  48.                 afterDot = strTime.substring(1);  
  49.             }else{  
  50.                 beforeDot = strTime.substring(0, dotIndex);//00:01:23  
  51.                 afterDot = strTime.substring(dotIndex + 1); //45  
  52.             }  
  53.               
  54.             long intSeconds = 0;  
  55.             int counter = 0;  
  56.             while(beforeDot.length() > 0){  
  57.                 int colonPos = beforeDot.indexOf(":");  
  58.                 try{  
  59.                     if(colonPos > 0){//找到冒号了。  
  60.                         intSeconds *= 60;  
  61.                         intSeconds += new Integer(beforeDot.substring(0, colonPos));  
  62.                         beforeDot = beforeDot.substring(colonPos + 1);  
  63.                     }else if(colonPos < 0){//没找到,剩下都当一个数处理了。  
  64.                         intSeconds *= 60;  
  65.                         intSeconds += new Integer(beforeDot);  
  66.                         beforeDot = "";  
  67.                     }else{//第一个就是冒号,不可能!  
  68.                         return -1;  
  69.                     }  
  70.                 }catch(NumberFormatException e){  
  71.                     return -1;  
  72.                 }  
  73.                 ++counter;  
  74.                 if(counter > 3){//不会超过小时,分,秒吧。  
  75.                     return -1;  
  76.                 }  
  77.             }  
  78.             //intSeconds=83  
  79.               
  80.             String totalTime = String.format("%d.%s", intSeconds, afterDot);//totaoTimer = "83.45"  
  81.             Double doubleSeconds = new Double(totalTime); //转成小数83.25  
  82.             return (long)(doubleSeconds * 1000);//转成毫秒8345  
  83.         }  
  84.     }  
  85.       
  86.     //歌词读入  
  87.     public void LoadLyric(String path){  
  88.         mLyricLines= new ArrayList<LyricLine>();  
  89.         mLyricLines.clear();  
  90.         mCurrentLyric = -1;  
  91.         try {  
  92.             FileReader fr = new FileReader(path);  
  93.             BufferedReader br = new BufferedReader (fr);  
  94.  
  95.             String line;  
  96.             while ((line = br.readLine())!=null){//读出一行  
  97.                 int timeEndIndex = line.lastIndexOf("]");//找到歌词时间的最后位置  
  98.                 if(timeEndIndex >= 3){//最起码[1]程度应该有吧。  
  99.                     String lyricText = new String();  
  100.                     //先取出歌词  
  101.                     if(timeEndIndex < (line.length() - 1)){  
  102.                         lyricText = line.substring(timeEndIndex + 1, line.length());  
  103.                     }  
  104.                       
  105.                     //处理重复的歌词,如下面的例子  
  106.                     //[时间1][时间2][时间3][时间4]歌词  
  107.                     int timeSegmentEnd = timeEndIndex;  
  108.                     while(timeSegmentEnd > 0){  
  109.                         timeEndIndex = line.lastIndexOf("]", timeSegmentEnd);  
  110.                         if(timeEndIndex < 1break//没找到"]",算了  
  111.                         int timeStartIndex = line.lastIndexOf("[", timeEndIndex - 1);  
  112.                         if(timeStartIndex < 0break//没找到"[",算了  
  113.                         //"["和"]"都找到了,取出时间字符串  
  114.                         long lyricTime = TimeParser.parse(line.substring(timeStartIndex + 1, timeEndIndex));  
  115.                         if(lyricTime >= 0){  
  116.                             lyricTime += mLyricOffset;//调整时间  
  117.                             if(lyricTime < 0){  
  118.                                 lyricTime = 0;//也别太小了。  
  119.                             }  
  120.                             //行了,保存一句。  
  121.                             mLyricLines.add(new LyricLine(lyricTime, lyricText));  
  122.                         }  
  123.                         timeSegmentEnd = timeStartIndex;  
  124.                     }  
  125.                 }  
  126.             }  
  127.             //按时间排序  
  128.             Collections.sort(mLyricLines, new Comparator<LyricLine>(){  
  129.                 //内嵌,匿名的compare类  
  130.                 public int compare(LyricLine object1, LyricLine object2){  
  131.                     if(object1.mLyricTime > object2.mLyricTime){  
  132.                         return 1;  
  133.                     } else if(object1.mLyricTime < object2.mLyricTime){  
  134.                         return -1;  
  135.                     }else{  
  136.                         return 0;  
  137.                     }  
  138.                 }  
  139.             });  
  140.             fr.close();  
  141.  
  142.         } catch (FileNotFoundException e) {  
  143.             mLyricLines = null;  
  144.             e.printStackTrace();  
  145.         } catch (IOException e) {  
  146.             mLyricLines = null;  
  147.             e.printStackTrace();  
  148.         }  
  149.         if(mListener != null){  
  150.             //如果有人想知道,告诉一声,歌词已经读进来了。  
  151.             mListener.onLyricLoaded();  
  152.         }  
  153.     }  
  154.       
  155.     public int getLyricCount(){  
  156.         if(mLyricLines != null){  
  157.             return mLyricLines.size();  
  158.         }else{  
  159.             return 0;  
  160.         }  
  161.     }  
  162.       
  163.     public String getLyric(int index){  
  164.         if(mLyricLines != null){  
  165.             if(index >= 0 && index < mLyricLines.size()){  
  166.                 return mLyricLines.get(index).mLyricText;  
  167.             }else{  
  168.                 return null;  
  169.             }  
  170.         }else{  
  171.             return null;  
  172.         }  
  173.     }  
  174.       
  175.     public long getLyricTime(int index){  
  176.         if(mLyricLines != null){  
  177.             if(index >= 0 && index < mLyricLines.size()){  
  178.                 return mLyricLines.get(index).mLyricTime;  
  179.             }else{  
  180.                 return -1;  
  181.             }  
  182.         }else{  
  183.             return -1;  
  184.         }  
  185.     }  
  186.       
  187.     public int getCurrentLyric(){  
  188.         return mCurrentLyric;  
  189.     }  
  190.       
  191.     public void setListener(LyricListener listener){  
  192.         mListener = listener;  
  193.     }  
  194.       
  195.     //由利用者调用,用来通知现在的播放时间,一边找到合适的歌词。  
  196.     public void notifyTime(long millisecond){  
  197.         if (mLyricLines != null){  
  198.             int newLyric = seekLyric(millisecond);  
  199.             Log.i(TAG, "newLyric = " + newLyric);  
  200.             if(newLyric != -1  && newLyric != mCurrentLyric){//如果找到的歌词和现在的不是一句。  
  201.                 if(mListener != null){  
  202.                     //告诉一声,歌词已经编程另外一句啦!  
  203.                     mListener.onLyricChanged(newLyric);  
  204.                 }  
  205.                 mCurrentLyric = newLyric;  
  206.             }  
  207.         }  
  208.     }  
  209.       
  210.     private int seekLyric(long millisecond){  
  211.         int findStart = 0;   
  212.         if(mCurrentLyric >= 0){  
  213.             //如果已经指定当前字幕,则现在位置开始  
  214.             findStart = mCurrentLyric;  
  215.         }  
  216.           
  217.         long lyricTime = mLyricLines.get(findStart).mLyricTime;  
  218.                   
  219.         if(millisecond > lyricTime){ //如果想要查找的时间在现在字幕的时间之后  
  220.             //如果开始位置经是最后一句了,直接返回最后一句。  
  221.             if(findStart == (mLyricLines.size() - 1)) return findStart;  
  222.               
  223.             int new_index = findStart + 1;  
  224.             //找到第一句开始时间大于输入时间的歌词  
  225.             while(new_index < mLyricLines.size() && mLyricLines.get(new_index).mLyricTime <= millisecond){  
  226.                 ++new_index;  
  227.             }  
  228.             //这句歌词的前一句就是我们要找的了。  
  229.             return new_index - 1;  
  230.         }else if(millisecond < lyricTime){ //如果想要查找的时间在现在字幕的时间之前  
  231.             //如果开始位置经是第一句了,直接返回第一句。  
  232.             if(findStart == 0return 0;  
  233.               
  234.             int new_index = findStart - 1;  
  235.             //找到开始时间小于输入时间的歌词  
  236.             while(new_index > 0 && mLyricLines.get(new_index).mLyricTime > millisecond){  
  237.                 --new_index;  
  238.             }  
  239.             //就是它了。  
  240.             return new_index;  
  241.         }else{  
  242.             //不用找了  
  243.             return findStart;  
  244.         }  
  245.           
  246.     }  
  247. }  

 参考资料:

软件功能说明:原创:Android应用开发-Andorid歌词秀,含源码

工程,×××:Android歌词秀源码,工程文件2011/9/11版

SafetyTimer:Andorid歌词秀设计思路(1)SafetyTimer