Android自定義View之簡約風歌詞控件實戰指南

前言

最近重構瞭之前的音樂播放器,添加瞭許多功能,比如歌詞,下載功能等。這篇文章就讓我們聊聊歌詞控件的實現,先上效果圖,如果感覺海星,就繼續瞧下去!

看到這裡,估計你對這個控件還有點感興趣的吧,那接下來就讓我們來瞧瞧實現這個歌詞控件需要做些什麼!

一、 歌詞解析

首先,我們得知道正常的歌詞格式是怎樣的,大概是長這個樣子:

 1[ti:喜歡你]
 2[ar:.]
 3[al:]
 4[by:]
 5[offset:0]
 6[00:00.10]喜歡你 – G.E.M. 鄧紫棋 (Gem Tang)
 7[00:00.20]詞:黃傢駒
 8[00:00.30]曲:黃傢駒
 9[00:00.40]編曲:Lupo Groinig
10[00:00.50]
11[00:12.65]細雨帶風濕透黃昏的街道
12[00:18.61]抹去雨水雙眼無故地仰望
13[00:24.04]望向孤單的晚燈
14[00:26.91]
15[00:27.44]是那傷感的記憶
16[00:30.52]
17[00:34.12]再次泛起心裡無數的思念
18[00:39.28]
19[00:40.10]以往片刻歡笑仍掛在臉上
20[00:45.49]願你此刻可會知
21[00:48.23]
22[00:48.95]是我衷心的說聲
23[00:53.06]
24[00:54.35]喜歡你 那雙眼動人
25[00:59.35]
26[01:00.10]笑聲更迷人
27[01:02.37]
28[01:03.15]願再可 輕撫你
29[01:08.56]
30[01:09.35]那可愛面容
31[01:12.40]挽手說夢話
32[01:14.78]
33[01:15.48]像昨天 你共我
34[01:20.84]
35[01:26.32]滿帶理想的我曾經多沖動
36[01:32.45]屢怨與她相愛難有自由
37[01:37.82]願你此刻可會知
38[01:40.40]
39[01:41.25]是我衷心的說聲
40[01:44.81]
41[01:46.39]喜歡你 那雙眼動人
42[01:51.72]
43[01:52.42]笑聲更迷人
44[01:54.75]
45[01:55.48]願再可 輕撫你
46[02:00.93]
47[02:01.68]那可愛面容
48[02:03.99]
49[02:04.73]挽手說夢話
50[02:07.13]
51[02:07.82]像昨天 你共我
52[02:14.53]
53[02:25.54]每晚夜裡自我獨行
54[02:29.30]隨處蕩 多冰冷
55[02:35.40]
56[02:37.83]以往為瞭自我掙紮
57[02:41.62]從不知 她的痛苦
58[02:52.02]
59[02:54.11]喜歡你 那雙眼動人
60[03:00.13]笑聲更迷人
61[03:02.38]
62[03:03.14]願再可 輕撫你
63[03:08.77]
64[03:09.33]那可愛面容
65[03:11.71]
66[03:12.41]挽手說夢話
67[03:14.61]
68[03:15.45]像昨天 你共我

從上面可以看出這種格式前面是開始時間,從左往右一一對應分,秒,毫秒,後面就是歌詞。所以我們要創建一個實體類來保存每一句的歌詞信息。

1.歌詞實體類LrcBean

 1public class LrcBean {
 2    private String lrc;//歌詞
 3    private long start;//開始時間
 4    private long end;//結束時間
 5
 6    public String getLrc() {
 7        return lrc;
 8    }
 9
10    public void setLrc(String lrc) {
11        this.lrc = lrc;
12    }
13
14    public long getStart() {
15        return start;
16    }
17
18    public void setStart(long start) {
19        this.start = start;
20    }
21
22    public long getEnd() {
23        return end;
24    }
25
26    public void setEnd(long end) {
27        this.end = end;
28    }
29}

