溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Android怎么自定義View實(shí)現(xiàn)簡(jiǎn)約風(fēng)歌詞控件

發(fā)布時(shí)間:2022-03-30 10:55:20 來源:億速云 閱讀:150 作者:iii 欄目:移動(dòng)開發(fā)

本篇內(nèi)容介紹了“Android怎么自定義View實(shí)現(xiàn)簡(jiǎn)約風(fēng)歌詞控件”的有關(guān)知識(shí),在實(shí)際案例的操作過程中,不少人都會(huì)遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

先上效果圖

Android怎么自定義View實(shí)現(xiàn)簡(jiǎn)約風(fēng)歌詞控件

一、 歌詞解析

首先,我們得知道正常的歌詞格式是怎樣的,大概是長(zhǎng)這個(gè)樣子:

 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]細(xì)雨帶風(fēng)濕透黃昏的街道
12[00:18.61]抹去雨水雙眼無故地仰望
13[00:24.04]望向孤單的晚燈
14[00:26.91]
15[00:27.44]是那傷感的記憶
16[00:30.52]
17[00:34.12]再次泛起心里無數(shù)的思念
18[00:39.28]
19[00:40.10]以往片刻歡笑仍掛在臉上
20[00:45.49]愿你此刻可會(huì)知
21[00:48.23]
22[00:48.95]是我衷心的說聲
23[00:53.06]
24[00:54.35]喜歡你 那雙眼動(dòng)人
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]挽手說夢(mèng)話
32[01:14.78]
33[01:15.48]像昨天 你共我
34[01:20.84]
35[01:26.32]滿帶理想的我曾經(jīng)多沖動(dòng)
36[01:32.45]屢怨與她相愛難有自由
37[01:37.82]愿你此刻可會(huì)知
38[01:40.40]
39[01:41.25]是我衷心的說聲
40[01:44.81]
41[01:46.39]喜歡你 那雙眼動(dòng)人
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]挽手說夢(mèng)話
50[02:07.13]
51[02:07.82]像昨天 你共我
52[02:14.53]
53[02:25.54]每晚夜里自我獨(dú)行
54[02:29.30]隨處蕩 多冰冷
55[02:35.40]
56[02:37.83]以往為了自我掙扎
57[02:41.62]從不知 她的痛苦
58[02:52.02]
59[02:54.11]喜歡你 那雙眼動(dòng)人
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]挽手說夢(mèng)話
67[03:14.61]
68[03:15.45]像昨天 你共我

從上面可以看出這種格式前面是開始時(shí)間,從左往右一一對(duì)應(yīng)分,秒,毫秒,后面就是歌詞。所以我們要?jiǎng)?chuàng)建一個(gè)實(shí)體類來保存每一句的歌詞信息。

1.歌詞實(shí)體類LrcBean

 1public class LrcBean {
 2    private String lrc;//歌詞
 3    private long start;//開始時(shí)間
 4    private long end;//結(jié)束時(shí)間
 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}

