Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發 >> 關於android開發 >> 我的Android進階之旅之Android自定義View來實現解析lrc歌詞同步滾動、上下拖動、縮放歌詞等功能

我的Android進階之旅之Android自定義View來實現解析lrc歌詞同步滾動、上下拖動、縮放歌詞等功能

編輯:關於android開發

我的Android進階之旅之Android自定義View來實現解析lrc歌詞同步滾動、上下拖動、縮放歌詞等功能


 

前言

最近有個項目有關於播放音樂時候,關於歌詞有以下幾個功能:
1、實現歌詞同步滾動的功能,即歌曲播放到哪句歌詞,就高亮地顯示出正在播放的這個歌詞;
2、實現上下拖動歌詞時候,可以拖動播放器的進度。即可以不停地上下拖動歌詞,當手指離開屏幕時候 即從當前拖動到的歌詞位置播放。
3、實現歌詞的字體大小可以進行縮放的功能。即雙指在屏幕進行縮放操作時,歌詞的字體大小也進行相應的縮放操作。

下面我將這幾個功能做成一個demo來展示給大家。首先來看看這個demo的具體實現效果,如下面幾幅圖所示。

圖1、同步滾動歌詞
同步滾動歌詞

圖2、上下拖動歌詞1
上下拖動歌詞1

圖3、上下拖動歌詞2
上下拖動歌詞2

圖4、縮放歌詞
縮放歌詞

圖5、歌詞顯示(較大字體)
歌詞顯示(較大字體)

圖6、歌詞顯示(較小字體)
歌詞顯示(較小字體)

圖7、歌詞滾動時候,高亮地畫出正滾動到的歌詞內容以及歌詞的開始時間,並該句歌詞下面畫出一條直線
歌詞滾動截圖


一、LRC歌詞文件簡介

1、什麼是LRC歌詞文件

lrc是英文lyric(歌詞)的縮寫,被用做歌詞文件的擴展名。以lrc為擴展名的歌詞文件可以在各類數碼播放器中同步顯示。

2、LRC歌詞文件的格式

先來看一份標准的LRC歌詞文件,下面展示的是王力宏的《依然愛你》的lrc歌詞的內容

[ti:依然愛你]
[ar:王力宏]
[al:火力全開 新歌+精選]
[by:歐陽鵬]
[00:01.17]一閃一閃亮晶晶 留下歲月的痕跡 
[00:07.29]我的世界的重心 依然還是你
[00:13.37]一年一年又一年 飛逝盡在一轉眼
[00:20.29]唯一永遠不改變 是不停的改變
[00:27.14]我不像從前的自己 你也有點不像你
[00:33.36]但在我眼中你的笑 依然的美麗
[00:39.53]這次只能往前走 一個方向順時鐘
[00:46.12]不知道還要多久 所以要讓你懂
[00:51.82]我依然愛你 就是唯一的退路
[00:57.36]我依然珍惜 時時刻刻的幸福
[01:04.65]你每個呼吸 每個動作 每個表情
[01:11.43]到最後一定會依然愛你
[01:18.08]依然愛你 依然愛你
[01:25.58]我不像從前的自己 你也有點不像你
[01:31.52]但在我眼中你的笑 依然的美麗
[01:37.61]這次只能往前走 一個方向順時鐘
[01:44.42]不知道還要多久 所以要讓你懂
[01:50.18]我依然愛你 就是唯一的退路
[01:55.65]我依然珍惜 時時刻刻的幸福
[02:02.84]你每個呼吸 每個動作 每個表情
[02:09.77]到最後一定會依然愛你
[02:15.61]
[02:17.61]lrc制作:http://blog.csdn.net/ouyang_peng 歐陽鵬
[02:25.61]
[02:31.06]依然愛你 依然愛你
[02:36.63]
[02:42.32]我依然愛你 或許是命中注定
[02:47.70]多年之後 任何人都無法代替
[02:54.57]那些時光 是我這一輩子 最美好
[03:01.84]那些回憶 依然無法忘記 
[03:07.88]我依然愛你 就是唯一的退路
[03:13.95]我依然珍惜 時時刻刻的幸福
[03:21.32]你每個呼吸 每個動作 每個表情
[03:28.20]到最後一定會依然愛你
[03:34.76]你每個呼吸 每個動作 每個表情
[03:42.04]到永遠一定會依然愛你
[03:53.28]
[04:01.28]  

LRC歌詞文件的標簽類型

lrc歌詞文本中含有兩類標簽:一是標識標簽 ,二是時間標簽

1、標識標簽

標識標簽,其格式為“[標識名:值]”,主要包含以下預定義的標簽:

[ar:歌手名] [ti:歌曲名] [al:專輯名] [by:編輯者(指lrc歌詞的制作人)] [offset:時間補償值] (其單位是毫秒,正值表示整體提前,負值相反。這是用於總體調整顯示快慢的,但多數的MP3可能不會支持這種標簽)。

2、時間標簽

時間標簽,形式為“[mm:ss]”或“[mm:ss.ff]”(分鐘數:秒數.毫秒數),數字須為非負整數,

比如”[12:34.50]”是有效的,而”[0x0C:-34.50]”無效。

時間標簽需位於某行歌詞中的句首部分,一行歌詞可以包含多個時間標簽

(比如歌詞中的迭句部分)。當歌曲播放到達某一時間點時,MP3就會尋找對應的時間標簽並顯示標簽後面的歌詞文本,這樣就完成了“歌詞同步”的功能。

例如下面的這首 草蜢的《失戀戰線聯盟》,就是一行歌詞包含了多個時間標簽。

[ti:失戀戰線聯盟]
[ar:草蜢]
[al:]
[00:00.00]草蜢-失戀戰線聯盟
[00:08.78]編輯:小婧
[01:43.33][00:16.27]她總是只留下電話號碼
[01:46.97][00:19.81]從不肯讓我送她回家
[01:50.61][00:23.43]聽說你也曾經愛上過她
[01:54.15][00:27.07]曾經也同樣無法自拔
[01:57.78][00:30.72]你說你學不會假裝潇灑
[02:01.41][00:34.36]卻叫我別太早放棄她
[02:05.05][00:37.99]把過去傳說成一段神話
[02:08.70][00:41.59]然後笑你是一樣的傻
[02:12.01][00:45.11]我們那麼在乎她
[02:14.15][00:47.01]卻被她全部抹殺
[02:15.96][00:48.87]越談她越相信永遠得不到回答
[02:19.57][00:52.49]到底她怎麼想
[02:21.35][00:54.28]應該繼續在這麼
[02:23.37][00:56.36]還是說穿跑了吧
[02:26.89][00:59.80]找一個承認失戀的方法
[02:30.48][01:03.41]讓心情好好地放個假
[02:34.14][01:07.00]當你我不小心又想起她
[02:45.69][02:42.20][02:37.69][01:10.60]就在記憶裡畫一個叉
[02:48.69]
[01:33.58]編輯:小婧

[01:43.33][00:16.27]她總是只留下電話號碼
上面這行歌詞表示:

[00:16.27] 這個時間點播放 “她總是只留下電話號碼” 這句歌詞,

[01:43.33] 這個時間點再一個播放 “她總是只留下電話號碼” 這句歌詞。

其實可以把上面這行歌詞拆分為下面兩句歌詞:

[00:16.27]她總是只留下電話號碼
[01:43.33]她總是只留下電話號碼

二、解析LRC歌詞