每句歌詞,我們需要開始時間,結束時間和歌詞這些信息,那麼你就會有疑問瞭?上面提到的歌詞格式好像隻有歌詞開始時間,那我們怎麼知道結束時間呢?其實很簡單,這一句歌詞的開始時間就是上一句歌詞的結束時間。有瞭歌詞實體類,我們就得開始對歌詞進行解析瞭!

2. 解析歌詞工具類LrcUtil

 1public class LrcUtil {
 2
 3    /**
 4     * 解析歌詞,將字符串歌詞封裝成LrcBean的集合
 5     * @param lrcStr 字符串的歌詞,歌詞有固定的格式,一般為
 6     * [ti:喜歡你]
 7     * [ar:.]
 8     * [al:]
 9     * [by:]
10     * [offset:0]
11     * [00:00.10]喜歡你 - G.E.M. 鄧紫棋 (Gem Tang)
12     * [00:00.20]詞:黃傢駒
13     * [00:00.30]曲:黃傢駒
14     * [00:00.40]編曲:Lupo Groinig
15     * @return 歌詞集合
16     */
17    public static List<LrcBean> parseStr2List(String lrcStr){
18        List<LrcBean> res = new ArrayList<>();
19        //根據轉行字符對字符串進行分割
20        String[] subLrc = lrcStr.split("\n");
21        //跳過前四行,從第五行開始,因為前四行的歌詞我們並不需要
22        for (int i = 5; i < subLrc.length; i++) {
23            String lineLrc = subLrc[i];
24            //[00:00.10]喜歡你 - G.E.M. 鄧紫棋 (Gem Tang)
25            String min = lineLrc.substring(lineLrc.indexOf("[")+1,lineLrc.indexOf("[")+3);
26            String sec = lineLrc.substring(lineLrc.indexOf(":")+1,lineLrc.indexOf(":")+3);
27            String mills = lineLrc.substring(lineLrc.indexOf(".")+1,lineLrc.indexOf(".")+3);
28            //進制轉化,轉化成毫秒形式的時間
29            long startTime = getTime(min,sec,mills);
30            //歌詞
31            String lrcText = lineLrc.substring(lineLrc.indexOf("]")+1);
32            //有可能是某個時間段是沒有歌詞,則跳過下面
33            if(lrcText.equals("")) continue;
34            //在第一句歌詞中有可能是很長的,我們隻截取一部分,即歌曲加演唱者
35            //比如 光年之外 (《太空旅客(Passengers)》電影中國區主題曲) - G.E.M. 鄧紫棋 (Gem Tang)
36            if (i == 5) {
37                int lineIndex = lrcText.indexOf("-");
38                int first = lrcText.indexOf("(");
39                if(first<lineIndex&&first!=-1){
40                    lrcText = lrcText.substring(0,first)+lrcText.substring(lineIndex);
41                }
42                LrcBean lrcBean = new LrcBean();
43                lrcBean.setStart(startTime);
44                lrcBean.setLrc(lrcText);
45                res.add(lrcBean);
46                continue;
47            }
48            //添加到歌詞集合中
49            LrcBean lrcBean = new LrcBean();
50            lrcBean.setStart(startTime);
51            lrcBean.setLrc(lrcText);
52            res.add(lrcBean);
53            //如果是最後一句歌詞,其結束時間是不知道的,我們將人為的設置為開始時間加上100s
54            if(i == subLrc.length-1){
55                res.get(res.size()-1).setEnd(startTime+100000);
56            }else if(res.size()>1){
57                //當集合數目大於1時,這句的歌詞的開始時間就是上一句歌詞的結束時間
58                res.get(res.size()-2).setEnd(startTime);
59            }
60
61        }
62        return res;
63    }
64
65    /**
66     *  根據時分秒獲得總時間
67     * @param min 分鐘
68     * @param sec 秒
69     * @param mills 毫秒
70     * @return 總時間
71     */
72    private static long getTime(String min,String sec,String mills){
73        return Long.valueOf(min)*60*1000+Long.valueOf(sec)*1000+Long.valueOf(mills);
74    }
75}

相信上面的代碼和註釋已經將這個歌詞解析解釋的挺明白瞭,需要註意的是上面對i=5,也就是歌詞真正開始的第一句做瞭特殊處理,因為i=5這句有可能是很長的,假設i=5是“光年之外

(《太空旅客(Passengers)》電影中國區主題曲) – G.E.M. 鄧紫棋 (Gem

Tang)”這句歌詞,如果我們不做特殊處理,在後面繪制的時候,就會發現這句歌詞會超過屏幕大小,很影響美觀,所以我們隻截取歌曲名和演唱者,有些說明直接省略掉瞭。解析好瞭歌詞,接下來就是重頭戲-歌詞繪制!

二、歌詞繪制

歌詞繪制就涉及到瞭自定義View的知識,所以還未接觸自定義View的小夥伴需要先去看看自定View的基礎知識。歌詞繪制的主要工作主要由下面幾部分構成:

  • 為歌詞控件設置自定義屬性,在構造方法中獲取並設置自定義屬性的默認值
  • 初始化兩支畫筆。分別是歌詞普通畫筆,歌詞高亮畫筆。
  • 獲取當前播放歌詞的位置
  • 畫歌詞,根據當前播放歌詞的位置來決定用哪支畫筆畫
  • 歌詞隨歌曲播放同步滑動
  • 重新繪制

1.設置自定View屬性,在代碼中設置默認值

在res文件中的values中新建一個attrs.xml文件,然後定義歌詞的自定義View屬性

1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3    <declare-styleable name="LrcView">
4        <attr name="highLineTextColor" format="color|reference|integer"/>
5        <attr name="lrcTextColor" format="color|reference|integer"/>
6        <attr name="lineSpacing" format="dimension"/>
7        <attr name="textSize" format="dimension"/>
8    </declare-styleable>
9</resources>

這裡隻自定義瞭歌詞顏色,歌詞高亮顏色,歌詞大小,歌詞行間距的屬性,可根據自己需要自行添加。

然後在Java代碼中,設置默認值。

 1    private int lrcTextColor;//歌詞顏色
 2    private int highLineTextColor;//當前歌詞顏色
 3    private int width, height;//屏幕寬高
 4    private int lineSpacing;//行間距
 5    private int textSize;//字體大小
 6
 7    public LrcView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
 8        super(context, attrs, defStyleAttr);
 9        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LrcView);
10        lrcTextColor = ta.getColor(R.styleable.LrcView_lrcTextColor, Color.GRAY);
11        highLineTextColor = ta.getColor(R.styleable.LrcView_highLineTextColor, Color.BLUE);
12        float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
13        float scale = context.getResources().getDisplayMetrics().density;
14        //默認字體大小為16sp
15        textSize = ta.getDimensionPixelSize(R.styleable.LrcView_textSize, (int) (16 * fontScale));
16        //默認行間距為30dp
17        lineSpacing = ta.getDimensionPixelSize(R.styleable.LrcView_lineSpacing, (int) (30 * scale));
18        //回收
19        ta.recycle();
20    }

2. 初始化兩支畫筆

 1    private void init() {
 2        //初始化歌詞畫筆
 3        dPaint = new Paint();
 4        dPaint.setStyle(Paint.Style.FILL);//填滿
 5        dPaint.setAntiAlias(true);//抗鋸齒
 6        dPaint.setColor(lrcTextColor);//畫筆顏色
 7        dPaint.setTextSize(textSize);//歌詞大小
 8        dPaint.setTextAlign(Paint.Align.CENTER);//文字居中
 9
10        //初始化當前歌詞畫筆
11        hPaint = new Paint();
12        hPaint.setStyle(Paint.Style.FILL);
13        hPaint.setAntiAlias(true);
14        hPaint.setColor(highLineTextColor);
15        hPaint.setTextSize(textSize);
16        hPaint.setTextAlign(Paint.Align.CENTER);
17    }

我們把初始化的方法放到瞭構造方法中,這樣就可以避免在重繪時再次初始化。另外由於我們把init方法隻放到瞭第三個構造方法中,所以在上面兩個構造方法需要將super改成this,這樣就能保證哪個構造方法都能執行init方法

 1    public LrcView(Context context) {
 2        this(context, null);
 3    }
 4
 5    public LrcView(Context context, @Nullable AttributeSet attrs) {
 6        this(context, attrs, 0);
 7    }
 8
 9    public LrcView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
10        super(context, attrs, defStyleAttr);
11        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LrcView);
12        ......
13        //回收
14        ta.recycle();
15        init();
16    }

3. 重復執行onDraw方法

因為後面的步驟都是在onDraw方法中執行的,所以我們先貼出onDraw方法中的代碼

 1    @Override
 2    protected void onDraw(Canvas canvas) {
 3        super.onDraw(canvas);
 4
 5        getMeasuredWidthAndHeight();//得到測量後的寬高
 6        getCurrentPosition();//得到當前歌詞的位置
 7        drawLrc(canvas);//畫歌詞
 8        scrollLrc();//歌詞滑動
 9        postInvalidateDelayed(100);//延遲0.1s刷新
10    }

1.獲得控件的測量後的寬高

1     private int width, height;//屏幕寬高
2    private void getMeasuredWidthAndHeight(){
3        if (width == 0 || height == 0) {
4            width = getMeasuredWidth();
5            height = getMeasuredHeight();
6        }
7    }

為什麼要獲得控件的寬高呢?因為在下面我們需要畫歌詞,畫歌詞時需要畫的位置,這時候就需要用到控件的寬高瞭。

2. 得到當前歌詞的位置

 1     private List<LrcBean> lrcBeanList;//歌詞集合
 2    private int currentPosition;//當前歌詞的位置
 3    private MediaPlayer player;//當前的播放器
 4
 5
 6    private void getCurrentPosition() {
 7        int curTime = player.getCurrentPosition();
 8        //如果當前的時間大於10分鐘,證明歌曲未播放,則當前位置應該為0
 9        if (curTime < lrcBeanList.get(0).getStart()||curTime>10*60*1000) {
10            currentPosition = 0;
11            return;
12        } else if (curTime > lrcBeanList.get(lrcBeanList.size() - 1).getStart()) {
13            currentPosition = lrcBeanList.size() - 1;
14            return;
15        }
16        for (int i = 0; i < lrcBeanList.size(); i++) {
17            if (curTime >= lrcBeanList.get(i).getStart() && curTime <= lrcBeanList.get(i).getEnd()) {
18                currentPosition = i;
19            }
20        }
21    }

我們根據當前播放的歌曲時間來遍歷歌詞集合,從而判斷當前播放的歌詞的位置。細心的你可能會發現在currentPosition = 0中有個curTime>10601000的判斷,這是因為在實際使用中發現當player還未播放時,這時候得到的curTime會很大,所以才有瞭這個判斷(因為正常的歌曲不會超過10分鐘)。

在這個方法我們會發現出現瞭歌詞集合和播放器,你可能會感到困惑,這些不是還沒賦值嗎?困惑就對瞭,所以我們需要提供外部方法來給外部傳給歌詞控件歌詞集合和播放器。

 1    //將歌詞集合傳給到這個自定義View中
 2    public LrcView setLrc(String lrc) {
 3        lrcBeanList = LrcUtil.parseStr2List(lrc);
 4        return this;
 5    }
 6
 7    //傳遞mediaPlayer給自定義View中
 8    public LrcView setPlayer(MediaPlayer player) {
 9        this.player = player;
10        return this;
11    }

外部方法中setLrc的參數必須是前面提到的標準歌詞格式的字符串形式,這樣我們就能利用上文的解析工具類LrcUtil中的解析方法將字符串解析成歌詞集合。

3. 畫歌詞

1     private void drawLrc(Canvas canvas) {
2        for (int i = 0; i < lrcBeanList.size(); i++) {
3            if (currentPosition == i) {//如果是當前的歌詞就用高亮的畫筆畫
4                canvas.drawText(lrcBeanList.get(i).getLrc(), width / 2, height / 2 + i * lineSpacing, hPaint);
5            } else {
6                canvas.drawText(lrcBeanList.get(i).getLrc(), width / 2, height / 2 + i * lineSpacing, dPaint);
7            }
8        }
9    }

