Android歌詞秀設計思路(2)歌詞處理 – Android移動開發技術文章_手機開發 Android移動開發教學課程

這次的內容是歌詞處理模塊LyricAdapter類。這個類的主要功能有
1.歌詞文件的解析
2.對外提供歌詞訪問服務(歌詞數取得,歌詞內容,時間的取得等)
3.根據播放位置檢索對應的歌詞。
4.在歌詞文件取得後和當前歌詞變化以後通過登錄的LyricListener進行通知。
 
先看看LyricAdapter類在整個軟件中的位置。

從圖中可以看出,LyricAdapter類和SafeTimer類一樣,歸LyricPlayerService管理,並位置提供服務。
 
接下來在說明LyricAdapter的功能之前,先讓我們看看我們的處理對象,歌詞文件的內容。打開一個歌詞文件(*.lrc)可以看到以下內容。
[ti:δ֪]  
[ar:]  
[al:Family Album, U.S.A.]  
[by:SPJ]  
[00:00.97]EPISODE 12    You're Tops  
[00:06.20]ACT II  
[00:10.52]Sam, would you come in, please?  
[00:16.62]You sound like something's bothering you, Susan.  
[00:19.15]The sketches for the cover of the new doll book?  
[00:21.74]That's not it.  
[00:23.35]Please sit down.  
[00:24.56]Sure.  
[00:30.31]I need your advice on a personsal matter,  
[00:32.53]but it's not about me.  
[00:35.12]You need my advice on a personal matter,  
[00:36.89]and it's not about you. OK.  
[00:40.01]It's about my grandfather.  
[00:42.62]What's the problem?  
[00:45.17]It won't sound like a big deal, 
 
除瞭前面的幾個特殊的ti,ar,al,by等關鍵字以外的每一句歌詞都是有包含在中括號中的時間和後面的歌詞組成的。歌詞處理模塊的功能就是解析歌詞文件並按照歌曲播放的時間選擇合適的歌詞表示就可以瞭。
 
下面來看一看今天的主角和配角吧。

LyricAdapter類主要提供以下功能
解析歌詞文件並管理得到的信息。
提供訪問歌詞的接口(歌詞語句數,取得特定歌詞信息等)
根據提供的時間選擇合適的歌詞並將結果通知給LyricPlayerServie類。
 
SafetyTimer類主要提供以下功能
負責定時啟動從MediaPlayer取得播放的當前時間並傳達給LyricAdapter
 
LyricPlayerService類主要提供以下功能
負責控制LyricAdapter,SafetyTimer的創建
建立LyricPlayerService,LyricAdapter,SafetyTimer之間的聯系。
控制LyricAdapter,SafetyTimer的動作
處理傳出的LyricAdapter通知
 
以下是時序圖

在這個時序圖中,為瞭說明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的代碼,請參考。很簡單的。
package LyricPlayer.xwg; 
 
import java.io.BufferedReader; 
import java.io.FileNotFoundException; 
import java.io.FileReader; 
import java.io.IOException; 
import java.util.ArrayList; 
import java.util.Collections; 
import java.util.Comparator; 
 
import android.util.Log; 
 
public class LyricAdapter{ 
    private ArrayList<LyricLine> mLyricLines= null; 
    private LyricListener mListener = null; //歌詞載入,變化Listener 
    private int mCurrentLyric = 0;   //當前的歌詞 
    private int mLyricOffset = -300; //為瞭解決播放滯後的問題設置的調整時間,單位是毫秒 
    private static final String TAG = new String("LyricAdapter"); 
 
    //用於向外通知歌詞載入,變化的Listener 
    public interface LyricListener{ 
        public void onLyricChanged(int lyric_index); 
        public void onLyricLoaded(); 
    } 
     
    //歌詞信息 
    private class LyricLine{ 
        long mLyricTime;  //in milliseconds 
        String mLyricText; 
        LyricLine(long time, String lyric){ 
            mLyricTime = time; 
            mLyricText = lyric; 
        } 
    } 
     
    //將歌詞的時間字符串轉化成毫秒數的Utility類,如果參數是00:01:23.45 
    private static class TimeParser{ 
        //@return value in milliseconds. 
        static long parse(String strTime){ 
            String beforeDot = new String("00:00:00"); 
            String afterDot = new String("0"); 
             
            //將字符串按小數點拆分成整秒部分和小數部分。 
            int dotIndex = strTime.indexOf("."); 
            if(dotIndex < 0){ 
                beforeDot = strTime; 
            }else if(dotIndex == 0){ 
                afterDot = strTime.substring(1); 
            }else{ 
                beforeDot = strTime.substring(0, dotIndex);//00:01:23 
                afterDot = strTime.substring(dotIndex + 1); //45 
            } 
             
            long intSeconds = 0; 
            int counter = 0; 
            while(beforeDot.length() > 0){ 
                int colonPos = beforeDot.indexOf(":"); 
                try{ 
                    if(colonPos > 0){//找到冒號瞭。 
                        intSeconds *= 60; 
                        intSeconds += new Integer(beforeDot.substring(0, colonPos)); 
                        beforeDot = beforeDot.substring(colonPos + 1); 
                    }else if(colonPos < 0){//沒找到,剩下都當一個數處理瞭。 
                        intSeconds *= 60; 
                        intSeconds += new Integer(beforeDot); 
                        beforeDot = ""; 
                    }else{//第一個就是冒號,不可能! 
                        return -1; 
                    } 
                }catch(NumberFormatException e){ 
                    return -1; 
                } 
                ++counter; 
                if(counter > 3){//不會超過小時,分,秒吧。 
                    return -1; 
                } 
            } 
            //intSeconds=83 
             
            String totalTime = String.format("%d.%s", intSeconds, afterDot);//totaoTimer = "83.45" 
            Double doubleSeconds = new Double(totalTime); //轉成小數83.25 
            return (long)(doubleSeconds * 1000);//轉成毫秒8345 
        } 
    } 
     
    //歌詞讀入 
    public void LoadLyric(String path){ 
        mLyricLines= new ArrayList<LyricLine>(); 
        mLyricLines.clear(); 
        mCurrentLyric = -1; 
        try { 
            FileReader fr = new FileReader(path); 
            BufferedReader br = new BufferedReader (fr); 
 
            String line; 
            while ((line = br.readLine())!=null){//讀出一行 
                int timeEndIndex = line.lastIndexOf("]");//找到歌詞時間的最後位置 
                if(timeEndIndex >= 3){//最起碼[1]程度應該有吧。 
                    String lyricText = new String(); 
                    //先取出歌詞 
                    if(timeEndIndex < (line.length() – 1)){ 
                        lyricText = line.substring(timeEndIndex + 1, line.length()); 
                    } 
                     
                    //處理重復的歌詞,如下面的例子 
                    //[時間1][時間2][時間3][時間4]歌詞 
                    int timeSegmentEnd = timeEndIndex; 
                    while(timeSegmentEnd > 0){ 
                        timeEndIndex = line.lastIndexOf("]", timeSegmentEnd); 
                        if(timeEndIndex < 1) break; //沒找到"]",算瞭 
                        int timeStartIndex = line.lastIndexOf("[", timeEndIndex – 1); 
                        if(timeStartIndex < 0) break; //沒找到"[",算瞭 
                        //"["和"]"都找到瞭,取出時間字符串 
                        long lyricTime = TimeParser.parse(line.substring(timeStartIndex + 1, timeEndIndex)); 
                        if(lyricTime >= 0){ 
                            lyricTime += mLyricOffset;//調整時間 
                            if(lyricTime < 0){ 
                                lyricTime = 0;//也別太小瞭。 
                            } 
                            //行瞭,保存一句。 
                            mLyricLines.add(new LyricLine(lyricTime, lyricText)); 
                        } 
                        timeSegmentEnd = timeStartIndex; 
                    } 
                } 
            } 
            //按時間排序 
            Collections.sort(mLyricLines, new Comparator<LyricLine>(){ 
                //內嵌,匿名的compare類 
                public int compare(LyricLine object1, LyricLine object2){ 
                    if(object1.mLyricTime > object2.mLyricTime){ 
                        return 1; 
                    } else if(object1.mLyricTime < object2.mLyricTime){ 
                        return -1; 
                    }else{ 
                        return 0; 
                    } 
                } 
            }); 
            fr.close(); 
 
        } catch (FileNotFoundException e) { 
            mLyricLines = null; 
            e.printStackTrace(); 
        } catch (IOException e) { 
            mLyricLines = null; 
            e.printStackTrace(); 
        } 
        if(mListener != null){ 
            //如果有人想知道,告訴一聲,歌詞已經讀進來瞭。 
            mListener.onLyricLoaded(); 
        } 
    } 
     
    public int getLyricCount(){ 
        if(mLyricLines != null){ 
            return mLyricLines.size(); 
        }else{ 
            return 0; 
        } 
    } 
     
    public String getLyric(int index){ 
        if(mLyricLines != null){ 
            if(index >= 0 && index < mLyricLines.size()){ 
                return mLyricLines.get(index).mLyricText; 
            }else{ 
                return null; 
            } 
        }else{ 
            return null; 
        } 
    } 
     
    public long getLyricTime(int index){ 
        if(mLyricLines != null){ 
            if(index >= 0 && index < mLyricLines.size()){ 
                return mLyricLines.get(index).mLyricTime; 
            }else{ 
                return -1; 
            } 
        }else{ 
            return -1; 
        } 
    } 
     
    public int getCurrentLyric(){ 
        return mCurrentLyric; 
    } 
     
    public void setListener(LyricListener listener){ 
        mListener = listener; 
    } 
     
    //由利用者調用,用來通知現在的播放時間,一邊找到合適的歌詞。 
    public void notifyTime(long millisecond){ 
        if (mLyricLines != null){ 
            int newLyric = seekLyric(millisecond); 
            Log.i(TAG, "newLyric = " + newLyric); 
            if(newLyric != -1  && newLyric != mCurrentLyric){//如果找到的歌詞和現在的不是一句。 
                if(mListener != null){ 
                    //告訴一聲,歌詞已經編程另外一句啦! 
                    mListener.onLyricChanged(newLyric); 
                } 
                mCurrentLyric = newLyric; 
            } 
        } 
    } 
     
    private int seekLyric(long millisecond){ 
        int findStart = 0;  
        if(mCurrentLyric >= 0){ 
            //如果已經指定當前字幕,則現在位置開始 
            findStart = mCurrentLyric; 
        } 
         
        long lyricTime = mLyricLines.get(findStart).mLyricTime; 
                 
        if(millisecond > lyricTime){ //如果想要查找的時間在現在字幕的時間之後 
            //如果開始位置經是最後一句瞭,直接返回最後一句。 
            if(findStart == (mLyricLines.size() – 1)) return findStart; 
             
            int new_index = findStart + 1; 
            //找到第一句開始時間大於輸入時間的歌詞 
            while(new_index < mLyricLines.size() && mLyricLines.get(new_index).mLyricTime <= millisecond){ 
                ++new_index; 
            } 
            //這句歌詞的前一句就是我們要找的瞭。 
            return new_index – 1; 
        }else if(millisecond < lyricTime){ //如果想要查找的時間在現在字幕的時間之前 
            //如果開始位置經是第一句瞭,直接返回第一句。 
            if(findStart == 0) return 0; 
             
            int new_index = findStart – 1; 
            //找到開始時間小於輸入時間的歌詞 
            while(new_index > 0 && mLyricLines.get(new_index).mLyricTime > millisecond){ 
                –new_index; 
            } 
            //就是它瞭。 
            return new_index; 
        }else{ 
            //不用找瞭 
            return findStart; 
        } 
         
    } 

 參考資料:
軟件功能說明:原創:Android應用開發-Andorid歌詞秀,含源碼
工程,源碼下載:Android歌詞秀源碼,工程文件2011/9/11版
SafetyTimer:Andorid歌詞秀設計思路(1)SafetyTimer
作者 “來自大連”

發佈留言