LRC歌詞原理和實現高仿Android網易雲音樂

愛學啊發表於2018-10-16

大家好,我們是愛學啊,今天給大家帶來一篇關於LRC歌詞原理和在Android上如何實現歌詞逐行滾動的效果,本文來自【Android開發專案實戰我的雲音樂】課程;逐字滾動下一篇文章講解。

效果圖

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

LRC歌詞原理和實現高仿Android網易雲音樂

效果和現在市面上大部分播放器差不多,當然如果要運用到商業專案中,肯定還需要進行一些優化,例如:滾動效果有彈性,字型大小,字型顏色等。

什麼是LRC歌詞

LRC是英文Lyric(歌詞)的縮寫,常用作逐行歌詞副檔名。他是純文字檔案,格式簡單,能實現歌詞逐行滾動;當然目前業界大部分播放器都是在他的基礎上定製了,但基本原理一樣,當學完我們這篇文章後,大家也可以根據自己的需求定製。

LRC歌詞格式

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

[ti:愛的代價]
[ar:李宗盛]
[al:滾石香港黃金十年 李宗盛精選]
[ly:李宗盛]
[mu:李宗盛]
[ma:]
[pu:]
[by:ttpod]
[total:272073]
[offset:0]
[00:00.300]愛的代價 - 李宗盛
[00:01.979]作詞:李宗盛
[00:03.312]作曲:李宗盛
[00:06.429]
[00:16.282]還記得年少時的夢嗎
[00:20.575]像朵永遠不調零的花
[00:24.115]陪我經過那風吹雨打
[00:27.921]看世事無常
[00:29.653]看滄桑變化
[00:32.576]那些為愛所付出的代價
[00:36.279]是永遠都難忘的啊
[00:40.485]所有真心的痴心的話
複製程式碼

可以看到內容是用換行符分割的,如果這些資料是通過介面返回,而不是直接返回一個LRC檔案,那麼這裡面的換行符應該變為\n換行符,這一點我們也在課程中講解到了。

每一行是一句歌詞;每一行歌詞又分為兩部分,中括號裡面是當前這行歌詞的開始時間,格式為分:秒:毫秒,有些歌詞可能沒有毫秒,只有秒;歌詞開頭由於部分資料稱為LRC後設資料,他是用來描述這個歌詞的,部分欄位解釋如下:

ti:title,標題,通常是歌曲名稱
ar:artist,藝人名
al:album,專輯名
by:歌詞建立人,這裡是ttpod,指的是天天動聽
total:整首歌曲時長,單位毫秒
offset:時間補償值,單位毫秒,正值表示整體提前,負值相反
複製程式碼

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

歌詞滾動原理

將每行歌詞前面的時間解析後,轉為毫秒,這樣播放器在播放的時候可以獲取到播放時間,然後拿著時間查詢當前時間對應哪一行歌詞,然後在介面上高亮這一行歌詞,或者做更多的處理,例如:字型增大等操作;就實現了歌詞逐行高亮;至於滾動不同的平臺不一樣,滾動思路是:獲取到當前時間所對應哪一行,然後我們肯定能算出每一行歌詞高度,所以行*每一行高度就是滾動的高度。

歌詞解析

不同的語言語法不一樣,我們這裡先說思路,我們的實現是Java語言。

讀取該檔案每一行,然後用]拆分,第二部分就是歌詞,第一部分繼續用:拆分,然後將三部分轉為毫秒;最後將這些資訊儲存到物件上。

當然為了以後更好的擴充套件,因為歌詞格式很多,可以進行一些架構:

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);

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

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

        return line;
    }

    return null;
}
複製程式碼

歌詞繪製

不同的平臺也不一樣,我們這裡是Android,所以繪製用Canvas。我們這裡的思路是:歌詞View的高度是固定的,由於我們希望當前行歌詞始終顯示到歌詞View中間,所以先算出View的中心高度,然後在該位置繪製當前行歌詞,這一步根據不同的歌詞處理的邏輯也不一樣,但歌詞可分為兩類,一類是逐行,一類是逐字,對於逐行來說就直接繪製就行了,只是顏色,大小不一樣而已;逐字下一節講解;然後從當前行歌詞位置像前繪製歌詞,直到超出View頂部為止,在從當前行歌詞向下歌詞繪製,直到超出View底部為止;當前你可以使用LinearLayout新增所有歌詞當前容器內,然後滾動。

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

    //當前歌詞的寬高
    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;

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


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

        //當前歌詞的寬高
        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++) {
        //從當前行的下一行開始繪製
        line = lyricsLines.get(i);

        //當前歌詞的寬高
        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);
    }

}
複製程式碼

歌詞滾動

Android中不同的實現方法滾動方式也不一樣,如果是直接繪製,那麼滾動其實就是繪製不同行歌詞,給人的感覺就是滾動了;如果是將所有歌詞新增到容器中,那麼就可以使用容器預設的滾動;對於我們這裡的實現滾動其實就是更改lineNumber值,例如;當前lineNumber為5,表示當前播放的是第5行歌詞,通過使用者滾動的距離就能計算出當前滾動距離是哪一行,因為我們知道每一行高度所以可以計算出當前位置,滾動到的位置,然後使用屬性動畫滾動:

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();
複製程式碼

到這裡LRC歌詞View核心功能基本就實現完成了,如果要深入學習可以檢視我們的【Android開發專案實戰我的雲音樂】課程,或者線上電子書【電子書】;同時大家也可以關注我們的微信公眾號【ixuea666】和Android開發QQ交流群:702321063。

相關文章