知道瞭當前歌詞的位置就很容易畫歌詞瞭。遍歷歌詞集合,如果是當前歌詞,則用高亮的畫筆畫,其它歌詞就用普通畫筆畫。這裡需註意的是兩支畫筆畫的位置公式都是一樣的,坐標位置為x=寬的一半,y=高的一半+當前位置*行間距。隨著當前位置的變化,就能畫出上下句歌詞來。所以其實繪制出來後你會發現歌詞是從控件的正中央開始繪制的,這是為瞭方便與下面歌詞同步滑動功能配合。

4. 歌詞同步滑動

 1     //歌詞滑動
 2    private void scrollLrc() {
 3        //下一句歌詞的開始時間
 4        long startTime = lrcBeanList.get(currentPosition).getStart();
 5        long currentTime = player.getCurrentPosition();
 6
 7        //判斷是否換行,在0.5內完成滑動,即實現彈性滑動
 8        float y = (currentTime - startTime) > 500 ? currentPosition * lineSpacing : lastPosition * lineSpacing + (currentPosition - lastPosition) * lineSpacing * ((currentTime - startTime) / 500f);
 9        scrollTo(0,(int)y);
10        if (getScrollY() == currentPosition * lineSpacing) {
11            lastPosition = currentPosition;
12        }
13    }

如果不實現彈性滑動的話,隻要判斷當前播放歌曲的時間是否大於當前位置歌詞的結束時間,然後進行scrollTo(0,(int)currentPosition * lineSpacing)滑動即可。但是為瞭實現彈性滑動,我們需要將一次滑動分成若幹次小的滑動並在一個時間段內完成,所以我們動態設置y的值,由於不斷重繪,就能實現在0.5秒內完成View的滑動,這樣就能實現歌詞同步彈性滑動。

500其實就是0.5s,因為在這裡currentTime和startTime的單位都是ms

1        float y = (currentTime - startTime) > 500 ? currentPosition * lineSpacing : lastPosition * lineSpacing + (currentPosition - lastPosition) * lineSpacing * ((currentTime - startTime) / 500f);

5.不斷重繪

通過不斷重繪才能實現歌詞同步滑動,這裡每隔0.1s進行重繪

1postInvalidateDelayed(100);//延遲0.1s刷新

你以為這樣就結束瞭嗎?其實還沒有,答案下文揭曉!

三 、使用

然後我們興高采烈的在xml中,引用這個自定義View

LrcView前面的名稱為你建這個類的完整包名

1    <com.example.library.view.LrcView
2        android:id="@+id/lrcView"
3        android:layout_width="match_parent"
4        android:layout_height="match_parent"
5        app:lineSpacing="40dp"
6        app:textSize="18sp"
7        app:lrcTextColor="@color/colorPrimary"
8        app:highLineTextColor="@color/highTextColor"
9        />

在Java代碼中給這個自定義View傳入標準歌詞字符串和播放器。

1lrcView.setLrc(lrc).setPlayer(player);

點擊運行,滿心期待自己的成果,接著你就會一臉懵逼,what?怎麼是一片空白,什麼也沒有!其實這時候你重新理一下上面歌詞繪制的流程,就會發現問題所在。 首先我們的自定義View控件引用到佈局中時是先執行onDraw方法的,所以當你調用setLrc和setPlayer方法後,是不會再重新調用onDraw方法的,等於你並沒有傳入歌詞字符串和播放器,所以當然會顯示一片空白

解決方法 :我們在剛才自定義View歌詞控件中添加一個外部方法來調用onDraw,剛好這個invalidate()就能夠重新調用onDraw方法

1    public LrcView draw() {
2        currentPosition = 0;
3        lastPosition = 0;
4        invalidate();
5        return this;
6    }

然後我們在主代碼中,在調用setLrc和setPlayer後還得調用draw方法

1lrcView.setLrc(lrc).setPlayer(player).draw();

這樣我們節約風的歌詞控件就大功告成瞭。

總結

到此這篇關於Android自定義View之簡約風歌詞控件的文章就介紹到這瞭,更多相關Android簡約風歌詞控件內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: