最近重構了以前的音樂播放器(音樂播放器的源碼地址在文章底部),添加了許多功能,好比歌詞,下載功能等。這篇文章就讓咱們聊聊歌詞控件的實現(歌詞控件也已經開源,地址也在文章底部),先上效果圖,若是感受海星,就繼續瞧下去!php
看到這裏,估計你對這個控件還有點感興趣的吧,那接下來就讓咱們來瞧瞧實現這個歌詞控件須要作些什麼!(若是想直接使用就直接點擊文末中的開源庫地址,裏面會有添加依賴庫的說明)java
首先,咱們得知道正常的歌詞格式是怎樣的,大概是長這個樣子:android
[ti:喜歡你]
[ar:.]
[al:]
[by:]
[offset:0]
[00:00.10]喜歡你 - G.E.M. 鄧紫棋 (Gem Tang)
[00:00.20]詞:黃家駒
[00:00.30]曲:黃家駒
[00:00.40]編曲:Lupo Groinig
[00:00.50]
[00:12.65]細雨帶風溼透黃昏的街道
[00:18.61]抹去雨水雙眼無端地仰望
[00:24.04]望向孤單的晚燈
[00:26.91]
[00:27.44]是那傷感的記憶
[00:30.52]
[00:34.12]再次泛起內心無數的思念
[00:39.28]
[00:40.10]以往片刻歡笑仍掛在臉上
[00:45.49]願你此刻可會知
[00:48.23]
[00:48.95]是我衷心的說聲
[00:53.06]
[00:54.35]喜歡你 那雙眼動人
[00:59.35]
[01:00.10]笑聲更迷人
[01:02.37]
[01:03.15]願再可 輕撫你
[01:08.56]
[01:09.35]那可愛面容
[01:12.40]挽手說夢話
[01:14.78]
[01:15.48]像昨天 你共我
[01:20.84]
[01:26.32]滿帶理想的我曾經多衝動
[01:32.45]屢怨與她相愛難有自由
[01:37.82]願你此刻可會知
[01:40.40]
[01:41.25]是我衷心的說聲
[01:44.81]
[01:46.39]喜歡你 那雙眼動人
[01:51.72]
[01:52.42]笑聲更迷人
[01:54.75]
[01:55.48]願再可 輕撫你
[02:00.93]
[02:01.68]那可愛面容
[02:03.99]
[02:04.73]挽手說夢話
[02:07.13]
[02:07.82]像昨天 你共我
[02:14.53]
[02:25.54]每晚夜裏自我獨行
[02:29.30]隨處蕩 多冰冷
[02:35.40]
[02:37.83]以往爲了自我掙扎
[02:41.62]從不知 她的痛苦
[02:52.02]
[02:54.11]喜歡你 那雙眼動人
[03:00.13]笑聲更迷人
[03:02.38]
[03:03.14]願再可 輕撫你
[03:08.77]
[03:09.33]那可愛面容
[03:11.71]
[03:12.41]挽手說夢話
[03:14.61]
[03:15.45]像昨天 你共我
複製代碼
從上面能夠看出這種格式前面是開始時間,從左往右一一對應分,秒,毫秒,後面就是歌詞。因此咱們要建立一個實體類來保存每一句的歌詞信息。git
public class LrcBean {
private String lrc;//歌詞
private long start;//開始時間
private long end;//結束時間
public String getLrc() {
return lrc;
}
public void setLrc(String lrc) {
this.lrc = lrc;
}
public long getStart() {
return start;
}
public void setStart(long start) {
this.start = start;
}
public long getEnd() {
return end;
}
public void setEnd(long end) {
this.end = end;
}
}
複製代碼
每句歌詞,咱們須要開始時間,結束時間和歌詞這些信息,那麼你就會有疑問了?上面提到的歌詞格式好像只有歌詞開始時間,那咱們怎麼知道結束時間呢?其實很簡單,這一句歌詞的開始時間就是上一句歌詞的結束時間。有了歌詞實體類,咱們就得開始對歌詞進行解析了!github
public class LrcUtil {
/** * 解析歌詞,將字符串歌詞封裝成LrcBean的集合 * @param lrcStr 字符串的歌詞,歌詞有固定的格式,通常爲 * [ti:喜歡你] * [ar:.] * [al:] * [by:] * [offset:0] * [00:00.10]喜歡你 - G.E.M. 鄧紫棋 (Gem Tang) * [00:00.20]詞:黃家駒 * [00:00.30]曲:黃家駒 * [00:00.40]編曲:Lupo Groinig * @return 歌詞集合 */
public static List<LrcBean> parseStr2List(String lrcStr){
List<LrcBean> res = new ArrayList<>();
//根據轉行字符對字符串進行分割
String[] subLrc = lrcStr.split("\n");
//跳過前四行,從第五行開始,由於前四行的歌詞咱們並不須要
for (int i = 5; i < subLrc.length; i++) {
String lineLrc = subLrc[i];
//[00:00.10]喜歡你 - G.E.M. 鄧紫棋 (Gem Tang)
String min = lineLrc.substring(lineLrc.indexOf("[")+1,lineLrc.indexOf("[")+3);
String sec = lineLrc.substring(lineLrc.indexOf(":")+1,lineLrc.indexOf(":")+3);
String mills = lineLrc.substring(lineLrc.indexOf(".")+1,lineLrc.indexOf(".")+3);
//進制轉化,轉化成毫秒形式的時間
long startTime = getTime(min,sec,mills);
//歌詞
String lrcText = lineLrc.substring(lineLrc.indexOf("]")+1);
//有多是某個時間段是沒有歌詞,則跳過下面
if(lrcText.equals("")) continue;
//在第一句歌詞中有多是很長的,咱們只截取一部分,即歌曲加演唱者
//好比 光年以外 (《太空旅客(Passengers)》電影中國區主題曲) - G.E.M. 鄧紫棋 (Gem Tang)
if (i == 5) {
int lineIndex = lrcText.indexOf("-");
int first = lrcText.indexOf("(");
if(first<lineIndex&&first!=-1){
lrcText = lrcText.substring(0,first)+lrcText.substring(lineIndex);
}
LrcBean lrcBean = new LrcBean();
lrcBean.setStart(startTime);
lrcBean.setLrc(lrcText);
res.add(lrcBean);
continue;
}
//添加到歌詞集合中
LrcBean lrcBean = new LrcBean();
lrcBean.setStart(startTime);
lrcBean.setLrc(lrcText);
res.add(lrcBean);
//若是是最後一句歌詞,其結束時間是不知道的,咱們將人爲的設置爲開始時間加上100s
if(i == subLrc.length-1){
res.get(res.size()-1).setEnd(startTime+100000);
}else if(res.size()>1){
//當集合數目大於1時,這句的歌詞的開始時間就是上一句歌詞的結束時間
res.get(res.size()-2).setEnd(startTime);
}
}
return res;
}
/** * 根據時分秒得到總時間 * @param min 分鐘 * @param sec 秒 * @param mills 毫秒 * @return 總時間 */
private static long getTime(String min,String sec,String mills){
return Long.valueOf(min)*60*1000+Long.valueOf(sec)*1000+Long.valueOf(mills);
}
}
複製代碼
相信上面的代碼和註釋已經將這個歌詞解析解釋的挺明白了,須要注意的是上面對i=5,也就是歌詞真正開始的第一句作了特殊處理,由於i=5這句有多是很長的,假設i=5是「光年以外 (《太空旅客(Passengers)》電影中國區主題曲) - G.E.M. 鄧紫棋 (Gem Tang)」這句歌詞,若是咱們不作特殊處理,在後面繪製的時候,就會發現這句歌詞會超過屏幕大小,很影響美觀,因此咱們只截取歌曲名和演唱者,有些說明直接省略掉了。解析好了歌詞,接下來就是重頭戲-歌詞繪製!canvas
歌詞繪製就涉及到了自定義View的知識,因此還未接觸自定義View的小夥伴須要先去看看自定View的基礎知識。歌詞繪製的主要工做主要由下面幾部分構成:app
在res文件中的values中新建一個attrs.xml文件,而後定義歌詞的自定義View屬性ide
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="LrcView">
<attr name="highLineTextColor" format="color|reference|integer"/>
<attr name="lrcTextColor" format="color|reference|integer"/>
<attr name="lineSpacing" format="dimension"/>
<attr name="textSize" format="dimension"/>
</declare-styleable>
</resources>
複製代碼
這裏只自定義了歌詞顏色,歌詞高亮顏色,歌詞大小,歌詞行間距的屬性,可根據本身須要自行添加。工具
而後在Java代碼中,設置默認值。佈局
private int lrcTextColor;//歌詞顏色
private int highLineTextColor;//當前歌詞顏色
private int width, height;//屏幕寬高
private int lineSpacing;//行間距
private int textSize;//字體大小
public LrcView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LrcView);
lrcTextColor = ta.getColor(R.styleable.LrcView_lrcTextColor, Color.GRAY);
highLineTextColor = ta.getColor(R.styleable.LrcView_highLineTextColor, Color.BLUE);
float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
float scale = context.getResources().getDisplayMetrics().density;
//默認字體大小爲16sp
textSize = ta.getDimensionPixelSize(R.styleable.LrcView_textSize, (int) (16 * fontScale));
//默認行間距爲30dp
lineSpacing = ta.getDimensionPixelSize(R.styleable.LrcView_lineSpacing, (int) (30 * scale));
//回收
ta.recycle();
}
複製代碼
private void init() {
//初始化歌詞畫筆
dPaint = new Paint();
dPaint.setStyle(Paint.Style.FILL);//填滿
dPaint.setAntiAlias(true);//抗鋸齒
dPaint.setColor(lrcTextColor);//畫筆顏色
dPaint.setTextSize(textSize);//歌詞大小
dPaint.setTextAlign(Paint.Align.CENTER);//文字居中
//初始化當前歌詞畫筆
hPaint = new Paint();
hPaint.setStyle(Paint.Style.FILL);
hPaint.setAntiAlias(true);
hPaint.setColor(highLineTextColor);
hPaint.setTextSize(textSize);
hPaint.setTextAlign(Paint.Align.CENTER);
}
複製代碼
咱們把初始化的方法放到了構造方法中,這樣就能夠避免在重繪時再次初始化。另外因爲咱們把init方法只放到了第三個構造方法中,因此在上面兩個構造方法須要將super改爲this,這樣就能保證哪一個構造方法都能執行init方法
public LrcView(Context context) {
this(context, null);
}
public LrcView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public LrcView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LrcView);
......
//回收
ta.recycle();
init();
}
複製代碼
由於後面的步驟都是在onDraw方法中執行的,因此咱們先貼出onDraw方法中的代碼
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
getMeasuredWidthAndHeight();//獲得測量後的寬高
getCurrentPosition();//獲得當前歌詞的位置
drawLrc(canvas);//畫歌詞
scrollLrc();//歌詞滑動
postInvalidateDelayed(100);//延遲0.1s刷新
}
複製代碼
1.得到控件的測量後的寬高
private int width, height;//屏幕寬高
private void getMeasuredWidthAndHeight(){
if (width == 0 || height == 0) {
width = getMeasuredWidth();
height = getMeasuredHeight();
}
}
複製代碼
爲何要得到控件的寬高呢?由於在下面咱們須要畫歌詞,畫歌詞時須要畫的位置,這時候就須要用到控件的寬高了。
2. 獲得當前歌詞的位置
private List<LrcBean> lrcBeanList;//歌詞集合
private int currentPosition;//當前歌詞的位置
private MediaPlayer player;//當前的播放器
private void getCurrentPosition() {
int curTime = player.getCurrentPosition();
//若是當前的時間大於10分鐘,證實歌曲未播放,則當前位置應該爲0
if (curTime < lrcBeanList.get(0).getStart()||curTime>10*60*1000) {
currentPosition = 0;
return;
} else if (curTime > lrcBeanList.get(lrcBeanList.size() - 1).getStart()) {
currentPosition = lrcBeanList.size() - 1;
return;
}
for (int i = 0; i < lrcBeanList.size(); i++) {
if (curTime >= lrcBeanList.get(i).getStart() && curTime <= lrcBeanList.get(i).getEnd()) {
currentPosition = i;
}
}
}
複製代碼
咱們根據當前播放的歌曲時間來遍歷歌詞集合,從而判斷當前播放的歌詞的位置。細心的你可能會發如今currentPosition = 0中有個curTime>10 * 60 *1000的判斷,這是由於在實際使用中發現當player還未播放時,這時候獲得的curTime會很大,因此纔有了這個判斷(由於正常的歌曲不會超過10分鐘)。
在這個方法咱們會發現出現了歌詞集合和播放器,你可能會感到困惑,這些不是還沒賦值嗎?困惑就對了,因此咱們須要提供外部方法來給外部傳給歌詞控件歌詞集合和播放器。
//將歌詞集合傳給到這個自定義View中
public LrcView setLrc(String lrc) {
lrcBeanList = LrcUtil.parseStr2List(lrc);
return this;
}
//傳遞mediaPlayer給自定義View中
public LrcView setPlayer(MediaPlayer player) {
this.player = player;
return this;
}
複製代碼
外部方法中setLrc的參數必須是前面提到的標準歌詞格式的字符串形式,這樣咱們就能利用上文的解析工具類LrcUtil中的解析方法將字符串解析成歌詞集合。
3. 畫歌詞
private void drawLrc(Canvas canvas) {
for (int i = 0; i < lrcBeanList.size(); i++) {
if (currentPosition == i) {//若是是當前的歌詞就用高亮的畫筆畫
canvas.drawText(lrcBeanList.get(i).getLrc(), width / 2, height / 2 + i * lineSpacing, hPaint);
} else {
canvas.drawText(lrcBeanList.get(i).getLrc(), width / 2, height / 2 + i * lineSpacing, dPaint);
}
}
}
複製代碼
知道了當前歌詞的位置就很容易畫歌詞了。遍歷歌詞集合,若是是當前歌詞,則用高亮的畫筆畫,其它歌詞就用普通畫筆畫。這裏需注意的是兩支畫筆畫的位置公式都是同樣的,座標位置爲x=寬的一半,y=高的一半+當前位置*行間距。隨着當前位置的變化,就能畫出上下句歌詞來。因此其實繪製出來後你會發現歌詞是從控件的正中央開始繪製的,這是爲了方便與下面歌詞同步滑動功能配合。
4. 歌詞同步滑動
//歌詞滑動
private void scrollLrc() {
//下一句歌詞的開始時間
long startTime = lrcBeanList.get(currentPosition).getStart();
long currentTime = player.getCurrentPosition();
//判斷是否換行,在0.5內完成滑動,即實現彈性滑動
float y = (currentTime - startTime) > 500 ? currentPosition * lineSpacing : lastPosition * lineSpacing + (currentPosition - lastPosition) * lineSpacing * ((currentTime - startTime) / 500f);
scrollTo(0,(int)y);
if (getScrollY() == currentPosition * lineSpacing) {
lastPosition = currentPosition;
}
}
複製代碼
若是不實現彈性滑動的話,只要判斷當前播放歌曲的時間是否大於當前位置歌詞的結束時間,而後進行scrollTo(0,(int)currentPosition * lineSpacing)滑動便可。可是爲了實現彈性滑動,咱們須要將一次滑動分紅若干次小的滑動並在一個時間段內完成,因此咱們動態設置y的值,因爲不斷重繪,就能實如今0.5秒內完成View的滑動,這樣就能實現歌詞同步彈性滑動。
500其實就是0.5s,由於在這裏currentTime和startTime的單位都是ms
float y = (currentTime - startTime) > 500 ? currentPosition * lineSpacing : lastPosition * lineSpacing + (currentPosition - lastPosition) * lineSpacing * ((currentTime - startTime) / 500f);
複製代碼
5.不斷重繪
經過不斷重繪才能實現歌詞同步滑動,這裏每隔0.1s進行重繪
postInvalidateDelayed(100);//延遲0.1s刷新
複製代碼
你覺得這樣就結束了嗎?其實尚未,答案下文揭曉!
而後咱們興高采烈的在xml中,引用這個自定義View
LrcView前面的名稱爲你建這個類的完整包名
<com.example.library.view.LrcView
android:id="@+id/lrcView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:lineSpacing="40dp"
app:textSize="18sp"
app:lrcTextColor="@color/colorPrimary"
app:highLineTextColor="@color/highTextColor"
/>
複製代碼
在Java代碼中給這個自定義View傳入標準歌詞字符串和播放器。
lrcView.setLrc(lrc).setPlayer(player);
複製代碼
點擊運行,滿心期待本身的成果,接着你就會一臉懵逼,what?怎麼是一片空白,什麼也沒有!其實這時候你從新理一下上面歌詞繪製的流程,就會發現問題所在。首先咱們的自定義View控件引用到佈局中時是先執行onDraw方法的,因此當你調用setLrc和setPlayer方法後,是不會再從新調用onDraw方法的,等於你並無傳入歌詞字符串和播放器,因此固然會顯示一片空白
解決方法:咱們在剛纔自定義View歌詞控件中添加一個外部方法來調用onDraw,恰好這個invalidate()就可以從新調用onDraw方法
public LrcView draw() {
currentPosition = 0;
lastPosition = 0;
invalidate();
return this;
}
複製代碼
而後咱們在主代碼中,在調用setLrc和setPlayer後還得調用draw方法
lrcView.setLrc(lrc).setPlayer(player).draw();
複製代碼
這樣咱們節約風的歌詞控件就大功告成了。
若是以爲不錯的話,歡迎你們來star!