自定义歌词展示控件
歌詞展示
封裝歌詞信息
歌詞的內容如下,一行歌詞由兩部分組成,[]里面的是開始時間,后面的是歌詞內容
[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;}@Overridepublic 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);}@Overrideprotected 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() {@Overridepublic void handleMessage(Message msg) {switch (msg.what) {case UPDATE_POSITION:updateCurrentPosition();break;case UPDATE_LRYIC_ROLL:startRoll();break;}}};private class AudioBroadcastReceiver extends BroadcastReceiver {@Overridepublic 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=水平居中使用的xdrawHorizontalText(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(newFileInputStream(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.51String[] arr = timeStr.split(":");// 01 22.51String 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 方法
@Overridepublic 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 {@Overridepublic 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 = newFile(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 {@Overridepublic 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 后綴的歌詞,如果都沒有的話需要從服務器下載
源代碼:https://github.com/JackChan1999/LyricView
總結
- 上一篇: 新闻频道管理的炫酷实现
- 下一篇: Kotlin极简教程:第7章 面向对象编