1、讀取出歌詞文件

        /**
         * 從assets目錄下讀取歌詞文件內容
         * @param fileName
         * @return
         */
        public String getFromAssets(String fileName){
            try {
                InputStreamReader inputReader = new InputStreamReader( getResources().getAssets().open(fileName) );
                BufferedReader bufReader = new BufferedReader(inputReader);
                String line="";
                String result="";
                while((line = bufReader.readLine()) != null){
                    if(line.trim().equals(""))
                        continue;
                    result += line + "\r\n";
                }
                return result;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return "";
        }

例如:從assets目錄下讀取test.lrc歌詞文件內容,則可以調用上面的getFromAssets(String fileName)方法得到歌詞的文本內容,如下所示:

    String lrc = getFromAssets("test.lrc");

2、解析得到的歌詞內容

1、表示每行歌詞內容的實體類LrcRow

首先封裝一個表示每行歌詞內容的實體類LrcRow,該類由三個屬性,分別為:
strTime、time、content。

例如一行歌詞內容為:[02:34.14]當你我不小心又想起她 , 解析該行歌詞後的實體類LrcRow的屬性如下所示:

strTime表示該行歌詞要開始播放的時間,格式如下:[02:34.14]

time表示將strTime轉換為long型之後的數值

例如將strTime為[02:34.14]格式轉換154014(154014=02 * 60 * 1000 + 34 * 1000+14)

content表示該行歌詞的內容,如:當你我不小心又想起她

代碼如下:

    package com.oyp.lrc.view.impl;

    import android.util.Log;

    import java.util.ArrayList;
    import java.util.List;

    /**
     * 歌詞行
     * 包括該行歌詞的時間,歌詞內容
     */
    public class LrcRow implements Comparable{
        public final static String TAG = "LrcRow";

        /** 該行歌詞要開始播放的時間,格式如下:[02:34.14] */
        public String strTime;

        /** 該行歌詞要開始播放的時間,由[02:34.14]格式轉換為long型,
         * 即將2分34秒14毫秒都轉為毫秒後 得到的long型值:time=02*60*1000+34*1000+14
         */
        public long time;

        /** 該行歌詞的內容 */
        public String content;


        public LrcRow(){}

        public LrcRow(String strTime,long time,String content){
            this.strTime = strTime;
            this.time = time;
            this.content = content;
    //      Log.d(TAG,"strTime:" + strTime + " time:" + time + " content:" + content);
        }

        @Override
        public String toString() {
            return "[" + strTime + " ]"  + content;
        }

        /**
         * 讀取歌詞的每一行內容,轉換為LrcRow,加入到集合中
         */
        public static List createRows(String standardLrcLine){
            /**
                一行歌詞只有一個時間的  例如:徐佳瑩   《我好想你》
                [01:15.33]我好想你 好想你

                一行歌詞有多個時間的  例如:草蜢 《失戀戰線聯盟》
                [02:34.14][01:07.00]當你我不小心又想起她
                [02:45.69][02:42.20][02:37.69][01:10.60]就在記憶裡畫一個叉
             **/
            try{
                if(standardLrcLine.indexOf("[") != 0 || standardLrcLine.indexOf("]") != 9 ){
                    return null;
                }
                //[02:34.14][01:07.00]當你我不小心又想起她
                //找到最後一個 ‘]’ 的位置
                int lastIndexOfRightBracket = standardLrcLine.lastIndexOf("]");
                //歌詞內容就是 ‘]’ 的位置之後的文本   eg:   當你我不小心又想起她
                String content = standardLrcLine.substring(lastIndexOfRightBracket + 1, standardLrcLine.length());
                //歌詞時間就是 ‘]’ 的位置之前的文本   eg:   [02:34.14][01:07.00]

                /**
                    將時間格式轉換一下  [mm:ss.SS][mm:ss.SS] 轉換為  -mm:ss.SS--mm:ss.SS-
                    即:[02:34.14][01:07.00]  轉換為      -02:34.14--01:07.00-
                 */
                String times = standardLrcLine.substring(0,lastIndexOfRightBracket + 1).replace("[", "-").replace("]", "-");
                //通過 ‘-’ 來拆分字符串
                String arrTimes[] = times.split("-");
                List listTimes = new ArrayList();
                for(String temp : arrTimes){
                    if(temp.trim().length() == 0){
                        continue;
                    }
                    /** [02:34.14][01:07.00]當你我不小心又想起她
                     *
                        上面的歌詞的就可以拆分為下面兩句歌詞了
                        [02:34.14]當你我不小心又想起她
                        [01:07.00]當你我不小心又想起她
                     */
                    LrcRow lrcRow = new LrcRow(temp, timeConvert(temp), content);
                    listTimes.add(lrcRow);
                }
                return listTimes;
            }catch(Exception e){
                Log.e(TAG,"createRows exception:" + e.getMessage());
                return null;
            }
        }

        /**
         * 將解析得到的表示時間的字符轉化為Long型
         */
        private static long timeConvert(String timeString){
            //因為給如的字符串的時間格式為XX:XX.XX,返回的long要求是以毫秒為單位
            //將字符串 XX:XX.XX 轉換為 XX:XX:XX
            timeString = timeString.replace('.', ':');
            //將字符串 XX:XX:XX 拆分
            String[] times = timeString.split(":");
            // mm:ss:SS
            return Integer.valueOf(times[0]) * 60 * 1000 +//分
                    Integer.valueOf(times[1]) * 1000 +//秒
                    Integer.valueOf(times[2]) ;//毫秒
        }

        /**
         * 排序的時候,根據歌詞的時間來排序
         */
        public int compareTo(LrcRow another) {
            return (int)(this.time - another.time);
        }
    }

該LrcRow的List createRows(String standardLrcLine)方法 ,將循環地一行一行的去讀取歌詞的內容。然後對每一行的歌詞進行解析,每解析出一個時間標簽[XX:XX.XX]則new出一個LrcRow對象,然後加入到歌詞行List集合中去。

該LrcRow類實現Comparable接口,用來進行解析之後的排序操作,排序按時間從小到大排序。

2、解析歌詞的構造器

ILrcBuilder接口

定義一個ILrcBuilder接口,接口有一個List getLrcRows(String rawLrc)方法,該方法用來解析歌詞,得到LrcRow的集合

    package com.oyp.lrc.view;

    import com.oyp.lrc.view.impl.LrcRow;

    import java.util.List;

    /**
     * 解析歌詞,得到LrcRow的集合
     */
    public interface ILrcBuilder {
        List getLrcRows(String rawLrc);
    }

DefaultLrcBuilder歌詞解析構造器

DefaultLrcBuilder實現ILrcBuilder接口,List getLrcRows(String rawLrc)方法會循環地讀取歌詞的每一行,然後調用LrcRow類的List createRows(String standardLrcLine)方法,得到解析每一行歌詞之後的LrcRow集合,再將每一行得到LrcRow集合中得到的LrcRow實體加入一個總 的到LrcRow集合rows中去,然後將rows集合根據歌詞行的時間排序,得到排序後的LrcRow集合,該集合就是最終的解析歌詞後的內容了。

代碼如下:

    package com.oyp.lrc.view.impl;

    import android.util.Log;
    import com.oyp.lrc.view.ILrcBuilder;
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.StringReader;
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;

    /**
     * 解析歌詞,得到LrcRow的集合
     */
    public class DefaultLrcBuilder implements ILrcBuilder {
        static final String TAG = "DefaultLrcBuilder";

        public List getLrcRows(String rawLrc) {
            Log.d(TAG,"getLrcRows by rawString");
            if(rawLrc == null || rawLrc.length() == 0){
                Log.e(TAG,"getLrcRows rawLrc null or empty");
                return null;
            }
            StringReader reader = new StringReader(rawLrc);
            BufferedReader br = new BufferedReader(reader);
            String line = null;
            List rows = new ArrayList();
            try{
                //循環地讀取歌詞的每一行
                do{
                    line = br.readLine();
                    /**
                     一行歌詞只有一個時間的  例如:徐佳瑩   《我好想你》
                     [01:15.33]我好想你 好想你

                     一行歌詞有多個時間的  例如:草蜢 《失戀戰線聯盟》
                     [02:34.14][01:07.00]當你我不小心又想起她
                     [02:45.69][02:42.20][02:37.69][01:10.60]就在記憶裡畫一個叉
                     **/
                    Log.d(TAG,"lrc raw line: " + line);
                    if(line != null && line.length() > 0){
                        //解析每一行歌詞 得到每行歌詞的集合,因為有些歌詞重復有多個時間,就可以解析出多個歌詞行來
                        List lrcRows = LrcRow.createRows(line);
                        if(lrcRows != null && lrcRows.size() > 0){
                            for(LrcRow row : lrcRows){
                                rows.add(row);
                            }
                        }
                    }
                }while(line != null);

                if( rows.size() > 0 ){
                    // 根據歌詞行的時間排序
                    Collections.sort(rows);
                    if(rows!=null&&rows.size()>0){
                        for(LrcRow lrcRow:rows){
                            Log.d(TAG, "lrcRow:" + lrcRow.toString());
                        }
                    }
                }
            }catch(Exception e){
                Log.e(TAG,"parse exceptioned:" + e.getMessage());
                return null;
            }finally{
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                reader.close();
            }
            return rows;
        }
    }

例如:通過下面代碼來調用ILrcBuilder解析歌詞,

        //從assets目錄下讀取歌詞文件內容
        String lrc = getFromAssets("test.lrc");
        //解析歌詞構造器
        ILrcBuilder builder = new DefaultLrcBuilder();
        //解析歌詞返回LrcRow集合
        List rows = builder.getLrcRows(lrc);

lrc歌詞原始內容

草蜢的《失戀戰線聯盟》,lrc原始內容如下:

[ti:失戀戰線聯盟]
[ar:草蜢]
[al:]
[00:00.00]草蜢-失戀戰線聯盟
[00:08.78]編輯:小婧
[01:43.33][00:16.27]她總是只留下電話號碼
[01:46.97][00:19.81]從不肯讓我送她回家
[01:50.61][00:23.43]聽說你也曾經愛上過她
[01:54.15][00:27.07]曾經也同樣無法自拔
[01:57.78][00:30.72]你說你學不會假裝潇灑
[02:01.41][00:34.36]卻叫我別太早放棄她
[02:05.05][00:37.99]把過去傳說成一段神話
[02:08.70][00:41.59]然後笑你是一樣的傻
[02:12.01][00:45.11]我們那麼在乎她
[02:14.15][00:47.01]卻被她全部抹殺
[02:15.96][00:48.87]越談她越相信永遠得不到回答
[02:19.57][00:52.49]到底她怎麼想
[02:21.35][00:54.28]應該繼續在這麼
[02:23.37][00:56.36]還是說穿跑了吧
[02:26.89][00:59.80]找一個承認失戀的方法
[02:30.48][01:03.41]讓心情好好地放個假
[02:34.14][01:07.00]當你我不小心又想起她
[02:45.69][02:42.20][02:37.69][01:10.60]就在記憶裡畫一個叉
[02:48.69]
[01:33.58]編輯:小婧

讀取該歌詞內容,過程中的打印日志為:

03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [ti:失戀戰線聯盟]
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [ar:草蜢]
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [al:]
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [00:00.00]草蜢-失戀戰線聯盟
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [00:08.78]編輯:小婧
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [01:43.33][00:16.27]她總是只留下電話號碼
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [01:46.97][00:19.81]從不肯讓我送她回家
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [01:50.61][00:23.43]聽說你也曾經愛上過她
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [01:54.15][00:27.07]曾經也同樣無法自拔
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [01:57.78][00:30.72]你說你學不會假裝潇灑
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:01.41][00:34.36]卻叫我別太早放棄她
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:05.05][00:37.99]把過去傳說成一段神話
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:08.70][00:41.59]然後笑你是一樣的傻
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:12.01][00:45.11]我們那麼在乎她
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:14.15][00:47.01]卻被她全部抹殺
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:15.96][00:48.87]越談她越相信永遠得不到回答
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:19.57][00:52.49]到底她怎麼想
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:21.35][00:54.28]應該繼續在這麼
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:23.37][00:56.36]還是說穿跑了吧
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:26.89][00:59.80]找一個承認失戀的方法
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:30.48][01:03.41]讓心情好好地放個假
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:34.14][01:07.00]當你我不小心又想起她
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:45.69][02:42.20][02:37.69][01:10.60]就在記憶裡畫一個叉
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:48.69]
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [01:33.58]編輯:小婧
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: null

解析歌詞後遍歷List集合的打印日志為:

03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:00.00 ]草蜢-失戀戰線聯盟
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:08.78 ]編輯:小婧
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:16.27 ]她總是只留下電話號碼
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:19.81 ]從不肯讓我送她回家
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:23.43 ]聽說你也曾經愛上過她
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:27.07 ]曾經也同樣無法自拔
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:30.72 ]你說你學不會假裝潇灑
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:34.36 ]卻叫我別太早放棄她
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:37.99 ]把過去傳說成一段神話
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:41.59 ]然後笑你是一樣的傻
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:45.11 ]我們那麼在乎她
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:47.01 ]卻被她全部抹殺
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:48.87 ]越談她越相信永遠得不到回答
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:52.49 ]到底她怎麼想
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:54.28 ]應該繼續在這麼
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:56.36 ]還是說穿跑了吧
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:59.80 ]找一個承認失戀的方法
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[01:03.41 ]讓心情好好地放個假
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[01:07.00 ]當你我不小心又想起她
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[01:10.60 ]就在記憶裡畫一個叉
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[01:33.58 ]編輯:小婧
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[01:43.33 ]她總是只留下電話號碼
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[01:46.97 ]從不肯讓我送她回家
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[01:50.61 ]聽說你也曾經愛上過她
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[01:54.15 ]曾經也同樣無法自拔
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[01:57.78 ]你說你學不會假裝潇灑
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:01.41 ]卻叫我別太早放棄她
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:05.05 ]把過去傳說成一段神話
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:08.70 ]然後笑你是一樣的傻
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:12.01 ]我們那麼在乎她
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:14.15 ]卻被她全部抹殺
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:15.96 ]越談她越相信永遠得不到回答
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:19.57 ]到底她怎麼想
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:21.35 ]應該繼續在這麼
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:23.37 ]還是說穿跑了吧
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:26.89 ]找一個承認失戀的方法
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:30.48 ]讓心情好好地放個假
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:34.14 ]當你我不小心又想起她
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:37.69 ]就在記憶裡畫一個叉
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:42.20 ]就在記憶裡畫一個叉
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:45.69 ]就在記憶裡畫一個叉
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:48.69 ]

lrc歌詞解析後的內容

即,草蜢的《失戀戰線聯盟》的lrc歌詞解析完後的內容如下:

[00:00.00 ]草蜢-失戀戰線聯盟
[00:08.78 ]編輯:小婧
[00:16.27 ]她總是只留下電話號碼
[00:19.81 ]從不肯讓我送她回家
[00:23.43 ]聽說你也曾經愛上過她
[00:27.07 ]曾經也同樣無法自拔
[00:30.72 ]你說你學不會假裝潇灑
[00:34.36 ]卻叫我別太早放棄她
[00:37.99 ]把過去傳說成一段神話
[00:41.59 ]然後笑你是一樣的傻
[00:45.11 ]我們那麼在乎她
[00:47.01 ]卻被她全部抹殺
[00:48.87 ]越談她越相信永遠得不到回答
[00:52.49 ]到底她怎麼想
[00:54.28 ]應該繼續在這麼
[00:56.36 ]還是說穿跑了吧
[00:59.80 ]找一個承認失戀的方法
[01:03.41 ]讓心情好好地放個假
[01:07.00 ]當你我不小心又想起她
[01:10.60 ]就在記憶裡畫一個叉
[01:33.58 ]編輯:小婧
[01:43.33 ]她總是只留下電話號碼
[01:46.97 ]從不肯讓我送她回家
[01:50.61 ]聽說你也曾經愛上過她
[01:54.15 ]曾經也同樣無法自拔
[01:57.78 ]你說你學不會假裝潇灑
[02:01.41 ]卻叫我別太早放棄她
[02:05.05 ]把過去傳說成一段神話
[02:08.70 ]然後笑你是一樣的傻
[02:12.01 ]我們那麼在乎她
[02:14.15 ]卻被她全部抹殺
[02:15.96 ]越談她越相信永遠得不到回答
[02:19.57 ]到底她怎麼想
[02:21.35 ]應該繼續在這麼
[02:23.37 ]還是說穿跑了吧
[02:26.89 ]找一個承認失戀的方法
[02:30.48 ]讓心情好好地放個假
[02:34.14 ]當你我不小心又想起她
[02:37.69 ]就在記憶裡畫一個叉
[02:42.20 ]就在記憶裡畫一個叉
[02:45.69 ]就在記憶裡畫一個叉
[02:48.69 ]

下面是解析歌詞前後的對比圖
這裡寫圖片描述

至此,歌詞解析完畢!


三、顯示LRC歌詞內容

1、定義一個ILrcViewListener接口

ILrcViewListener接口,該接口定義了一個onLrcSeeked方法用來監聽用戶上下拖動歌詞的動作定義了一個方法

onLrcSeeked(int newPosition, LrcRow row)

當歌詞被用戶上下拖動的時候回調該方法

    package com.oyp.lrc.view;

    import com.oyp.lrc.view.impl.LrcRow;

    /**
     * 歌詞拖動時候的監聽類
     */
    public interface ILrcViewListener {
        /**
         * 當歌詞被用戶上下拖動的時候回調該方法
         */
        void onLrcSeeked(int newPosition, LrcRow row);
    }

2、定義一個ILrcView接口

ILrcView接口接口,定義了三個方法

setLrc(List lrcRows)

調用該方法設置要展示的歌詞行集合

seekLrcToTime(long time)

音樂播放的時候調用該方法滾動歌詞,高亮正在播放的那句歌詞

setListener(ILrcViewListener l)

調用該方法設設置歌詞拖動時候的監聽類,用以回調ILrcViewListener的onLrcSeeked(int newPosition, LrcRow row)方法

    package com.oyp.lrc.view;

    import com.oyp.lrc.view.impl.LrcRow;

    import java.util.List;

    /**
     * 展示歌詞的接口
     */
    public interface ILrcView {

        /**
         * 設置要展示的歌詞行集合
         */
        void setLrc(List lrcRows);

        /**
         * 音樂播放的時候調用該方法滾動歌詞,高亮正在播放的那句歌詞
         */
        void seekLrcToTime(long time);
        /**
         * 設置歌詞拖動時候的監聽類
         */
        void setListener(ILrcViewListener l);
    }

3、自定義一個LrcView

自定義一個LrcView,該LrcView繼承android.view.View對象,實現了ILrcView接口。該自定義LrcView可以實現了同步顯示歌詞,拖動歌詞,縮放歌詞等功能。

同步顯示歌詞功能

首先來說說顯示歌詞的實現思路,要顯示歌詞即把歌詞的內容繪制出來,可以分以下三步來繪制歌詞:

第1步:高亮地畫出正在播放的那句歌詞

第2步:畫出正在播放的那句歌詞的上面可以展示出來的歌詞

第3步:畫出正在播放的那句歌詞的下面的可以展示出來的歌詞

重寫onDraw(Canvas canvas)方法,在方法中按照上面的思路來繪制者三部分的歌詞。代碼如下:

 @Override
    protected void onDraw(Canvas canvas) {
        final int height = getHeight(); // height of this view
        final int width = getWidth(); // width of this view
        //當沒有歌詞的時候
        if (mLrcRows == null || mLrcRows.size() == 0) {
            if (mLoadingLrcTip != null) {
                // draw tip when no lrc.
                mPaint.setColor(mHignlightRowColor);
                mPaint.setTextSize(mLrcFontSize);
                mPaint.setTextAlign(Align.CENTER);
                canvas.drawText(mLoadingLrcTip, width / 2, height / 2 - mLrcFontSize, mPaint);
            }
            return;
        }

        int rowY = 0; // vertical point of each row.
        final int rowX = width / 2;
        int rowNum = 0;
        /**
         * 分以下三步來繪制歌詞:
         *
         *  第1步:高亮地畫出正在播放的那句歌詞
         *  第2步:畫出正在播放的那句歌詞的上面可以展示出來的歌詞
         *  第3步:畫出正在播放的那句歌詞的下面的可以展示出來的歌詞
         */
        // 1、 高亮地畫出正在要高亮的的那句歌詞
        String highlightText = mLrcRows.get(mHignlightRow).content;
        int highlightRowY = height / 2 - mLrcFontSize;
        mPaint.setColor(mHignlightRowColor);
        mPaint.setTextSize(mLrcFontSize);
        mPaint.setTextAlign(Align.CENTER);
        canvas.drawText(highlightText, rowX, highlightRowY, mPaint);

        // 上下拖動歌詞的時候 畫出拖動要高亮的那句歌詞的時間 和 高亮的那句歌詞下面的一條直線
        if (mDisplayMode == DISPLAY_MODE_SEEK) {
            // 畫出高亮的那句歌詞下面的一條直線
            mPaint.setColor(mSeekLineColor);
            //該直線的x坐標從0到屏幕寬度  y坐標為高亮歌詞和下一行歌詞中間
            canvas.drawLine(mSeekLinePaddingX, highlightRowY + mPaddingY, width - mSeekLinePaddingX, highlightRowY + mPaddingY, mPaint);

            // 畫出高亮的那句歌詞的時間
            mPaint.setColor(mSeekLineTextColor);
            mPaint.setTextSize(mSeekLineTextSize);
            mPaint.setTextAlign(Align.LEFT);
            canvas.drawText(mLrcRows.get(mHignlightRow).strTime, 0, highlightRowY, mPaint);
        }

        // 2、畫出正在播放的那句歌詞的上面可以展示出來的歌詞
        mPaint.setColor(mNormalRowColor);
        mPaint.setTextSize(mLrcFontSize);
        mPaint.setTextAlign(Align.CENTER);
        rowNum = mHignlightRow - 1;
        rowY = highlightRowY - mPaddingY - mLrcFontSize;
        //只畫出正在播放的那句歌詞的上一句歌詞
//        if (rowY > -mLrcFontSize && rowNum >= 0) {
//            String text = mLrcRows.get(rowNum).content;
//            canvas.drawText(text, rowX, rowY, mPaint);
//        }

        //畫出正在播放的那句歌詞的上面所有的歌詞
        while( rowY > -mLrcFontSize && rowNum >= 0){
            String text = mLrcRows.get(rowNum).content;
            canvas.drawText(text, rowX, rowY, mPaint);
            rowY -=  (mPaddingY + mLrcFontSize);
            rowNum --;
        }

        // 3、畫出正在播放的那句歌詞的下面的可以展示出來的歌詞
        rowNum = mHignlightRow + 1;
        rowY = highlightRowY + mPaddingY + mLrcFontSize;

        //只畫出正在播放的那句歌詞的下一句歌詞
//        if (rowY < height && rowNum < mLrcRows.size()) {
//            String text2 = mLrcRows.get(rowNum).content;
//            canvas.drawText(text2, rowX, rowY, mPaint);
//        }

        //畫出正在播放的那句歌詞的所有下面的可以展示出來的歌詞
        while( rowY < height && rowNum < mLrcRows.size()){
            String text = mLrcRows.get(rowNum).content;
            canvas.drawText(text, rowX, rowY, mPaint);
            rowY += (mPaddingY + mLrcFontSize);
            rowNum ++;
        }

    }

為了實現同步顯示功能的功能,則需要不停地將自定義的LrcView進行重繪。首先當MediaPlayer開始播放的時候,同步的啟動一個TimerTask來進行歌詞的滾動操作。如代碼所示:

mPlayer.setOnPreparedListener(new OnPreparedListener() {
                //准備完畢
                public void onPrepared(MediaPlayer mp) {
                    mp.start();
                    if(mTimer == null){
                        mTimer = new Timer();
                        mTask = new LrcTask();
                        mTimer.scheduleAtFixedRate(mTask, 0, mPalyTimerDuration);
                    }
                }
            });

上面代碼的意思是,當MediaPlayer開始播放的時候,啟動一個定時器Timer,然後通過這個定時器每隔mPalyTimerDuration時間來執行一次LrcTask任務。LrcTask的代碼如下:

 /**
     * 展示歌曲的定時任務
     */
    class LrcTask extends TimerTask{
        @Override
        public void run() {
            //獲取歌曲播放的位置
            final long timePassed = mPlayer.getCurrentPosition();
            MainActivity.this.runOnUiThread(new Runnable() {
                public void run() {
                    //滾動歌詞
                    mLrcView.seekLrcToTime(timePassed);
                }
            });

        }
    };

上面的代碼是:首先獲取MediaPlayer的播放進度值,然後調用了LrcView的seekLrcToTime(long time)方法進行歌詞同步滾動,LrcView的seekLrcToTime(long time)方法的實現代碼如下:

/**
     * 播放的時候調用該方法滾動歌詞,高亮正在播放的那句歌詞
     * @param time
     */
    public void seekLrcToTime(long time) {
        if (mLrcRows == null || mLrcRows.size() == 0) {
            return;
        }
        if (mDisplayMode != DISPLAY_MODE_NORMAL) {
            return;
        }
        Log.d(TAG, "seekLrcToTime:" + time);

        for (int i = 0; i < mLrcRows.size(); i++) {
            LrcRow current = mLrcRows.get(i);
            LrcRow next = i + 1 == mLrcRows.size() ? null : mLrcRows.get(i + 1);
            /**
             *  正在播放的時間大於current行的歌詞的時間而小於next行歌詞的時間, 設置要高亮的行為current行
             *  正在播放的時間大於current行的歌詞,而current行為最後一句歌詞時,設置要高亮的行為current行
             */
            if ((time >= current.time && next != null && time < next.time)
                    || (time > current.time && next == null)){
                seekLrc(i, false);
                return;
            }
        }
    }

上面代碼意思是,首先通過傳入進來的MediaPlayer的播放進度值,來判斷需要高亮地歌詞行LrcRow是哪一行,然後調用seekLrc(int position, boolean cb)方法來進行歌詞重繪操作。seekLrc(int position, boolean cb)方法的實現如下所示:

/**
     * 設置要高亮的歌詞為第幾行歌詞
     *
     * @param position 要高亮的歌詞行數
     * @param cb       是否是手指拖動後要高亮的歌詞
     */
    public void seekLrc(int position, boolean cb) {
        if (mLrcRows == null || position < 0 || position > mLrcRows.size()) {
            return;
        }
        LrcRow lrcRow = mLrcRows.get(position);
        mHignlightRow = position;
        invalidate();
        //如果是手指拖動歌詞後
        if (mLrcViewListener != null && cb) {
            //回調onLrcSeeked方法,將音樂播放器播放的位置移動到高亮歌詞的位置
            mLrcViewListener.onLrcSeeked(position, lrcRow);
        }
    }

上面方法是將要高亮的歌詞行設置為目前正在播放的歌詞行,然後重繪LrcView。

拖動歌詞的功能

要實現拖動歌詞的功能,可以分為以下幾步來實現
1、給LrcView注冊一個ILrcViewListener監聽接口。

下面是LrcView注冊ILrcViewListener監聽的具體實現。

 //設置自定義的LrcView上下拖動歌詞時監聽
        mLrcView.setListener(new ILrcViewListener() {
            //當歌詞被用戶上下拖動的時候回調該方法,從高亮的那一句歌詞開始播放
            public void onLrcSeeked(int newPosition, LrcRow row) {
                if (mPlayer != null) {
                    Log.d(TAG, "onLrcSeeked:" + row.time);
                    mPlayer.seekTo((int) row.time);
                }
            }
        });

2、當歌詞進行拖動的時候,回調ILrcViewListener接口的onLrcSeeked(int newPosition, LrcRow row)方法。

如下面代碼所示:回調了onLrcSeeked(int newPosition, LrcRow row)方法。

/**
     * 設置要高亮的歌詞為第幾行歌詞
     *
     * @param position 要高亮的歌詞行數
     * @param cb       是否是手指拖動後要高亮的歌詞
     */
    public void seekLrc(int position, boolean cb) {
        if (mLrcRows == null || position < 0 || position > mLrcRows.size()) {
            return;
        }
        LrcRow lrcRow = mLrcRows.get(position);
        mHignlightRow = position;
        invalidate();
        //如果是手指拖動歌詞後
        if (mLrcViewListener != null && cb) {
            //回調onLrcSeeked方法,將音樂播放器播放的位置移動到高亮歌詞的位置
            mLrcViewListener.onLrcSeeked(position, lrcRow);
        }
    }

3、判斷手指在屏幕上的操作,來進行歌詞滾動的操作。
重寫onTouchEvent(MotionEvent event)方法,來判斷手指的操作是拖動歌詞還是縮放歌詞。

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mLrcRows == null || mLrcRows.size() == 0) {
            return super.onTouchEvent(event);
        }
        switch (event.getAction()) {
            //手指按下
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "down,mLastMotionY:" + mLastMotionY);
                mLastMotionY = event.getY();
                mIsFirstMove = true;
                invalidate();
                break;
            //手指移動
            case MotionEvent.ACTION_MOVE:
                if (event.getPointerCount() == 2) {
                    Log.d(TAG, "two move");
                    doScale(event);
                    return true;
                }
                Log.d(TAG, "one move");
                // single pointer mode ,seek
                //如果是雙指同時按下,進行歌詞大小縮放,抬起其中一個手指,另外一個手指不離開屏幕地移動的話,不做任何處理
                if (mDisplayMode == DISPLAY_MODE_SCALE) {
                    //if scaling but pointer become not two ,do nothing.
                    return true;
                }
                //如果一個手指按下,在屏幕上移動的話,拖動歌詞上下
                doSeek(event);
                break;
            case MotionEvent.ACTION_CANCEL:
                //手指抬起
            case MotionEvent.ACTION_UP:
                if (mDisplayMode == DISPLAY_MODE_SEEK) {
                    //高亮手指抬起時的歌詞並播放從該句歌詞開始播放
                    seekLrc(mHignlightRow, true);
                }
                mDisplayMode = DISPLAY_MODE_NORMAL;
                invalidate();
                break;
        }
        return true;
    }

如上代碼所示,當一個手指移動的時候,則調用doSeek(MotionEvent event)方法來進行拖動歌詞的操作,doSeek(MotionEvent event)方法的具體實現代碼如下:

/**
     * 處理單指在屏幕移動時,歌詞上下滾動
     */
    private void doSeek(MotionEvent event) {
        float y = event.getY();//手指當前位置的y坐標
        float offsetY = y - mLastMotionY; //第一次按下的y坐標和目前移動手指位置的y坐標之差
        //如果移動距離小於10,不做任何處理
        if (Math.abs(offsetY) < mMinSeekFiredOffset) {
            return;
        }
        //將模式設置為拖動歌詞模式
        mDisplayMode = DISPLAY_MODE_SEEK;
        int rowOffset = Math.abs((int) offsetY / mLrcFontSize); //歌詞要滾動的行數

        Log.d(TAG, "move to new hightlightrow : " + mHignlightRow + " offsetY: " + offsetY + " rowOffset:" + rowOffset);

        if (offsetY < 0) {
            //手指向上移動,歌詞向下滾動
            mHignlightRow += rowOffset;//設置要高亮的歌詞為 當前高亮歌詞 向下滾動rowOffset行後的歌詞
        } else if (offsetY > 0) {
            //手指向下移動,歌詞向上滾動
            mHignlightRow -= rowOffset;//設置要高亮的歌詞為 當前高亮歌詞 向上滾動rowOffset行後的歌詞
        }
        //設置要高亮的歌詞為0和mHignlightRow中的較大值,即如果mHignlightRow < 0,mHignlightRow=0
        mHignlightRow = Math.max(0, mHignlightRow);
        //設置要高亮的歌詞為0和mHignlightRow中的較小值,即如果mHignlight > RowmLrcRows.size()-1,mHignlightRow=mLrcRows.size()-1
        mHignlightRow = Math.min(mHignlightRow, mLrcRows.size() - 1);
        //如果歌詞要滾動的行數大於0,則重畫LrcView
        if (rowOffset > 0) {
            mLastMotionY = y;
            invalidate();
        }
    }

如上面代碼所示,當一個手指不停的在屏幕上移動時,將會不停地調用doSeek(MotionEvent event)方法來進行LrcView的重繪操作,從而實現了歌詞拖動的效果。

當手指離開屏幕的時候,即MotionEvent 為MotionEvent.ACTION_UP的時候,會調用seekLrc(int position, boolean cb)方法,從而回調ILrcViewListener接口的onLrcSeeked方法,來拖動MediaPlayer的播放進度值,從而達到了拖動歌詞後從最終高亮的歌詞開始重新播放歌詞的功能。如下代碼所示:

case MotionEvent.ACTION_UP:
                if (mDisplayMode == DISPLAY_MODE_SEEK) {
                    //高亮手指抬起時的歌詞並播放從該句歌詞開始播放
                    seekLrc(mHignlightRow, true);
                }
                mDisplayMode = DISPLAY_MODE_NORMAL;
                invalidate();
                break;

縮放歌詞的功能

如onTouchEvent(MotionEvent event)方法中所示,當兩個手指在屏幕上移動的時候,調用doScale(MotionEvent event)方法來做縮放歌詞的功能。

case MotionEvent.ACTION_MOVE:
                if (event.getPointerCount() == 2) {
                    Log.d(TAG, "two move");
                    doScale(event);
                    return true;
                }
                Log.d(TAG, "one move");
                // single pointer mode ,seek
                //如果是雙指同時按下,進行歌詞大小縮放,抬起其中一個手指,另外一個手指不離開屏幕地移動的話,不做任何處理
                if (mDisplayMode == DISPLAY_MODE_SCALE) {
                    //if scaling but pointer become not two ,do nothing.
                    return true;
                }
                //如果一個手指按下,在屏幕上移動的話,拖動歌詞上下
                doSeek(event);
                break;

doScale(MotionEvent event)方法的具體實現代碼如下所示:

/**
     * 處理雙指在屏幕移動時的,歌詞大小縮放
     */
    private void doScale(MotionEvent event) {
        //如果歌詞的模式為:拖動歌詞模式
        if (mDisplayMode == DISPLAY_MODE_SEEK) {
            //如果是單指按下,在進行歌詞上下滾動,然後按下另外一個手指,則把歌詞模式從 拖動歌詞模式 變為 縮放歌詞模式
            mDisplayMode = DISPLAY_MODE_SCALE;
            Log.d(TAG, "change mode from DISPLAY_MODE_SEEK to DISPLAY_MODE_SCALE");
            return;
        }
        // two pointer mode , scale font
        if (mIsFirstMove) {
            mDisplayMode = DISPLAY_MODE_SCALE;
            invalidate();
            mIsFirstMove = false;
            //兩個手指的x坐標和y坐標
            setTwoPointerLocation(event);
        }
        //獲取歌詞大小要縮放的比例
        int scaleSize = getScale(event);
        Log.d(TAG, "scaleSize:" + scaleSize);
        //如果縮放大小不等於0,進行縮放,重繪LrcView
        if (scaleSize != 0) {
            setNewFontSize(scaleSize);
            invalidate();
        }
        setTwoPointerLocation(event);
    }

如上代碼所示,當兩個手指第一次放在屏幕上時候,調用setTwoPointerLocation(MotionEvent event)方法來記錄兩個手指的x坐標和y坐標,setTwoPointerLocation(MotionEvent event)方法代碼如下所示:

 /**
     * 設置當前兩個手指的x坐標和y坐標
     */
    private void setTwoPointerLocation(MotionEvent event) {
        mPointerOneLastMotion.x = event.getX(0);
        mPointerOneLastMotion.y = event.getY(0);
        mPointerTwoLastMotion.x = event.getX(1);
        mPointerTwoLastMotion.y = event.getY(1);
    }

當兩個手指在屏幕上移動的時候,調用getScale(MotionEvent event)方法來對比兩個手指前後兩次的x坐標和y坐標,從而得到要縮放的比例scaleSize。getScale(MotionEvent event)方法具體實現如下所示:

/**
     * 獲取歌詞大小要縮放的比例
     */
    private int getScale(MotionEvent event) {
        Log.d(TAG, "scaleSize getScale");
        float x0 = event.getX(0);
        float y0 = event.getY(0);
        float x1 = event.getX(1);
        float y1 = event.getY(1);

        float maxOffset = 0; // max offset between x or y axis,used to decide scale size

        boolean zoomin = false;
        //第一次雙指之間的x坐標的差距
        float oldXOffset = Math.abs(mPointerOneLastMotion.x - mPointerTwoLastMotion.x);
        //第二次雙指之間的x坐標的差距
        float newXoffset = Math.abs(x1 - x0);

        //第一次雙指之間的y坐標的差距
        float oldYOffset = Math.abs(mPointerOneLastMotion.y - mPointerTwoLastMotion.y);
        //第二次雙指之間的y坐標的差距
        float newYoffset = Math.abs(y1 - y0);

        //雙指移動之後,判斷雙指之間移動的最大差距
        maxOffset = Math.max(Math.abs(newXoffset - oldXOffset), Math.abs(newYoffset - oldYOffset));
        //如果x坐標移動的多一些
        if (maxOffset == Math.abs(newXoffset - oldXOffset)) {
            //如果第二次雙指之間的x坐標的差距大於第一次雙指之間的x坐標的差距則是放大,反之則縮小
            zoomin = newXoffset > oldXOffset ? true : false;
        }
        //如果y坐標移動的多一些
        else {
            //如果第二次雙指之間的y坐標的差距大於第一次雙指之間的y坐標的差距則是放大,反之則縮小
            zoomin = newYoffset > oldYOffset ? true : false;
        }
        Log.d(TAG, "scaleSize maxOffset:" + maxOffset);
        if (zoomin) {
            return (int) (maxOffset / 10);//放大雙指之間移動的最大差距的1/10
        } else {
            return -(int) (maxOffset / 10);//縮小雙指之間移動的最大差距的1/10
        }
    }

當通過getScale(MotionEvent event)方法獲得了縮放比scaleSize後,調用setNewFontSize(int scaleSize)來設置歌詞的新的字體大小,然後重繪LrcView,從而實現了縮放歌詞的功能。
setNewFontSize(int scaleSize)方法的具體實現如下所示:

   /**
     * 設置縮放後的字體大小
     */
    private void setNewFontSize(int scaleSize) {
        //設置歌詞縮放後的的最新字體大小
        mLrcFontSize += scaleSize;
        mLrcFontSize = Math.max(mLrcFontSize, mMinLrcFontSize);
        mLrcFontSize = Math.min(mLrcFontSize, mMaxLrcFontSize);

        //設置顯示高亮的那句歌詞的時間最新字體大小
        mSeekLineTextSize += scaleSize;
        mSeekLineTextSize = Math.max(mSeekLineTextSize, mMinSeekLineTextSize);
        mSeekLineTextSize = Math.min(mSeekLineTextSize, mMaxSeekLineTextSize);
    }

至此,縮放功能已經實現。


LrcView的全部代碼如下所示:

package com.oyp.lrc.view.impl;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import com.oyp.lrc.view.ILrcView;
import com.oyp.lrc.view.ILrcViewListener;

import java.util.List;

/**
 * 自定義LrcView,可以同步顯示歌詞,拖動歌詞,縮放歌詞
 */
public class LrcView extends View implements ILrcView {

    public final static String TAG = "LrcView";

    /**
     * 正常歌詞模式
     */
    public final static int DISPLAY_MODE_NORMAL = 0;
    /**
     * 拖動歌詞模式
     */
    public final static int DISPLAY_MODE_SEEK = 1;
    /**
     * 縮放歌詞模式
     */
    public final static int DISPLAY_MODE_SCALE = 2;
    /**
     * 歌詞的當前展示模式
     */
    private int mDisplayMode = DISPLAY_MODE_NORMAL;

    /**
     * 歌詞集合,包含所有行的歌詞
     */
    private List mLrcRows;
    /**
     * 最小移動的距離,當拖動歌詞時如果小於該距離不做處理
     */
    private int mMinSeekFiredOffset = 10;

    /**
     * 當前高亮歌詞的行數
     */
    private int mHignlightRow = 0;
    /**
     * 當前高亮歌詞的字體顏色為黃色
     */
    private int mHignlightRowColor = Color.YELLOW;
    /**
     * 不高亮歌詞的字體顏色為白色
     */
    private int mNormalRowColor = Color.WHITE;

    /**
     * 拖動歌詞時,在當前高亮歌詞下面的一條直線的字體顏色
     **/
    private int mSeekLineColor = Color.CYAN;
    /**
     * 拖動歌詞時,展示當前高亮歌詞的時間的字體顏色
     **/
    private int mSeekLineTextColor = Color.CYAN;
    /**
     * 拖動歌詞時,展示當前高亮歌詞的時間的字體大小默認值
     **/
    private int mSeekLineTextSize = 15;
    /**
     * 拖動歌詞時,展示當前高亮歌詞的時間的字體大小最小值
     **/
    private int mMinSeekLineTextSize = 13;
    /**
     * 拖動歌詞時,展示當前高亮歌詞的時間的字體大小最大值
     **/
    private int mMaxSeekLineTextSize = 18;

    /**
     * 歌詞字體大小默認值
     **/
    private int mLrcFontSize = 23;    // font size of lrc
    /**
     * 歌詞字體大小最小值
     **/
    private int mMinLrcFontSize = 15;
    /**
     * 歌詞字體大小最大值
     **/
    private int mMaxLrcFontSize = 35;

    /**
     * 兩行歌詞之間的間距
     **/
    private int mPaddingY = 10;
    /**
     * 拖動歌詞時,在當前高亮歌詞下面的一條直線的起始位置
     **/
    private int mSeekLinePaddingX = 0;

    /**
     * 拖動歌詞的監聽類,回調LrcViewListener類的onLrcSeeked方法
     **/
    private ILrcViewListener mLrcViewListener;

    /**
     * 當沒有歌詞的時候展示的內容
     **/
    private String mLoadingLrcTip = "Downloading lrc...";

    private Paint mPaint;

    public LrcView(Context context, AttributeSet attr) {
        super(context, attr);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setTextSize(mLrcFontSize);
    }

    public void setListener(ILrcViewListener l) {
        mLrcViewListener = l;
    }

