Android項(xiàng)目實(shí)戰(zhàn)之高仿網(wǎng)易云音樂LRC歌詞

大家好,我們是愛學(xué)啊,今天給大家?guī)硪黄P(guān)于LRC歌詞原理和在Android上如何實(shí)現(xiàn)歌詞逐行滾動(dòng)的效果,本文來自【Android開發(fā)項(xiàng)目實(shí)戰(zhàn)我的云音樂】課程;逐字滾動(dòng)下一篇文章講解。

效果圖

相信大家都懂一張圖勝過千言萬語。

lrc1.gif

<img src="http://7xqoji.com1.z0.glb.clouddn.com/mytest.jpg" width="500" hegiht="313" align=center />

效果和現(xiàn)在市面上大部分播放器差不多,當(dāng)然如果要運(yùn)用到商業(yè)項(xiàng)目中,肯定還需要進(jìn)行一些優(yōu)化,例如:滾動(dòng)效果有彈性,字體大小,字體顏色等。

什么是LRC歌詞

LRC是英文Lyric(歌詞)的縮寫,常用作逐行歌詞擴(kuò)展名。他是純文本文件,格式簡(jiǎn)單,能實(shí)現(xiàn)歌詞逐行滾動(dòng);當(dāng)然目前業(yè)界大部分播放器都是在他的基礎(chǔ)上定制了,但基本原理一樣,當(dāng)學(xué)完我們這篇文章后,大家也可以根據(jù)自己的需求定制。

LRC歌詞格式

在實(shí)現(xiàn)歌詞功能前,肯定需要搞明白LRC歌詞格式,例如:我們找了一段LRC歌詞:

[ti:愛的代價(jià)]
[ar:李宗盛]
[al:滾石香港黃金十年 李宗盛精選]
[ly:李宗盛]
[mu:李宗盛]
[ma:]
[pu:]
[by:ttpod]
[total:272073]
[offset:0]
[00:00.300]愛的代價(jià) - 李宗盛
[00:01.979]作詞:李宗盛
[00:03.312]作曲:李宗盛
[00:06.429]
[00:16.282]還記得年少時(shí)的夢(mèng)嗎
[00:20.575]像朵永遠(yuǎn)不調(diào)零的花
[00:24.115]陪我經(jīng)過那風(fēng)吹雨打
[00:27.921]看世事無常
[00:29.653]看滄桑變化
[00:32.576]那些為愛所付出的代價(jià)
[00:36.279]是永遠(yuǎn)都難忘的啊
[00:40.485]所有真心的癡心的話

可以看到內(nèi)容是用換行符分割的,如果這些數(shù)據(jù)是通過接口返回,而不是直接返回一個(gè)LRC文件,那么這里面的換行符應(yīng)該變?yōu)閈n換行符,這一點(diǎn)我們也在課程中講解到了。

每一行是一句歌詞;每一行歌詞又分為兩部分,中括號(hào)里面是當(dāng)前這行歌詞的開始時(shí)間,格式為分:秒:毫秒,有些歌詞可能沒有毫秒,只有秒;歌詞開頭由于部分?jǐn)?shù)據(jù)稱為L(zhǎng)RC元數(shù)據(jù),他是用來描述這個(gè)歌詞的,部分字段解釋如下:

ti:title,標(biāo)題,通常是歌曲名稱
ar:artist,藝人名
al:album,專輯名
by:歌詞創(chuàng)建人,這里是ttpod,指的是天天動(dòng)聽
total:整首歌曲時(shí)長(zhǎng),單位毫秒
offset:時(shí)間補(bǔ)償值,單位毫秒,正值表示整體提前,負(fù)值相反

前面這些字段根據(jù)不同的播放器可能用的位置不一樣,我們課程中雖然解析了這些字段,但也沒有用到。

歌詞滾動(dòng)原理

