Android自定義View之簡約風(fēng)歌詞控件實戰(zhàn)指南
前言
最近重構(gòu)了之前的音樂播放器,添加了許多功能,比如歌詞,下載功能等。這篇文章就讓我們聊聊歌詞控件的實現(xiàn),先上效果圖,如果感覺海星,就繼續(xù)瞧下去!

看到這里,估計你對這個控件還有點感興趣的吧,那接下來就讓我們來瞧瞧實現(xiàn)這個歌詞控件需要做些什么!
一、 歌詞解析
首先,我們得知道正常的歌詞格式是怎樣的,大概是長這個樣子:
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]愿你此刻可會知
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]滿帶理想的我曾經(jīng)多沖動
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]像昨天 你共我
從上面可以看出這種格式前面是開始時間,從左往右一一對應(yīng)分,秒,毫秒,后面就是歌詞。所以我們要創(chuàng)建一個實體類來保存每一句的歌詞信息。
1.歌詞實體類LrcBean
1public class LrcBean {
2 private String lrc;//歌詞
3 private long start;//開始時間
4 private long end;//結(jié)束時間
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}
每句歌詞,我們需要開始時間,結(jié)束時間和歌詞這些信息,那么你就會有疑問了?上面提到的歌詞格式好像只有歌詞開始時間,那我們怎么知道結(jié)束時間呢?其實很簡單,這一句歌詞的開始時間就是上一句歌詞的結(jié)束時間。有了歌詞實體類,我們就得開始對歌詞進(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)行字符對字符串進(jìn)行分割
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 //進(jìn)制轉(zhuǎn)化,轉(zhuǎn)化成毫秒形式的時間
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)》電影中國區(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è)置為開始時間加上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時,這句的歌詞的開始時間就是上一句歌詞的結(jié)束時間
58 res.get(res.size()-2).setEnd(startTime);
59 }
60
61 }
62 return res;
63 }
64
65 /**
66 * 根據(jù)時分秒獲得總時間
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}
相信上面的代碼和注釋已經(jīng)將這個歌詞解析解釋的挺明白了,需要注意的是上面對i=5,也就是歌詞真正開始的第一句做了特殊處理,因為i=5這句有可能是很長的,假設(shè)i=5是“光年之外
(《太空旅客(Passengers)》電影中國區(qū)主題曲) - G.E.M. 鄧紫棋 (Gem
Tang)”這句歌詞,如果我們不做特殊處理,在后面繪制的時候,就會發(fā)現(xiàn)這句歌詞會超過屏幕大小,很影響美觀,所以我們只截取歌曲名和演唱者,有些說明直接省略掉了。解析好了歌詞,接下來就是重頭戲-歌詞繪制!
二、歌詞繪制
歌詞繪制就涉及到了自定義View的知識,所以還未接觸自定義View的小伙伴需要先去看看自定View的基礎(chǔ)知識。歌詞繪制的主要工作主要由下面幾部分構(gòu)成:
- 為歌詞控件設(shè)置自定義屬性,在構(gòu)造方法中獲取并設(shè)置自定義屬性的默認(rèn)值
- 初始化兩支畫筆。分別是歌詞普通畫筆,歌詞高亮畫筆。
- 獲取當(dāng)前播放歌詞的位置
- 畫歌詞,根據(jù)當(dāng)前播放歌詞的位置來決定用哪支畫筆畫
- 歌詞隨歌曲播放同步滑動
- 重新繪制
1.設(shè)置自定View屬性,在代碼中設(shè)置默認(rèn)值
在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>
這里只自定義了歌詞顏色,歌詞高亮顏色,歌詞大小,歌詞行間距的屬性,可根據(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)造方法中,這樣就可以避免在重繪時再次初始化。另外由于我們把init方法只放到了第三個構(gòu)造方法中,所以在上面兩個構(gòu)造方法需要將super改成this,這樣就能保證哪個構(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方法
因為后面的步驟都是在onDraw方法中執(zhí)行的,所以我們先貼出onDraw方法中的代碼
1 @Override
2 protected void onDraw(Canvas canvas) {
3 super.onDraw(canvas);
4
5 getMeasuredWidthAndHeight();//得到測量后的寬高
6 getCurrentPosition();//得到當(dāng)前歌詞的位置
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. 得到當(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)前的時間大于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)前播放的歌曲時間來遍歷歌詞集合,從而判斷當(dāng)前播放的歌詞的位置。細(xì)心的你可能會發(fā)現(xiàn)在currentPosition = 0中有個curTime>10601000的判斷,這是因為在實際使用中發(fā)現(xiàn)當(dāng)player還未播放時,這時候得到的curTime會很大,所以才有了這個判斷(因為正常的歌曲不會超過10分鐘)。
在這個方法我們會發(fā)現(xiàn)出現(xiàn)了歌詞集合和播放器,你可能會感到困惑,這些不是還沒賦值嗎?困惑就對了,所以我們需要提供外部方法來給外部傳給歌詞控件歌詞集合和播放器。
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的參數(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)前位置的變化,就能畫出上下句歌詞來。所以其實繪制出來后你會發(fā)現(xiàn)歌詞是從控件的正中央開始繪制的,這是為了方便與下面歌詞同步滑動功能配合。
4. 歌詞同步滑動
1 //歌詞滑動
2 private void scrollLrc() {
3 //下一句歌詞的開始時間
4 long startTime = lrcBeanList.get(currentPosition).getStart();
5 long currentTime = player.getCurrentPosition();
6
7 //判斷是否換行,在0.5內(nèi)完成滑動,即實現(xiàn)彈性滑動
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 }
如果不實現(xiàn)彈性滑動的話,只要判斷當(dāng)前播放歌曲的時間是否大于當(dāng)前位置歌詞的結(jié)束時間,然后進(jìn)行scrollTo(0,(int)currentPosition * lineSpacing)滑動即可。但是為了實現(xiàn)彈性滑動,我們需要將一次滑動分成若干次小的滑動并在一個時間段內(nèi)完成,所以我們動態(tài)設(shè)置y的值,由于不斷重繪,就能實現(xiàn)在0.5秒內(nèi)完成View的滑動,這樣就能實現(xiàn)歌詞同步彈性滑動。
500其實就是0.5s,因為在這里currentTime和startTime的單位都是ms
1 float y = (currentTime - startTime) > 500 ? currentPosition * lineSpacing : lastPosition * lineSpacing + (currentPosition - lastPosition) * lineSpacing * ((currentTime - startTime) / 500f);
5.不斷重繪
通過不斷重繪才能實現(xiàn)歌詞同步滑動,這里每隔0.1s進(jìn)行重繪
1postInvalidateDelayed(100);//延遲0.1s刷新
你以為這樣就結(jié)束了嗎?其實還沒有,答案下文揭曉!
三 、使用
然后我們興高采烈的在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傳入標(biāo)準(zhǔn)歌詞字符串和播放器。
1lrcView.setLrc(lrc).setPlayer(player);
點擊運行,滿心期待自己的成果,接著你就會一臉懵逼,what?怎么是一片空白,什么也沒有!其實這時候你重新理一下上面歌詞繪制的流程,就會發(fā)現(xiàn)問題所在。 首先我們的自定義View控件引用到布局中時是先執(zhí)行onDraw方法的,所以當(dāng)你調(diào)用setLrc和setPlayer方法后,是不會再重新調(diào)用onDraw方法的,等于你并沒有傳入歌詞字符串和播放器,所以當(dāng)然會顯示一片空白
解決方法 :我們在剛才自定義View歌詞控件中添加一個外部方法來調(diào)用onDraw,剛好這個invalidate()就能夠重新調(diào)用onDraw方法
1 public LrcView draw() {
2 currentPosition = 0;
3 lastPosition = 0;
4 invalidate();
5 return this;
6 }
然后我們在主代碼中,在調(diào)用setLrc和setPlayer后還得調(diào)用draw方法
1lrcView.setLrc(lrc).setPlayer(player).draw();
這樣我們節(jié)約風(fēng)的歌詞控件就大功告成了。
總結(jié)
到此這篇關(guān)于Android自定義View之簡約風(fēng)歌詞控件的文章就介紹到這了,更多相關(guān)Android簡約風(fēng)歌詞控件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android編程之SMS讀取短信并保存到SQLite的方法
這篇文章主要介紹了Android編程之SMS讀取短信并保存到SQLite的方法,涉及Android針對SMS短信及SQLite數(shù)據(jù)庫的相關(guān)操作技巧,需要的朋友可以參考下2015-11-11
Android設(shè)計模式之適配器(Adapter)模式
這篇文章主要介紹了Android設(shè)計模式之適配器(Adapter)模式,以源碼解析的方式分析適配器模式,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-11-11
Android TextView中文本點擊文字跳轉(zhuǎn) (代碼簡單)
用過微博Android手機(jī)端的朋友的都知道微博正文有時有一些高亮顯示的文本,如話題、提到的人等等,當(dāng)點擊這些文本時會跳到另外一個頁面(即另一個activity),下面就要來模仿微博的這個功能2016-01-01