    public void setLoadingTipText(String text) {
        mLoadingLrcTip = text;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        final int height = getHeight(); // height of this view
        final int width = getWidth(); // width of this view
        //當沒有歌詞的時候
        if (mLrcRows == null || mLrcRows.size() == 0) {
            if (mLoadingLrcTip != null) {
                // draw tip when no lrc.
                mPaint.setColor(mHignlightRowColor);
                mPaint.setTextSize(mLrcFontSize);
                mPaint.setTextAlign(Align.CENTER);
                canvas.drawText(mLoadingLrcTip, width / 2, height / 2 - mLrcFontSize, mPaint);
            }
            return;
        }

        int rowY = 0; // vertical point of each row.
        final int rowX = width / 2;
        int rowNum = 0;
        /**
         * 分以下三步來繪制歌詞:
         *
         *  第1步:高亮地畫出正在播放的那句歌詞
         *  第2步:畫出正在播放的那句歌詞的上面可以展示出來的歌詞
         *  第3步:畫出正在播放的那句歌詞的下面的可以展示出來的歌詞
         */
        // 1、 高亮地畫出正在要高亮的的那句歌詞
        String highlightText = mLrcRows.get(mHignlightRow).content;
        int highlightRowY = height / 2 - mLrcFontSize;
        mPaint.setColor(mHignlightRowColor);
        mPaint.setTextSize(mLrcFontSize);
        mPaint.setTextAlign(Align.CENTER);
        canvas.drawText(highlightText, rowX, highlightRowY, mPaint);

        // 上下拖動歌詞的時候 畫出拖動要高亮的那句歌詞的時間 和 高亮的那句歌詞下面的一條直線
        if (mDisplayMode == DISPLAY_MODE_SEEK) {
            // 畫出高亮的那句歌詞下面的一條直線
            mPaint.setColor(mSeekLineColor);
            //該直線的x坐標從0到屏幕寬度  y坐標為高亮歌詞和下一行歌詞中間
            canvas.drawLine(mSeekLinePaddingX, highlightRowY + mPaddingY, width - mSeekLinePaddingX, highlightRowY + mPaddingY, mPaint);

            // 畫出高亮的那句歌詞的時間
            mPaint.setColor(mSeekLineTextColor);
            mPaint.setTextSize(mSeekLineTextSize);
            mPaint.setTextAlign(Align.LEFT);
            canvas.drawText(mLrcRows.get(mHignlightRow).strTime, 0, highlightRowY, mPaint);
        }

        // 2、畫出正在播放的那句歌詞的上面可以展示出來的歌詞
        mPaint.setColor(mNormalRowColor);
        mPaint.setTextSize(mLrcFontSize);
        mPaint.setTextAlign(Align.CENTER);
        rowNum = mHignlightRow - 1;
        rowY = highlightRowY - mPaddingY - mLrcFontSize;
        //只畫出正在播放的那句歌詞的上一句歌詞
//        if (rowY > -mLrcFontSize && rowNum >= 0) {
//            String text = mLrcRows.get(rowNum).content;
//            canvas.drawText(text, rowX, rowY, mPaint);
//        }

        //畫出正在播放的那句歌詞的上面所有的歌詞
        while( rowY > -mLrcFontSize && rowNum >= 0){
            String text = mLrcRows.get(rowNum).content;
            canvas.drawText(text, rowX, rowY, mPaint);
            rowY -=  (mPaddingY + mLrcFontSize);
            rowNum --;
        }

        // 3、畫出正在播放的那句歌詞的下面的可以展示出來的歌詞
        rowNum = mHignlightRow + 1;
        rowY = highlightRowY + mPaddingY + mLrcFontSize;

        //只畫出正在播放的那句歌詞的下一句歌詞
//        if (rowY < height && rowNum < mLrcRows.size()) {
//            String text2 = mLrcRows.get(rowNum).content;
//            canvas.drawText(text2, rowX, rowY, mPaint);
//        }

        //畫出正在播放的那句歌詞的所有下面的可以展示出來的歌詞
        while( rowY < height && rowNum < mLrcRows.size()){
            String text = mLrcRows.get(rowNum).content;
            canvas.drawText(text, rowX, rowY, mPaint);
            rowY += (mPaddingY + mLrcFontSize);
            rowNum ++;
        }

    }

    /**
     * 設置要高亮的歌詞為第幾行歌詞
     *
     * @param position 要高亮的歌詞行數
     * @param cb       是否是手指拖動後要高亮的歌詞
     */
    public void seekLrc(int position, boolean cb) {
        if (mLrcRows == null || position < 0 || position > mLrcRows.size()) {
            return;
        }
        LrcRow lrcRow = mLrcRows.get(position);
        mHignlightRow = position;
        invalidate();
        //如果是手指拖動歌詞後
        if (mLrcViewListener != null && cb) {
            //回調onLrcSeeked方法,將音樂播放器播放的位置移動到高亮歌詞的位置
            mLrcViewListener.onLrcSeeked(position, lrcRow);
        }
    }

    private float mLastMotionY;
    /**
     * 第一個手指的坐標
     **/
    private PointF mPointerOneLastMotion = new PointF();
    /**
     * 第二個手指的坐標
     **/
    private PointF mPointerTwoLastMotion = new PointF();
    /**
     * 是否是第一次移動,當一個手指按下後開始移動的時候,設置為true,
     * 當第二個手指按下的時候,即兩個手指同時移動的時候,設置為false
     */
    private boolean mIsFirstMove = false;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mLrcRows == null || mLrcRows.size() == 0) {
            return super.onTouchEvent(event);
        }
        switch (event.getAction()) {
            //手指按下
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "down,mLastMotionY:" + mLastMotionY);
                mLastMotionY = event.getY();
                mIsFirstMove = true;
                invalidate();
                break;
            //手指移動
            case MotionEvent.ACTION_MOVE:
                if (event.getPointerCount() == 2) {
                    Log.d(TAG, "two move");
                    doScale(event);
                    return true;
                }
                Log.d(TAG, "one move");
                // single pointer mode ,seek
                //如果是雙指同時按下,進行歌詞大小縮放,抬起其中一個手指,另外一個手指不離開屏幕地移動的話,不做任何處理
                if (mDisplayMode == DISPLAY_MODE_SCALE) {
                    //if scaling but pointer become not two ,do nothing.
                    return true;
                }
                //如果一個手指按下,在屏幕上移動的話,拖動歌詞上下
                doSeek(event);
                break;
            case MotionEvent.ACTION_CANCEL:
                //手指抬起
            case MotionEvent.ACTION_UP:
                if (mDisplayMode == DISPLAY_MODE_SEEK) {
                    //高亮手指抬起時的歌詞並播放從該句歌詞開始播放
                    seekLrc(mHignlightRow, true);
                }
                mDisplayMode = DISPLAY_MODE_NORMAL;
                invalidate();
                break;
        }
        return true;
    }

    /**
     * 處理雙指在屏幕移動時的,歌詞大小縮放
     */
    private void doScale(MotionEvent event) {
        //如果歌詞的模式為:拖動歌詞模式
        if (mDisplayMode == DISPLAY_MODE_SEEK) {
            //如果是單指按下,在進行歌詞上下滾動,然後按下另外一個手指,則把歌詞模式從 拖動歌詞模式 變為 縮放歌詞模式
            mDisplayMode = DISPLAY_MODE_SCALE;
            Log.d(TAG, "change mode from DISPLAY_MODE_SEEK to DISPLAY_MODE_SCALE");
            return;
        }
        // two pointer mode , scale font
        if (mIsFirstMove) {
            mDisplayMode = DISPLAY_MODE_SCALE;
            invalidate();
            mIsFirstMove = false;
            //兩個手指的x坐標和y坐標
            setTwoPointerLocation(event);
        }
        //獲取歌詞大小要縮放的比例
        int scaleSize = getScale(event);
        Log.d(TAG, "scaleSize:" + scaleSize);
        //如果縮放大小不等於0,進行縮放,重繪LrcView
        if (scaleSize != 0) {
            setNewFontSize(scaleSize);
            invalidate();
        }
        setTwoPointerLocation(event);
    }

    /**
     * 處理單指在屏幕移動時,歌詞上下滾動
     */
    private void doSeek(MotionEvent event) {
        float y = event.getY();//手指當前位置的y坐標
        float offsetY = y - mLastMotionY; //第一次按下的y坐標和目前移動手指位置的y坐標之差
        //如果移動距離小於10,不做任何處理
        if (Math.abs(offsetY) < mMinSeekFiredOffset) {
            return;
        }
        //將模式設置為拖動歌詞模式
        mDisplayMode = DISPLAY_MODE_SEEK;
        int rowOffset = Math.abs((int) offsetY / mLrcFontSize); //歌詞要滾動的行數

        Log.d(TAG, "move to new hightlightrow : " + mHignlightRow + " offsetY: " + offsetY + " rowOffset:" + rowOffset);

        if (offsetY < 0) {
            //手指向上移動,歌詞向下滾動
            mHignlightRow += rowOffset;//設置要高亮的歌詞為 當前高亮歌詞 向下滾動rowOffset行後的歌詞
        } else if (offsetY > 0) {
            //手指向下移動,歌詞向上滾動
            mHignlightRow -= rowOffset;//設置要高亮的歌詞為 當前高亮歌詞 向上滾動rowOffset行後的歌詞
        }
        //設置要高亮的歌詞為0和mHignlightRow中的較大值,即如果mHignlightRow < 0,mHignlightRow=0
        mHignlightRow = Math.max(0, mHignlightRow);
        //設置要高亮的歌詞為0和mHignlightRow中的較小值,即如果mHignlight > RowmLrcRows.size()-1,mHignlightRow=mLrcRows.size()-1
        mHignlightRow = Math.min(mHignlightRow, mLrcRows.size() - 1);
        //如果歌詞要滾動的行數大於0,則重畫LrcView
        if (rowOffset > 0) {
            mLastMotionY = y;
            invalidate();
        }
    }

    /**
     * 設置當前兩個手指的x坐標和y坐標
     */
    private void setTwoPointerLocation(MotionEvent event) {
        mPointerOneLastMotion.x = event.getX(0);
        mPointerOneLastMotion.y = event.getY(0);
        mPointerTwoLastMotion.x = event.getX(1);
        mPointerTwoLastMotion.y = event.getY(1);
    }

    /**
     * 設置縮放後的字體大小
     */
    private void setNewFontSize(int scaleSize) {
        //設置歌詞縮放後的的最新字體大小
        mLrcFontSize += scaleSize;
        mLrcFontSize = Math.max(mLrcFontSize, mMinLrcFontSize);
        mLrcFontSize = Math.min(mLrcFontSize, mMaxLrcFontSize);

        //設置歌詞的最新字體大小
        mSeekLineTextSize += scaleSize;
        mSeekLineTextSize = Math.max(mSeekLineTextSize, mMinSeekLineTextSize);
        mSeekLineTextSize = Math.min(mSeekLineTextSize, mMaxSeekLineTextSize);
    }

    /**
     * 獲取歌詞大小要縮放的比例
     */
    private int getScale(MotionEvent event) {
        Log.d(TAG, "scaleSize getScale");
        float x0 = event.getX(0);
        float y0 = event.getY(0);
        float x1 = event.getX(1);
        float y1 = event.getY(1);

        float maxOffset = 0; // max offset between x or y axis,used to decide scale size

        boolean zoomin = false;
        //第一次雙指之間的x坐標的差距
        float oldXOffset = Math.abs(mPointerOneLastMotion.x - mPointerTwoLastMotion.x);
        //第二次雙指之間的x坐標的差距
        float newXoffset = Math.abs(x1 - x0);

        //第一次雙指之間的y坐標的差距
        float oldYOffset = Math.abs(mPointerOneLastMotion.y - mPointerTwoLastMotion.y);
        //第二次雙指之間的y坐標的差距
        float newYoffset = Math.abs(y1 - y0);

        //雙指移動之後,判斷雙指之間移動的最大差距
        maxOffset = Math.max(Math.abs(newXoffset - oldXOffset), Math.abs(newYoffset - oldYOffset));
        //如果x坐標移動的多一些
        if (maxOffset == Math.abs(newXoffset - oldXOffset)) {
            //如果第二次雙指之間的x坐標的差距大於第一次雙指之間的x坐標的差距則是放大,反之則縮小
            zoomin = newXoffset > oldXOffset ? true : false;
        }
        //如果y坐標移動的多一些
        else {
            //如果第二次雙指之間的y坐標的差距大於第一次雙指之間的y坐標的差距則是放大,反之則縮小
            zoomin = newYoffset > oldYOffset ? true : false;
        }
        Log.d(TAG, "scaleSize maxOffset:" + maxOffset);
        if (zoomin) {
            return (int) (maxOffset / 10);//放大雙指之間移動的最大差距的1/10
        } else {
            return -(int) (maxOffset / 10);//縮小雙指之間移動的最大差距的1/10
        }
    }

    /**
     * 設置歌詞行集合
     * @param lrcRows
     */
    public void setLrc(List lrcRows) {
        mLrcRows = lrcRows;
        invalidate();
    }

    /**
     * 播放的時候調用該方法滾動歌詞,高亮正在播放的那句歌詞
     * @param time
     */
    public void seekLrcToTime(long time) {
        if (mLrcRows == null || mLrcRows.size() == 0) {
            return;
        }
        if (mDisplayMode != DISPLAY_MODE_NORMAL) {
            return;
        }
        Log.d(TAG, "seekLrcToTime:" + time);

        for (int i = 0; i < mLrcRows.size(); i++) {
            LrcRow current = mLrcRows.get(i);
            LrcRow next = i + 1 == mLrcRows.size() ? null : mLrcRows.get(i + 1);
            /**
             *  正在播放的時間大於current行的歌詞的時間而小於next行歌詞的時間, 設置要高亮的行為current行
             *  正在播放的時間大於current行的歌詞,而current行為最後一句歌詞時,設置要高亮的行為current行
             */
            if ((time >= current.time && next != null && time < next.time)
                    || (time > current.time && next == null)){
                seekLrc(i, false);
                return;
            }
        }
    }
}

以上就是自定義LrcView的全部內容,下面將該自定義LrcView放在布局文件activity_main.xml中去顯示出來。

activity_main.xml的代碼如下所示:



    

MainActivity代碼如下所示:

然後通過MainActivity來加載該布局,並在MainActivity中播放音樂,MainActivity的代碼如下所示:

package com.oyp.lrc;

import android.app.Activity;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.os.Bundle;
import android.util.Log;

import com.oyp.lrc.view.ILrcBuilder;
import com.oyp.lrc.view.ILrcView;
import com.oyp.lrc.view.ILrcViewListener;
import com.oyp.lrc.view.impl.DefaultLrcBuilder;
import com.oyp.lrc.view.impl.LrcRow;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

public class MainActivity extends Activity {

    public final static String TAG = "MainActivity";

    //自定義LrcView,用來展示歌詞
    ILrcView mLrcView;
    //更新歌詞的頻率,每秒更新一次
    private int mPalyTimerDuration = 1000;
    //更新歌詞的定時器
    private Timer mTimer;
    //更新歌詞的定時任務
    private TimerTask mTask;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //獲取自定義的LrcView
        setContentView(R.layout.activity_main);
        mLrcView=(ILrcView)findViewById(R.id.lrcView);

        //從assets目錄下讀取歌詞文件內容
        String lrc = getFromAssets("test.lrc");
        //解析歌詞構造器
        ILrcBuilder builder = new DefaultLrcBuilder();
        //解析歌詞返回LrcRow集合
        List rows = builder.getLrcRows(lrc);
        //將得到的歌詞集合傳給mLrcView用來展示
        mLrcView.setLrc(rows);

        //開始播放歌曲並同步展示歌詞
        beginLrcPlay();

        //設置自定義的LrcView上下拖動歌詞時監聽
        mLrcView.setListener(new ILrcViewListener() {
            //當歌詞被用戶上下拖動的時候回調該方法,從高亮的那一句歌詞開始播放
            public void onLrcSeeked(int newPosition, LrcRow row) {
                if (mPlayer != null) {
                    Log.d(TAG, "onLrcSeeked:" + row.time);
                    mPlayer.seekTo((int) row.time);
                }
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mPlayer != null) {
            mPlayer.stop();
        }
    }

    /**
     * 從assets目錄下讀取歌詞文件內容
     * @param fileName
     * @return
     */
    public String getFromAssets(String fileName){
        try {
            InputStreamReader inputReader = new InputStreamReader( getResources().getAssets().open(fileName) );
            BufferedReader bufReader = new BufferedReader(inputReader);
            String line="";
            String result="";
            while((line = bufReader.readLine()) != null){
                if(line.trim().equals(""))
                    continue;
                result += line + "\r\n";
            }
            return result;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }

    MediaPlayer mPlayer;

    /**
     * 開始播放歌曲並同步展示歌詞
     */
    public void beginLrcPlay(){
        mPlayer = new MediaPlayer();
        try {
            mPlayer.setDataSource(getAssets().openFd("test.mp3").getFileDescriptor());
            //准備播放歌曲監聽
            mPlayer.setOnPreparedListener(new OnPreparedListener() {
                //准備完畢
                public void onPrepared(MediaPlayer mp) {
                    mp.start();
                    if(mTimer == null){
                        mTimer = new Timer();
                        mTask = new LrcTask();
                        mTimer.scheduleAtFixedRate(mTask, 0, mPalyTimerDuration);
                    }
                }
            });
            //歌曲播放完畢監聽
            mPlayer.setOnCompletionListener(new OnCompletionListener() {
                public void onCompletion(MediaPlayer mp) {
                    stopLrcPlay();
                }
            });
            //准備播放歌曲
            mPlayer.prepare();
            //開始播放歌曲
            mPlayer.start();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalStateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * 停止展示歌曲
     */
    public void stopLrcPlay(){
        if(mTimer != null){
            mTimer.cancel();
            mTimer = null;
        }
    }

    /**
     * 展示歌曲的定時任務
     */
    class LrcTask extends TimerTask{
        @Override
        public void run() {
            //獲取歌曲播放的位置
            final long timePassed = mPlayer.getCurrentPosition();
            MainActivity.this.runOnUiThread(new Runnable() {
                public void run() {
                    //滾動歌詞
                    mLrcView.seekLrcToTime(timePassed);
                }
            });

        }
    };
}

下面是項目的結構圖。
這裡寫圖片描述

四、項目源碼地址

CSDN下載
項目的源代碼可以從下面的地址去免費下載:
http://download.csdn.net/detail/qq446282412/9453906 Github下載
https://github.com/ouyangpeng/android-lrc-view-oyp    

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved