歌詞展示
封裝歌詞信息
歌詞的內容如下,一行歌詞由兩部分組成,[]裏面的是開始時間,後面的是歌詞內容
[00:03.25]最炫民族風 - 鳳凰傳奇
[00:08.67]獻給苦逼的黑馬程序員
[00:22.67]蒼茫的天涯是我的愛
[00:26.42]綿綿的青山腳下花正開
[00:30.18]什麼樣的節奏是最呀最搖擺
[00:33.90]什麼樣的歌聲纔是最開懷
[00:37.71]彎彎的河水從天上來
[00:41.51]流向那萬紫千紅一片海
[00:45.27]火辣辣的歌謠是我們的期待
[00:49.05]一路邊走邊唱纔是最自在
[00:52.86]我們要唱就要唱得最痛快
[00:56.61]你是我天邊 最美的雲彩
...
對應的實體類爲
public class Lyric implements Comparable<Lyric>{
private int startPoint; // 開始時間
private String content; // 一行歌詞的內容
public Lyric(int startPoint, String content) {
this.startPoint = startPoint;
this.content = content;
}
public int getStartPoint() {
return startPoint;
}
public void setStartPoint(int startPoint) {
this.startPoint = startPoint;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public int compareTo(Lyric another) {
return startPoint-another.getStartPoint();
}
}
繪製單行居中文本
自定義一個顯示歌詞的LyricView,歌詞本身就是一個文本,所以在這裏我們繼承TextView。它還有一個好處繼承TextView 之後不需要再去重寫onMeasure 方法。在onDraw 方法中去繪製一個文本。
public class LyricView extends TextView {
private float hightlightSize; // 高亮歌詞字體大小
private float normalSize;
private int hightLightColor; // 高亮歌詞字體顏色
private int normalColor;
private Paint paint;
public LyricView(Context context) {
super(context);
initView();
}
public LyricView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public LyricView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
// 初始化字體大小和顏色
private void initView() {
hightlightSize = getResources().getDimension(R.dimen.lyric_hightlight_size);
normalSize = getResources().getDimension(R.dimen.lyric_normal_size);
hightLightColor = Color.GREEN;
normalColor = Color.WHITE;
paint = new Paint();
paint.setAntiAlias(true);//抗鋸齒
paint.setTextSize(hightlightSize);
paint.setColor(hightLightColor);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String text = "正在加載歌詞...";
canvas.drawText(text, 0, 0, paint);
}
}
在項目中的歌詞佈局中引用View,重新build 之後的展示效果
從上圖中可以看到文本顯示的座標是view 的左上角。那麼我們需要將文本顯示的位置設置在view 的中間。計算的方法如圖
在onSizeChang 中計算出View 寬和高的一半,通過paint.getTextBounds 方法計算出文本的寬高的一半。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//計算View 的寬和高
halfViewW = w / 2;
halfViewH = h / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String text = "正在加載歌詞...";
Rect bounds = new Rect();
// paint.getTextBounds(text,0,text.length,bounds); // 測量歌詞內容文本矩形的大小
//計算text 的寬和高
int halfTextW = bounds.width() / 2;
int halfTextH = bounds.height() / 2;
//計算text 位置
int drawX = halfViewW - halfTextW;
int drawY = halfTextH + halfViewH;
canvas.drawText(text, drawX, drawY, paint);
}
重新build 之後的效果如下:
但是在Android Studio中使用bounds.width 方法獲取的文本寬度設置之後不在View 的中間。所以我們使用了paint.getTextMeasure(text)來重新獲取
int halfTextW= (int) paint.measureText(text)/2;
// paint.getTextBounds(text,0,text.length,bounds); // 測量歌詞內容文本矩形的大小
繪製多行歌詞
首先用List 模擬歌詞的數據並且記錄高亮行的行數。
private void initView() {
hightlightSize = getResources().getDimension(R.dimen.lyric_hightlight_size);
normalSize = getResources().getDimension(R.dimen.lyric_normal_size);
hightLightColor = Color.GREEN;
normalColor = Color.WHITE;
paint = new Paint();
paint.setAntiAlias(true);//抗鋸齒
paint.setTextSize(hightlightSize);
paint.setColor(hightLightColor);
//高亮的行數
currentLine = 5;
//模擬初始化數據
lyrics = new ArrayList<>();
for (int i = 0; i < 30; i++) {
lyrics.add(new Lryic(i * 2000, "當前正在播放行數爲:" + i));
}
}
獲取高亮行的位置。
/**
* 繪製多行文本
*/
private void drawMutiLineText(Canvas canvas) {
Lryic lyric = lyrics.get(currentLine);
//獲取高亮行Y 的位置
Rect bounds = new Rect();
//計算text的寬和高
paint.getTextBounds(lyric.getContent(), 0, lyric.getContent().length(), bounds);
// int halfTextW=bounds.width()/2;
int halfTextH = bounds.height() / 2;
int centerY = halfTextH + halfViewH;
}
按行繪製文本。
//按行繪製文本
for (int i = 0; i < lyrics.size(); i++) {
if (currentLine == i) {
paint.setColor(hightLightColor);
paint.setTextSize(hightlightSize);
} else {
paint.setColor(normalColor);
paint.setTextSize(normalSize);
}
}
y=居中行y 的位置+(繪製行的位置-高亮行的行數)*行高。
lineHeight=getResources().getDimensionPixelSize(R.dimen.lyric_line_height);
//y=居中行Y 的位置+(繪製行的行數-高亮行的行數)*行號
int downY=centerY+(i-currentLine)*lineHeight;
x=水平居中的x。
//x=水平居中使用的x
drawHorizontalText(canvas,lyrics.get(i).getContent(),downY);
效果圖如下
按行滾動歌詞
在LyricView 中提供一個滾動歌詞的方法。說白了其實只要設置歌詞高亮的位置就可以了。設置歌詞高亮的位置的算法如圖
/** 根據當前播放時間,改變高亮行的位置*/
public void roll(int position,int duration){
for (int i = 0; i < lyrics.size(); i++) {
Lyric lyric=lyrics.get(i);
if (i==lyrics.size()-1){
//最後一行
endPoint = duration;
}else{
Lyric nextLyric=lyrics.get(i+1);
endPoint=nextLyric.getStartPoint();
}
if (lyric.getStartPoint()<=position&&endPoint>position){
currentLine=i;
break;
}
}
invalidate();
}
在音樂播放界面中發消息讓歌詞滾動。在接收到準備完成的廣播之後就讓歌詞開始滾動。
private static final int UPDATE_LRYIC_ROLL = 1;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case UPDATE_POSITION:
updateCurrentPosition();
break;
case UPDATE_LRYIC_ROLL:
startRoll();
break;
}
}
};
private class AudioBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
//準備完成
//更新界面的按鈕
updatePlayBtn();
//初始化歌曲名和歌手
AudioItem audioItem = (AudioItem) intent.getSerializableExtra("audioItem");
tv_name.setText(StringUtil.formatDisplayName(audioItem.getName()));
tv_artist.setText(audioItem.getArtist());
sk_position.setMax(binder.getDuration());
//更新播放進度
updateCurrentPosition();
//初始化播放模式
updatePlayModeBtn();
//開啓歌詞滾動更新
startRoll();
}
}
/**
* 開啓歌詞滾動更新
*/
private void startRoll() {
lyricView.roll(binder.getCurrentPosition(), binder.getDuration());
handler.sendEmptyMessage(UPDATE_LRYIC_ROLL);
}
平滑滾動歌詞
平滑滾動歌詞的算法如圖
計算時使用的已播放時間和播放總時間需要從roll 方法中獲取
/**
* 繪製多行文本
*/
private void drawMutiLineText(Canvas canvas) {
Lyric lyric = lyrics.get(currentLine);
int endStartPoint;
//變化位置=居中行位置+偏移位置
//偏移位置=移動百分比*行高
//移動時間百分比=移動時間/可用時間
//可用時間=下一段的時間-本段的時間
//移動時間=已播放時間-起始時間
if (currentLine == lyrics.size() - 1) {
//最後一行
endStartPoint = mDuration;
} else {
Lyric nextLyric = lyrics.get(currentLine + 1);
endStartPoint = nextLyric.getStartPoint();
}
int moveTime = mPosition - lyric.getStartPoint();
int useTime = endStartPoint - lyric.getStartPoint();
float movePercent = moveTime / (float) useTime;
int offset = (int) (movePercent * lineHeight);
//獲取高亮行Y 的位置
Rect bounds = new Rect();
//計算text 的寬和高
paint.getTextBounds(lyric.getContent(), 0, lyric.getContent().length(), bounds);
// int halfTextW=bounds.width()/2;
int halfTextH = bounds.height() / 2;
// canvas.translate(0,-offset);
int centerY = halfTextH + halfViewH - offset;
//按行繪製文本
for (int i = 0; i < lyrics.size(); i++) {
if (currentLine == i) {
paint.setColor(hightLightColor);
paint.setTextSize(hightlightSize);
} else {
paint.setColor(normalColor);
paint.setTextSize(normalSize);
}
//y=居中行Y 的位置+(繪製行的行數-高亮行的行數)*行號
int downY = centerY + (i - currentLine) * lineHeight;
//x=水平居中使用的x
drawHorizontalText(canvas, lyrics.get(i).getContent(), downY);
}
}
運行結果
從文件中解析歌詞
從文件中解析歌詞。將歌詞一行一行的讀出來,並且根據歌詞的格式解析成List 集合,並將歌詞排序。
import com.jackchan.medioplayer.bean.Lyric;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class LyricParser {
/**
* 從歌詞文件中解析歌詞列表
*/
public static List<Lyric> parseLyricFromFile(File lyricFile) {
List<Lyric> lyrics = new ArrayList<>();
//數據可用性檢查
if (lyricFile == null || !lyricFile.exists()) {
lyrics.add(new Lyric(0, "沒有找到歌詞文件"));
return lyrics;
}
try {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new
FileInputStream(lyricFile), "GBK"));
String line = bufferedReader.readLine();
while (line != null) {
List<Lyric> lineLyrics = parserLine(line);
lyrics.addAll(lineLyrics);
line = bufferedReader.readLine();
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
//歌詞排序
Collections.sort(lyrics);
return lyrics;
}
/**
* 解析一行歌詞[ 01:22.51][ 01:22.51]滴答滴答
*/
private static List<Lyric> parserLine(String line) {
List<Lyric> lineLyric = new ArrayList<>();
// [ 01:22.51 [ 01:22.51 滴答滴答
String[] arr = line.split("]");
String content = arr[arr.length - 1];
for (int i = 0; i < arr.length - 1; i++) {
int startPoint = parserPoint(arr[i]);
lineLyric.add(new Lyric(startPoint, content));
}
return lineLyric;
}
/**
* 解析一行歌詞的時間[ 01:22.51
*/
private static int parserPoint(String s) {
int time = 0;
String timeStr = s.substring(1);
// 01:22.51
String[] arr = timeStr.split(":");
// 01 22.51
String minStr = arr[0];
arr = arr[1].split("\\.");
String senStr = arr[0];
String mSenStr = arr[1];
time = Integer.parseInt(minStr) * 60 * 1000 + Integer.parseInt(senStr) * 1000 + Integer.parseInt(mSenStr) * 100;
return time;
}
}
需要實現Comparable 接口,實現compareTo 方法
@Override
public int compareTo(Lyric lyric) {
return startPoint-lyric.getStartPoint();
}
在LyricView 中提供從文件中獲取歌詞集合和設置當前高亮行的方法。
public void setLyricFile(File lyricFile){
lyrics=LyricParser.parseLyricFromFile(lyricFile);
currentLine=0;
}
在onDraw 方法中繪製的時候,需要去判斷集合是否有數據,沒有數據的話就顯示歌詞正在加載中,如果有數據的話就顯示歌詞。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (lyrics==null||lyrics.size()==0){
//繪製單行居中
drawSingleLineText(canvas);
}else{
drawMutiLineText(canvas);
}
}
在接收準備的廣播中的滾動歌詞之前將歌詞加載出來。
private class AudioBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
//準備完成
//更新界面的按鈕
updatePlayBtn();
//初始化歌曲名和歌手
AudioItem audioItem= (AudioItem) intent.getSerializableExtra("audioItem");
tv_name.setText(StringUtil.formatDisplayName(audioItem.getName()));
tv_artist.setText(audioItem.getArtist());
sk_position.setMax(binder.getDuration());
//更新播放進度
updateCurrentPosition();
//初始化播放模式
updatePlayModeBtn();
File file=new File(Environment.getExternalStorageDirectory(),"test/audio/"+
StringUtil.formatDisplayName(audioItem.getName())+".lrc");
lyricView.setLyricFile(file);
//開啓歌詞滾動更新
startRoll();
}
}
運行結果
歌詞加載模塊
我們發現北京北京的歌詞沒有加載出來。是因爲上面我們傳的文件時lrc 後綴的文件,但如圖北京北京的歌詞的後綴是txt,所以在這裏我們需要寫一個歌詞加載器。當文件中沒有lrc 後綴的歌詞的時候,就看看有沒有txt 後綴的歌詞,如果都沒有的話需要從服務器下載。
package com.jackchan.medioplayer.db;
import android.os.Environment;
import java.io.File;
public class LyricLoader {
private static final File root = new
File(Environment.getExternalStorageDirectory(), "/test/audio");
//加載歌詞文件
public static File loadLyricFile(String title) {
//查找lrc 文件
File lyricFile = new File(root, title + ".lrc");
if (lyricFile.exists()) {
return lyricFile;
}
//查找txt 文件
lyricFile = new File(root, title + ".txt");
if (lyricFile.exists()) {
return lyricFile;
}
// TODO 服務器下載
return null;
}
}
在播放界面收到廣播之後調用方法初始化歌詞文件。
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import java.io.File;
import static com.jackchan.vmplayer.R.id.tv_artist;
private class AudioBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
//準備完成
//更新界面的按鈕
updatePlayBtn();
//初始化歌曲名和歌手
AudioItem audioItem = (AudioItem) intent.getSerializableExtra("audioItem");
tv_name.setText(StringUtil.formatDisplayName(audioItem.getName()));
tv_artist.setText(audioItem.getArtist());
sk_position.setMax(binder.getDuration());
//更新播放進度
updateCurrentPosition();
//初始化播放模式
updatePlayModeBtn();
// File file=new File(Environment.getExternalStorageDirectory(),"test/audio/"+
StringUtil.formatDisplayName(audioItem.getName()) + ".lrc");
File file = LyricLoader.loadLyricFile(StringUtil.formatDisplayName
(audioItem.getName()));
lyricView.setLyricFile(file);
//開啓歌詞滾動更新
startRoll();
}
}
運行結果
小結
本篇博客完成了音樂播放界面的歌詞展示,自定義了展示歌詞的控件,先在控件中間顯示一行文字,然後又顯示了集合中的所有文字。接着通過改變當前高亮顯示的行數來使歌詞移動起來。我們通過設置偏移量讓歌詞的移動看起來更平滑。最後從文件中將歌詞解析出來。但是我們爲了能夠適應txt 和lrc 文件格式的歌詞文件,自定義了一個歌詞加載器。當文件中沒有lrc 後綴的歌詞的時候,就看看有沒有txt 後綴的歌詞,如果都沒有的話需要從服務器下載