將每行歌詞前面的時(shí)間解析后,轉(zhuǎn)為毫秒,這樣播放器在播放的時(shí)候可以獲取到播放時(shí)間,然后拿著時(shí)間查找當(dāng)前時(shí)間對(duì)應(yīng)哪一行歌詞,然后在界面上高亮這一行歌詞,或者做更多的處理,例如:字體增大等操作;就實(shí)現(xiàn)了歌詞逐行高亮;至于滾動(dòng)不同的平臺(tái)不一樣,滾動(dòng)思路是:獲取到當(dāng)前時(shí)間所對(duì)應(yīng)哪一行,然后我們肯定能算出每一行歌詞高度,所以行*每一行高度就是滾動(dòng)的高度。

歌詞解析

不同的語言語法不一樣,我們這里先說思路,我們的實(shí)現(xiàn)是Java語言。

讀取該文件每一行,然后用]拆分,第二部分就是歌詞,第一部分繼續(xù)用:拆分,然后將三部分轉(zhuǎn)為毫秒;最后將這些信息保存到對(duì)象上。

當(dāng)然為了以后更好的擴(kuò)展,因?yàn)楦柙~格式很多,可以進(jìn)行一些架構(gòu):

String[] strings = content.split("\n");

lyric = new Lyric();

TreeMap<Integer, Line> lyrics = new TreeMap<>();
Map<String, Object> tags = new HashMap<>();

String lineInfo=null;
int lineNumber = 0;
for (int i = 0; i < strings.length; i++) {
    try {
        lineInfo=strings[i];
        Line line = parserLine(tags, lineInfo);
        if (line != null) {
            lyrics.put(lineNumber, line);
            lineNumber++;
        }
    } catch (Exception var9) {
        var9.printStackTrace();
    }
}

lyric.setLyrics(lyrics);
lyric.setTags(tags);

/**
 * 解析每一行歌詞
 */
private Line parserLine(Map<String, Object> tags, String lineInfo) {
    if (lineInfo.startsWith("[0")) {
        //歌詞開始了
        Line line = new Line();

        int leftIndex = 1;
        int rightIndex = lineInfo.length();
        String[] lineComments = lineInfo.substring(leftIndex, rightIndex).split("]", -1);

        //開始時(shí)間
        String startTimeStr = lineComments[0];
        int startTime = TimeUtil.parseInteger(startTimeStr);
        line.setStartTime(startTime);

        //歌詞
        String lineLyricsStr = lineComments[1];
        line.setLineLyrics(lineLyricsStr);

        return line;
    }

    return null;
}

歌詞繪制

不同的平臺(tái)也不一樣,我們這里是Android,所以繪制用Canvas。我們這里的思路是:歌詞View的高度是固定的,由于我們希望當(dāng)前行歌詞始終顯示到歌詞View中間,所以先算出View的中心高度,然后在該位置繪制當(dāng)前行歌詞,這一步根據(jù)不同的歌詞處理的邏輯也不一樣,但歌詞可分為兩類,一類是逐行,一類是逐字,對(duì)于逐行來說就直接繪制就行了,只是顏色,大小不一樣而已;逐字下一節(jié)講解;然后從當(dāng)前行歌詞位置像前繪制歌詞,直到超出View頂部為止,在從當(dāng)前行歌詞向下歌詞繪制,直到超出View底部為止;當(dāng)前你可以使用LinearLayout添加所有歌詞當(dāng)前容器內(nèi),然后滾動(dòng)。

private void drawLyricText(Canvas canvas) {
    //在當(dāng)前位置繪制正在演唱的歌詞
    Line line = lyricsLines.get(lineNumber);

    //當(dāng)前歌詞的寬高
    float textWidth = getTextWidth(backgroundTextPaint, line.getLineLyrics());
    float textHeight = getTextHeight(backgroundTextPaint);

    float centerY = (getMeasuredHeight() - textHeight) / 2 + lineNumber * getLineHeight(backgroundTextPaint) - offsetY;

    float x = (getMeasuredWidth() - textWidth) / 2;
    float y = centerY;

    //當(dāng)前歌詞高亮
    if (lyric.isAccurate()) {
        //TODO 精確到字,歌詞,下一節(jié)講解
    } else {
        //精確到行
        canvas.drawText(line.getLineLyrics(), x, y, foregroundTextPaint);
    }


    //繪制前面的歌詞
    for (int i = lineNumber - 1; i > 0; i--) {
        //從當(dāng)前行的上一行開始繪制
        line = lyricsLines.get(i);

        //當(dāng)前歌詞的寬高
        textWidth = getTextWidth(backgroundTextPaint, line.getLineLyrics());
        textHeight = getTextHeight(backgroundTextPaint);


        x = (getMeasuredWidth() - textWidth) / 2;
        y = centerY - (lineNumber - i) * getLineHeight(backgroundTextPaint);

        if (y < getLineHeight(backgroundTextPaint)) {
            //超出了View頂部,不再繪制
            break;
        }

        canvas.drawText(line.getLineLyrics(), x, y, backgroundTextPaint);
    }

    //繪制后面的歌詞
    for (int i = lineNumber + 1; i < lyricsLines.size(); i++) {
        //從當(dāng)前行的下一行開始繪制
        line = lyricsLines.get(i);

        //當(dāng)前歌詞的寬高
        textWidth = getTextWidth(backgroundTextPaint, line.getLineLyrics());
        textHeight = getTextHeight(backgroundTextPaint);


        x = (getMeasuredWidth() - textWidth) / 2;
        y = centerY + (i - lineNumber) * getLineHeight(backgroundTextPaint);

        if (y + getLineHeight(backgroundTextPaint) > getHeight()) {
            //超出了View底部,不再繪制
            break;
        }

        canvas.drawText(line.getLineLyrics(), x, y, backgroundTextPaint);
    }

}

歌詞滾動(dòng)

Android中不同的實(shí)現(xiàn)方法滾動(dòng)方式也不一樣,如果是直接繪制,那么滾動(dòng)其實(shí)就是繪制不同行歌詞,給人的感覺就是滾動(dòng)了;如果是將所有歌詞添加到容器中,那么就可以使用容器默認(rèn)的滾動(dòng);對(duì)于我們這里的實(shí)現(xiàn)滾動(dòng)其實(shí)就是更改lineNumber值,例如;當(dāng)前l(fā)ineNumber為5,表示當(dāng)前播放的是第5行歌詞,通過用戶滾動(dòng)的距離就能計(jì)算出當(dāng)前滾動(dòng)距離是哪一行,因?yàn)槲覀冎烂恳恍懈叨人钥梢杂?jì)算出當(dāng)前位置,滾動(dòng)到的位置,然后使用屬性動(dòng)畫滾動(dòng):

if (valueAnimator != null && valueAnimator.isRunning()) {
    valueAnimator.cancel();
}
valueAnimator = ValueAnimator.ofFloat(offsetY, distanceY);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator valueAnimator) {
        offsetY = (float) valueAnimator.getAnimatedValue();
        invalidate();
    }
});

valueAnimator.setDuration(200);
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.start();

到這里L(fēng)RC歌詞View核心功能基本就實(shí)現(xiàn)完成了,如果要深入學(xué)習(xí)可以查看我們的【Android開發(fā)項(xiàng)目實(shí)戰(zhàn)我的云音樂】課程,或者在線電子書【電子書】;同時(shí)大家也可以關(guān)注我們的微信公眾號(hào)【ixuea666】和Android開發(fā)交流群:702321063。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,466評(píng)論 25 708
  • 用兩張圖告訴你,為什么你的 App 會(huì)卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 12,970評(píng)論 2 59
  • 這一節(jié)我們來講解這個(gè)項(xiàng)目所用到的一些技術(shù),以及一些實(shí)現(xiàn)的效果圖,讓大家對(duì)該項(xiàng)目有一個(gè)整體的認(rèn)識(shí),推薦大家收藏該文章...
    愛學(xué)啊閱讀 7,067評(píng)論 6 86
  • 1、通過CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請(qǐng)求組件 FMDB本地?cái)?shù)據(jù)庫(kù)組件 SD...
    陽明AGI閱讀 16,018評(píng)論 3 119
  • 我是個(gè)超級(jí)懷舊的人。 恍恍惚惚又是一個(gè)夏天。 高考結(jié)束之后的很多天我都沒有去趟老家,好像很多次聽到奶奶的嘮叨“考完...
    小千的貓閱讀 223評(píng)論 1 3