Android音樂播放器中的歌詞同步學習分析
阿新 • • 發佈:2019-01-29
在網上查了一下資料,感謝 http://www.cr173.com/html/20184_1.html 給了我思路,可以說他提供了最基本的歌詞同步的功能,我在其上面添加了自己的修改的程式碼。
主要是自己為了實現歌詞同步,並且通過移動seekbar,改變歌曲的歌詞位置。當然還有自己不一樣的地方。
首先歌詞播放,是要一個子執行緒來操作,這個子執行緒負責在找到兩段歌詞之間的時間差,然後顯示當前正在播放的歌詞。
1
歌詞部分
1.歌詞的格式為.lrc 這是有一定格式的,最重要的是[MM:ss,mm]
以白玫瑰.lrc歌詞為例
需要對歌詞進行解析,歌詞的實體類。
MyLrc.java
public class MyLrc implements Comparable<Object>{ private int time; private String lyric; public int getTime() { return time; } public void setTime(int time) { this.time = time; } public String getLyric() { return lyric; } public void setLyric(String lyric) { this.lyric = lyric; } //放在set集合中可以看下面要求進行排序 @Override public int compareTo(Object arg0) { int later=0; if(arg0 instanceof MyLrc) { later=((MyLrc)arg0).getTime(); } return this.time-later; } @Override public String toString() { return this.time+""+this.lyric; } }
實現comparaTo的方法的目的是在把物件放入TreeSet中的時候,按照歌詞時間的循序放入,方便之後拿出來。比較使用(我覺得這部很重要,因為後期需要判斷拉動SeekBar的時候找到對應的時間的位置)
LrcUtil.java
//對歌詞進行解析 public class LrcUtil { private static TreeSet<MyLrc> tree; // 將對應的lrc檔案轉化為treeMap,分別對應的時間以及歌詞 public LrcUtil(InputStream musicTitle) { TreeSet<MyLrc> treeset = new TreeSet<MyLrc>(); // 用來存放歌曲的時間和對應的歌詞 InputStreamReader inReader = null; BufferedReader reader = null; try { inReader = new InputStreamReader(musicTitle); reader = new BufferedReader(inReader); String line = ""; while ((line = reader.readLine()) != null) { // 對那行歌詞進行分割,判斷,然後儲存 String[] substr = line.split("\\]"); for (String ss : substr) { if (ss.contains("[") && ss.contains(":") && ss.contains(".")) { String sss = ss.replaceAll("\\[", ""); String[] timeStart = sss.split(":"); String[] timeEnd = timeStart[1].split("\\."); // 計算出當前的時間的毫秒數 int time = (Integer.valueOf(timeStart[0]) * 60 + Integer .valueOf(timeEnd[0])) * 1000 + Integer.valueOf(timeEnd[1]) * 10; // 對應的時間放一個對應的歌詞 MyLrc lrc = new MyLrc(); lrc.setTime(time); lrc.setLyric(substr[substr.length - 1]); treeset.add(lrc); } } } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { try { inReader.close(); reader.close(); } catch (IOException e) { e.printStackTrace(); } } tree = treeset; } //獲得lrc檔案的歌詞點的列表 public List<String> getWords() { List<String> list = new ArrayList<String>(); Iterator<MyLrc> it = tree.iterator(); while (it.hasNext()) { MyLrc my = it.next(); list.add(my.getLyric()); } return list; } //獲得lrc檔案的時間點的列表 public List<Integer> getTimes() { List<Integer> list = new ArrayList<Integer>(); Iterator<MyLrc> it = tree.iterator(); while (it.hasNext()) { MyLrc my = it.next(); list.add(my.getTime()); } return list; } }
Lrc工具類在構造方法中傳入一個InputStream流,由於現在是本地測試,所以本地先放入lrc檔案。Android中可以把歌詞檔案放在Assets檔案中,但是有些歌是中文的,而Assets檔案中是不允許放中文的檔案的
顯示為非法名字,所以我打算把中文歌曲的名字轉化為拼音再查詢。
Activity中歌詞顯示的程式碼
根據當前歌曲的進度啟一個子執行緒,sleep一段時間,其他時間重新整理控制元件顯示。
private void initLrcThread(int currentPosition) { LrcUtil lrcUtil = null; try { lrcUtil = new LrcUtil(getAssets().open(Trans2PinYin.trans2PinYin(currentMusic.getTitle())+".lrc")); playing_text_lrc.init(Trans2PinYin.trans2PinYin(currentMusic.getTitle())+".lrc"); } catch (IOException e) { ToastInfo(R.string.notfind_lrc); } if(lrcUtil==null) return; final List<Integer> lrctime = lrcUtil.getTimes(); if(currentPosition>lrctime.get(lrctime.size()-1)) return; int position = 0; for (int i = 0; i < lrctime.size()-1; i++) { if (currentPosition < lrctime.get(0)) { position = 0; break; } else if (currentPosition > lrctime.get(i) && currentPosition < lrctime.get(i + 1)) { position = i; break; } } final int p = position; //找到對應位置的歌詞 playing_text_lrc.changeIndex(p); // 起一個子執行緒進行歌詞顯示 new Thread() { int i = p; public void run() { while (!mService.isPause()) { handler.post(new Runnable() { @Override public void run() { playing_text_lrc.invalidate(); } }); try { if (i == 0) Thread.sleep(lrctime.get(i)); else { Thread.sleep(lrctime.get(i + 1) - lrctime.get(i)); } } catch (InterruptedException e) { e.printStackTrace(); } i++; //防止下標越界! if (i+1 >= lrctime.size()) break; } ; } }.start(); }
這裡是對自定義控制元件顯示歌詞的操作,如果沒找到對應的lrc就打的Toast即可。
對於歌曲顯示的子執行緒。當歌曲在播放的時候。執行緒sleep一段時間後對自定義的歌詞控制元件進行重新整理。每次重新整理都會走空間中的ondraw的方法。
在ondraw方法中,重新整理一次就跳到下一個歌詞。
當然了,這裡通過SeekBar移動的時候可以得到當前歌曲的位置(時間點),通過這個時間點(currentPosition)找到對應的getTimes列表中的歌詞的位置
<span style="white-space:pre"> </span>for (int i = 0; i < lrctime.size()-1; i++) {
<span style="white-space:pre"> </span>if (currentPosition < lrctime.get(0)) {
<span style="white-space:pre"> </span>position = 0;
<span style="white-space:pre"> </span>break;
<span style="white-space:pre"> </span>} else if (currentPosition > lrctime.get(i)
<span style="white-space:pre"> </span>&& currentPosition < lrctime.get(i + 1)) {
<span style="white-space:pre"> </span>position = i;
<span style="white-space:pre"> </span>break;
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>}
並且同時改變playing_text_lrc.changeIndex(p);
LRCTextView.java
public class LRCTextView extends TextView {
private List<String> mWordsList = new ArrayList<String>();
private Paint mLoseFocusPaint;
private Paint mOnFocusePaint;
private float mX = 0;
private float mMiddleY = 0;
private float mY = 0;
private static final int DY = 50;
private int mIndex = 0;
private Context context;
public LRCTextView(Context context) throws IOException {
super(context);
this.context = context;
}
public LRCTextView(Context context, AttributeSet attrs) throws IOException {
super(context, attrs);
this.context = context;
}
public LRCTextView(Context context, AttributeSet attrs, int defStyle)
throws IOException {
super(context, attrs, defStyle);
this.context = context;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint p = mLoseFocusPaint;
// 未走到init未找到歌詞
if (p == null) {
p=new Paint();
p.setTextSize(50);
canvas.drawText("未找到歌詞", mX-100, mMiddleY, p);
return;
}
p.setTextAlign(Paint.Align.CENTER);
Paint p2 = mOnFocusePaint;
p2.setTextAlign(Paint.Align.CENTER);
// 防止下表越界
if (mIndex >= mWordsList.size())
return;
canvas.drawText(mWordsList.get(mIndex), mX, mMiddleY, p2);
int alphaValue = 25;
float tempY = mMiddleY;
for (int i = mIndex - 1; i >= 0; i--) {
tempY -= DY;
if (tempY < 0) {
break;
}
p.setColor(Color.argb(255 - alphaValue, 245, 245, 245));
canvas.drawText(mWordsList.get(i), mX, tempY, p);
alphaValue += 25;
}
alphaValue = 25;
tempY = mMiddleY;
for (int i = mIndex + 1, len = mWordsList.size(); i < len; i++) {
tempY += DY;
if (tempY > mY) {
break;
}
p.setColor(Color.argb(255 - alphaValue, 245, 245, 245));
canvas.drawText(mWordsList.get(i), mX, tempY, p);
alphaValue += 25;
}
mIndex++;
}
@Override
protected void onSizeChanged(int w, int h, int ow, int oh) {
super.onSizeChanged(w, h, ow, oh);
mX = w * 0.5f;
mY = h;
mMiddleY = h * 0.3f;
}
public void init(String musicName) throws IOException {
setFocusable(true);
LrcUtil lrcHandler = new LrcUtil(context.getAssets().open(musicName));
mWordsList = lrcHandler.getWords();
mLoseFocusPaint = new Paint();
mLoseFocusPaint.setAntiAlias(true);
mLoseFocusPaint.setTextSize(30);
mLoseFocusPaint.setColor(Color.BLACK);
mLoseFocusPaint.setTypeface(Typeface.SERIF);
mOnFocusePaint = new Paint();
mOnFocusePaint.setAntiAlias(true);
mOnFocusePaint.setColor(Color.RED);
mOnFocusePaint.setTextSize(50);
mOnFocusePaint.setTypeface(Typeface.SANS_SERIF);
}
public void changeIndex(int i) {
mIndex = i;
}
這裡我稍微修改了這個方法,傳入一個changeIndex用於Seekbar被拉動時候,歌詞的改變。其他的與http://www.cr173.com/html/20184_1.html相似
而在每次歌曲切換或者進度條移動的時候呼叫這個方法即可
initLrcThread(currentPosition);
這裡用到了
Trans2PinYin.trans2PinYin(currentMusic.getTitle()
這個方法,這是中文轉化為拼音的方法。百度即可。