每句歌詞,我們需要開始時(shí)間,結(jié)束時(shí)間和歌詞這些信息,那么你就會(huì)有疑問了?上面提到的歌詞格式好像只有歌詞開始時(shí)間,那我們?cè)趺粗澜Y(jié)束時(shí)間呢?其實(shí)很簡(jiǎn)單,這一句歌詞的開始時(shí)間就是上一句歌詞的結(jié)束時(shí)間。有了歌詞實(shí)體類,我們就得開始對(duì)歌詞進(jìn)行解析了!

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        //根據(jù)轉(zhuǎn)行字符對(duì)字符串進(jìn)行分割
20        String[] subLrc = lrcStr.split("
");
21        //跳過前四行,從第五行開始,因?yàn)榍八男械母柙~我們并不需要
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            //進(jìn)制轉(zhuǎn)化,轉(zhuǎn)化成毫秒形式的時(shí)間
29            long startTime = getTime(min,sec,mills);
30            //歌詞
31            String lrcText = lineLrc.substring(lineLrc.indexOf("]")+1);
32            //有可能是某個(gè)時(shí)間段是沒有歌詞,則跳過下面
33            if(lrcText.equals("")) continue;
34            //在第一句歌詞中有可能是很長(zhǎng)的,我們只截取一部分,即歌曲加演唱者
35            //比如 光年之外 (《太空旅客(Passengers)》電影中國區(qū)主題曲) - 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            //如果是最后一句歌詞,其結(jié)束時(shí)間是不知道的,我們將人為的設(shè)置為開始時(shí)間加上100s
54            if(i == subLrc.length-1){
55                res.get(res.size()-1).setEnd(startTime+100000);
56            }else if(res.size()>1){
57                //當(dāng)集合數(shù)目大于1時(shí),這句的歌詞的開始時(shí)間就是上一句歌詞的結(jié)束時(shí)間
58                res.get(res.size()-2).setEnd(startTime);
59            }
60
61        }
62        return res;
63    }
64
65    /**
66     *  根據(jù)時(shí)分秒獲得總時(shí)間
67     * @param min 分鐘
68     * @param sec 秒
69     * @param mills 毫秒
70     * @return 總時(shí)間
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}

相信上面的代碼和注釋已經(jīng)將這個(gè)歌詞解析解釋的挺明白了,需要注意的是上面對(duì)i=5,也就是歌詞真正開始的第一句做了特殊處理,因?yàn)閕=5這句有可能是很長(zhǎng)的,假設(shè)i=5是“光年之外

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

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

二、歌詞繪制

歌詞繪制就涉及到了自定義View的知識(shí),所以還未接觸自定義View的小伙伴需要先去看看自定View的基礎(chǔ)知識(shí)。歌詞繪制的主要工作主要由下面幾部分構(gòu)成:

  • 為歌詞控件設(shè)置自定義屬性,在構(gòu)造方法中獲取并設(shè)置自定義屬性的默認(rèn)值

  • 初始化兩支畫筆。分別是歌詞普通畫筆,歌詞高亮畫筆。

  • 獲取當(dāng)前播放歌詞的位置

  • 畫歌詞,根據(jù)當(dāng)前播放歌詞的位置來決定用哪支畫筆畫

  • 歌詞隨歌曲播放同步滑動(dòng)

  • 重新繪制

1.設(shè)置自定View屬性,在代碼中設(shè)置默認(rèn)值

在res文件中的values中新建一個(gè)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>

這里只自定義了歌詞顏色,歌詞高亮顏色,歌詞大小,歌詞行間距的屬性,可根據(jù)自己需要自行添加。

然后在Java代碼中,設(shè)置默認(rèn)值。

 1    private int lrcTextColor;//歌詞顏色
 2    private int highLineTextColor;//當(dāng)前歌詞顏色
 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        //默認(rèn)字體大小為16sp
15        textSize = ta.getDimensionPixelSize(R.styleable.LrcView_textSize, (int) (16 * fontScale));
16        //默認(rèn)行間距為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        //初始化當(dāng)前歌詞畫筆
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    }

我們把初始化的方法放到了構(gòu)造方法中,這樣就可以避免在重繪時(shí)再次初始化。另外由于我們把init方法只放到了第三個(gè)構(gòu)造方法中,所以在上面兩個(gè)構(gòu)造方法需要將super改成this,這樣就能保證哪個(gè)構(gòu)造方法都能執(zhí)行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. 重復(fù)執(zhí)行onDraw方法

因?yàn)楹竺娴牟襟E都是在onDraw方法中執(zhí)行的,所以我們先貼出onDraw方法中的代碼

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

1.獲得控件的測(cè)量后的寬高

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

為什么要獲得控件的寬高呢?因?yàn)樵谙旅嫖覀冃枰嫺柙~,畫歌詞時(shí)需要畫的位置,這時(shí)候就需要用到控件的寬高了。

2. 得到當(dāng)前歌詞的位置

 1     private List<LrcBean> lrcBeanList;//歌詞集合
 2    private int currentPosition;//當(dāng)前歌詞的位置
 3    private MediaPlayer player;//當(dāng)前的播放器
 4
 5
 6    private void getCurrentPosition() {
 7        int curTime = player.getCurrentPosition();
 8        //如果當(dāng)前的時(shí)間大于10分鐘,證明歌曲未播放,則當(dāng)前位置應(yīng)該為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    }

我們根據(jù)當(dāng)前播放的歌曲時(shí)間來遍歷歌詞集合,從而判斷當(dāng)前播放的歌詞的位置。細(xì)心的你可能會(huì)發(fā)現(xiàn)在currentPosition = 0中有個(gè)curTime>10601000的判斷,這是因?yàn)樵趯?shí)際使用中發(fā)現(xiàn)當(dāng)player還未播放時(shí),這時(shí)候得到的curTime會(huì)很大,所以才有了這個(gè)判斷(因?yàn)檎5母枨粫?huì)超過10分鐘)。

在這個(gè)方法我們會(huì)發(fā)現(xiàn)出現(xiàn)了歌詞集合和播放器,你可能會(huì)感到困惑,這些不是還沒賦值嗎?困惑就對(duì)了,所以我們需要提供外部方法來給外部傳給歌詞控件歌詞集合和播放器。

 1    //將歌詞集合傳給到這個(gè)自定義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的參數(shù)必須是前面提到的標(biāo)準(zhǔn)歌詞格式的字符串形式,這樣我們就能利用上文的解析工具類LrcUtil中的解析方法將字符串解析成歌詞集合。

3. 畫歌詞

1     private void drawLrc(Canvas canvas) {
2        for (int i = 0; i < lrcBeanList.size(); i++) {
3            if (currentPosition == i) {//如果是當(dāng)前的歌詞就用高亮的畫筆畫
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    }

知道了當(dāng)前歌詞的位置就很容易畫歌詞了。遍歷歌詞集合,如果是當(dāng)前歌詞,則用高亮的畫筆畫,其它歌詞就用普通畫筆畫。這里需注意的是兩支畫筆畫的位置公式都是一樣的,坐標(biāo)位置為x=寬的一半,y=高的一半+當(dāng)前位置*行間距。隨著當(dāng)前位置的變化,就能畫出上下句歌詞來。所以其實(shí)繪制出來后你會(huì)發(fā)現(xiàn)歌詞是從控件的正中央開始繪制的,這是為了方便與下面歌詞同步滑動(dòng)功能配合。

4. 歌詞同步滑動(dòng)

 1     //歌詞滑動(dòng)
 2    private void scrollLrc() {
 3        //下一句歌詞的開始時(shí)間
 4        long startTime = lrcBeanList.get(currentPosition).getStart();
 5        long currentTime = player.getCurrentPosition();
 6
 7        //判斷是否換行,在0.5內(nèi)完成滑動(dòng),即實(shí)現(xiàn)彈性滑動(dòng)
 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    }

如果不實(shí)現(xiàn)彈性滑動(dòng)的話,只要判斷當(dāng)前播放歌曲的時(shí)間是否大于當(dāng)前位置歌詞的結(jié)束時(shí)間,然后進(jìn)行scrollTo(0,(int)currentPosition * lineSpacing)滑動(dòng)即可。但是為了實(shí)現(xiàn)彈性滑動(dòng),我們需要將一次滑動(dòng)分成若干次小的滑動(dòng)并在一個(gè)時(shí)間段內(nèi)完成,所以我們動(dòng)態(tài)設(shè)置y的值,由于不斷重繪,就能實(shí)現(xiàn)在0.5秒內(nèi)完成View的滑動(dòng),這樣就能實(shí)現(xiàn)歌詞同步彈性滑動(dòng)。

500其實(shí)就是0.5s,因?yàn)樵谶@里currentTime和startTime的單位都是ms

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

5.不斷重繪

通過不斷重繪才能實(shí)現(xiàn)歌詞同步滑動(dòng),這里每隔0.1s進(jìn)行重繪

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

你以為這樣就結(jié)束了嗎?其實(shí)還沒有,答案下文揭曉!

三 、使用

然后我們興高采烈的在xml中,引用這個(gè)自定義View

LrcView前面的名稱為你建這個(gè)類的完整包名

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代碼中給這個(gè)自定義View傳入標(biāo)準(zhǔn)歌詞字符串和播放器。

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

點(diǎn)擊運(yùn)行,滿心期待自己的成果,接著你就會(huì)一臉懵逼,what?怎么是一片空白,什么也沒有!其實(shí)這時(shí)候你重新理一下上面歌詞繪制的流程,就會(huì)發(fā)現(xiàn)問題所在。 首先我們的自定義View控件引用到布局中時(shí)是先執(zhí)行onDraw方法的,所以當(dāng)你調(diào)用setLrc和setPlayer方法后,是不會(huì)再重新調(diào)用onDraw方法的,等于你并沒有傳入歌詞字符串和播放器,所以當(dāng)然會(huì)顯示一片空白

解決方法 :我們?cè)趧偛抛远xView歌詞控件中添加一個(gè)外部方法來調(diào)用onDraw,剛好這個(gè)invalidate()就能夠重新調(diào)用onDraw方法

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

然后我們?cè)谥鞔a中,在調(diào)用setLrc和setPlayer后還得調(diào)用draw方法

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

“Android怎么自定義View實(shí)現(xiàn)簡(jiǎn)約風(fēng)歌詞控件”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI