### 1.背景
歌詞是音樂軟件必備的,沒有它的存在就感覺少點什么,故實現了歌曲歌詞的顯示,使用LrcView實現,當然是在GitHub上找到的,是一個自定義View :
LrcView 地址 :?[https://github.com/ChanWong21/LrcView](https://github.com/ChanWong21/LrcView)
效果預覽 :?
? ? 現在說說我使用過程中對它的不足之處做一下總結:
? ?(1)只能加載本地asserts文件夾中的lrc文件,不能請求網絡上的歌詞;
? ?(2)不能設置當前播放到得時間,也就不能顯示當前時間的歌詞,只可以順序播放;
? ?(3)當播放完畢后,重新播放一直停留在最后(因為時間是最后,永遠大于當前播放的時間);
? ?(4)沒事回調事件,無法判斷有沒有歌詞存在;
? ? ? 下面我將一一解決;
?
# 2.歌詞LrcView實現
### ? ? (1)實現 attr.xml?
~~~
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="LrcView">
<attr name="textSize" format="dimension" />
<attr name="dividerHeight" format="dimension" />
<attr name="normalTextColor" format="reference|color" />
<attr name="currentTextColor" format="reference|color" />
<attr name="animationDuration" format="integer" />
</declare-styleable>
</resources>
~~~
### ? ? (2)LrcView實現
? ? ? ? ? ? ? ? ? 歌詞的加載也是使用了上篇中的網絡加載模塊,可以輕松的實現數據請求;
? ? ? ? ? ? ? ? ??[Android實戰 - 音心播放器 (MusicActivity-音樂播放頁面界面實現,網絡模塊實現)](http://blog.csdn.net/lablenet/article/details/50324913)
? ? ? ? ? ? ? ? ? 幾個重要的方法 說明:
? ? ? ? ? ? ? ? ? ?onDraw() : 繪制當前顯示的歌詞;
? ? ? ? ? ? ? ? ? ?updateTime() : 外部調用,切換歌詞;
? ? ? ? ? ? ? ? ? ?parseLine() : 解析歌詞的每一行;
? ? ? ? ? ? ? ? ? ?lrcViewToMusicActivity 對象 : 回調事件,MusicActivity 實現該接口;
? ? ? ? ? ? ? ? ? ?LrcPlayToEnd : LrcView實現該接口,為了使得MusicActivity告訴LrcView,播放完畢,重新初始化LrcView,其實就是講當前時間改為0,非最大值;
~~~
package cn.labelnet.ui;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import cn.labelnet.event.LrcPlayToEnd;
import cn.labelnet.event.LrcViewToMusicActivity;
import cn.labelnet.maskmusic.R;
import cn.labelnet.net.MusicAsyncGetUrl;
import cn.labelnet.net.MusicAsyncHandlerGetLrc;
import cn.labelnet.net.MusicRequest;
/**
* LrcView
*/
public class LrcView extends View implements MusicAsyncGetUrl, LrcPlayToEnd {
private static final String TAG = LrcView.class.getSimpleName();
private static final int MSG_NEW_LINE = 0;
private List<Long> mLrcTimes;
private List<String> mLrcTexts;
private LrcHandler mHandler;
private Paint mNormalPaint;
private Paint mCurrentPaint;
private float mTextSize;
private float mDividerHeight;
private long mAnimationDuration;
private long mNextTime = 0l;
private int mCurrentLine = 0;
private float mAnimOffset;
private boolean mIsEnd = false;
// 網絡
private MusicAsyncHandlerGetLrc musicAsyncHandlerGetLrc;
private MusicRequest musicRequest;
// 回調事件
private LrcViewToMusicActivity lrcViewToMusicActivity;
private boolean isLrc = false;
public void setLrcViewToMusicActivity(
LrcViewToMusicActivity lrcViewToMusicActivity) {
this.lrcViewToMusicActivity = lrcViewToMusicActivity;
}
private String songId = 001 + "";
public LrcView(Context context) {
this(context, null);
}
public LrcView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
/**
* 初始化
*
* @param attrs
* attrs
*/
private void init(AttributeSet attrs) {
TypedArray ta = getContext().obtainStyledAttributes(attrs,
R.styleable.LrcView);
mTextSize = ta.getDimension(R.styleable.LrcView_textSize, 48.0f);
mDividerHeight = ta.getDimension(R.styleable.LrcView_dividerHeight,
72.0f);
mAnimationDuration = ta.getInt(R.styleable.LrcView_animationDuration,
1000);
mAnimationDuration = mAnimationDuration < 0 ? 1000 : mAnimationDuration;
// int normalColor = ta.getColor(R.color.app_color_whrit,
// 0xffffffff);
// int currentColor = ta.getColor(R.color.app_color,
// 0xffff4081);
ta.recycle();
mLrcTimes = new ArrayList<Long>();
mLrcTexts = new ArrayList<String>();
WeakReference<LrcView> lrcViewRef = new WeakReference<LrcView>(this);
mHandler = new LrcHandler(lrcViewRef);
mNormalPaint = new Paint();
mCurrentPaint = new Paint();
mNormalPaint.setColor(Color.WHITE);
mNormalPaint.setTextSize(mTextSize);
mCurrentPaint.setColor(Color.RED);
mCurrentPaint.setTextSize(mTextSize);
// 設置網絡監聽
musicAsyncHandlerGetLrc = new MusicAsyncHandlerGetLrc();
musicAsyncHandlerGetLrc.setMusicasyncGetUrl(this);
musicRequest = new MusicRequest();
musicRequest.setMusicAsyncHandler(musicAsyncHandlerGetLrc);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mLrcTimes.isEmpty() || mLrcTexts.isEmpty()) {
return;
}
// 中心Y坐標
float centerY = getHeight() / 2 + mTextSize / 2 + mAnimOffset;
// 畫當前行
String currStr = mLrcTexts.get(mCurrentLine);
float currX = (getWidth() - mCurrentPaint.measureText(currStr)) / 2;
canvas.drawText(currStr, currX, centerY, mCurrentPaint);
// 畫當前行上面的
for (int i = mCurrentLine - 1; i >= 0; i--) {
String upStr = mLrcTexts.get(i);
float upX = (getWidth() - mNormalPaint.measureText(upStr)) / 2;
float upY = centerY - (mTextSize + mDividerHeight)
* (mCurrentLine - i);
canvas.drawText(upStr, upX, upY, mNormalPaint);
}
// 畫當前行下面的
for (int i = mCurrentLine + 1; i < mLrcTimes.size(); i++) {
String downStr = mLrcTexts.get(i);
float downX = (getWidth() - mNormalPaint.measureText(downStr)) / 2;
float downY = centerY + (mTextSize + mDividerHeight)
* (i - mCurrentLine);
canvas.drawText(downStr, downX, downY, mNormalPaint);
}
}
/**
* 加載歌詞文件
*
* @param lrcName
* assets下的歌詞文件名
* @throws Exception
*/
public void loadLrc(String lrcName) throws Exception {
mLrcTexts.clear();
mLrcTimes.clear();
BufferedReader br = new BufferedReader(new InputStreamReader(
getResources().getAssets().open(lrcName)));
String line;
while ((line = br.readLine()) != null) {
String[] arr = parseLine(line);
if (arr != null) {
mLrcTimes.add(Long.parseLong(arr[0]));
mLrcTexts.add(arr[1]);
}
}
br.close();
}
/**
* 加載歌詞文件
*
* @param isr
* @throws Exception
*/
public void loadLrcByUrl(String songid) throws Exception {
if (!songId.equals(songid)) {
mLrcTexts.clear();
mLrcTimes.clear();
mNextTime = 0;
mCurrentLine = 0;
mIsEnd = false;
musicRequest.requestStringLrcData(songid);
this.songId = songid;
}
}
/**
* 更新進度
*
* @param time
* 當前時間
*/
public synchronized void updateTime(long time) {
// 避免重復繪制
if (time < mNextTime || mIsEnd) {
return;
}
for (int i = 0; i < mLrcTimes.size(); i++) {
if (mLrcTimes.get(i) > time) {
Log.i(TAG, "newline ...");
mNextTime = mLrcTimes.get(i);
mCurrentLine = i < 1 ? 0 : i - 1;
// 屬性動畫只能在主線程使用,因此用Handler轉發操作
mHandler.sendEmptyMessage(MSG_NEW_LINE);
break;
} else if (i == mLrcTimes.size() - 1) {
// 最后一行
Log.i(TAG, "end ...");
mCurrentLine = mLrcTimes.size() - 1;
mIsEnd = true;
// 屬性動畫只能在主線程使用,因此用Handler轉發操作
mHandler.sendEmptyMessage(MSG_NEW_LINE);
break;
}
}
}
/**
* 解析一行
*
* @param line
* [00:10.61]走過了人來人往
* @return {10610, 走過了人來人往}
*/
private String[] parseLine(String line) {
Matcher matcher = Pattern.compile("\\[(\\d)+:(\\d)+(\\.)(\\d+)\\].+")
.matcher(line);
if (!matcher.matches()) {
Log.e(TAG, line);
return null;
}
line = line.replaceAll("\\[", "");
String[] result = line.split("\\]");
result[0] = parseTime(result[0]);
return result;
}
/**
* 解析時間
*
* @param time
* 00:10.61
* @return long
*/
private String parseTime(String time) {
time = time.replaceAll(":", "\\.");
String[] times = time.split("\\.");
long l = 0l;
try {
long min = Long.parseLong(times[0]);
long sec = Long.parseLong(times[1]);
long mil = Long.parseLong(times[2]);
l = min * 60 * 1000 + sec * 1000 + mil * 10;
} catch (NumberFormatException e) {
e.printStackTrace();
}
return String.valueOf(l);
}
/**
* 換行動畫 Note:屬性動畫只能在主線程使用
*/
private void newLineAnim() {
ValueAnimator animator = ValueAnimator.ofFloat(mTextSize
+ mDividerHeight, 0.0f);
animator.setDuration(mAnimationDuration);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mAnimOffset = (Float) animation.getAnimatedValue();
invalidate();
}
});
animator.start();
}
private static class LrcHandler extends Handler {
private WeakReference<LrcView> mLrcViewRef;
public LrcHandler(WeakReference<LrcView> lrcViewRef) {
mLrcViewRef = lrcViewRef;
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_NEW_LINE:
LrcView lrcView = mLrcViewRef.get();
if (lrcView != null) {
lrcView.newLineAnim();
}
break;
}
super.handleMessage(msg);
}
}
@Override
public void getSongImageURL(String songLrc) {
// 網絡請求成功歌詞
// Log.d("MaskMusic", songLrc);
parseSongLrc(songLrc);
}
private void parseSongLrc(String songLrc) {
Log.d("MaskMusic", songLrc);
String[] strs = songLrc.split("\\[");
for (String line : strs) {
line = ("[" + line).replace(":", ":").replace(".", ".")
.replace("
", "").replace(" ", " ")
.replace("-", "-").replace("(", "")
.replace(")", "").replace("&", "")
.replace(";", "").replace("'", "").replace("","");
String[] arr = parseLine(line);
if (arr != null) {
mLrcTimes.add(Long.parseLong(arr[0]));
mLrcTexts.add(arr[1]);
}
// Log.d("MaskMusic", line);
}
// 回調判斷有沒有歌詞
if (mLrcTexts.size() > 0 && mLrcTimes.size() > 0) {
isLrc = true;
}
lrcViewToMusicActivity.LrcViewIsLrc(isLrc);
}
@Override
public void playToEnd() {
// 播放完畢,進行初始化
// Log.d("MaskMusic", "playToEnd : 播放完畢");
mNextTime = 0;
mCurrentLine = 0;
mIsEnd = false;
updateTime(mNextTime);
}
@Override
public void playToPause(final long mt) {
Log.d("MaskMusic", "mNextTime CurrentTime : " + mt);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
System.out.println("執行了");
if (mLrcTexts.size() > 0 && mLrcTimes.size() > 0) {
for (int i = 0; i < mLrcTimes.size() - 1; i++) {
if (mt >= mLrcTimes.get(i)
&& mt <= mLrcTimes.get(i + 1)) {
Log.d("MaskMusic", mt + " 毫秒的歌詞為 "
+ mLrcTexts.get(i));
mNextTime = mLrcTimes.get(i);
mCurrentLine = i;
updateTime(mNextTime);
}
}
}else{
lrcViewToMusicActivity.LrcViewIsLrc(false);
}
Log.d("MaskMusic", "playToPause over");
}
}, 2000);
// 遇到問題 ,從MusicService 的 時間,很難與 集合中的時間匹配成功!
}
}
~~~
### ? ? (3)回調事件1 (LrcView-MusicActivity)
? ? ? ? ? ? ?作用是給MusicActivity 回調,判斷是否有歌詞
~~~
public interface LrcViewToMusicActivity {
/**
* LrcView的自定義事件,給
*/
/**
*
* 1.判斷是否有歌詞
* 2.在進行初始化成功后,2s之內沒有加載到歌詞就顯示提示
* @param isLrc,是否有歌詞
*/
void LrcViewIsLrc(boolean isLrc);
}
~~~
? ??
### ? ? ?(4)回調事件2(MusicActivity - LrcView)
~~~
<pre name="code" class="java">/**
* 接口實現意圖 :LrcView實現此接口,后在MusciActivity中,使用其接口,將調用LrcView中實現的playToEnd()方法,
* 進行歌詞初始化操作
*/
public interface LrcPlayToEnd {
/**
* 播放到最后,回調初始化 歌詞顯示
*/
void playToEnd();
/**
* 暫停后,初始化節面時,將歌詞設置到當前時間位置
*/
void playToPause(long mNextTime);
}
~~~
~~~
~~~
# 3.Activity與LrcView控制實現
### ? ?(1)一張圖看明白

### ? ? (2)LrcView - MusicActivity?
? ? ? ? ? ? ? 回調事件,一個接口作為目標的屬性,使用者實現這個接口,來使用,我們在前面已經使用了很多次,比如Fragment - > MainActivity 通信過程 等;
? ? ? ? ? ? ? 在這里,接口 LrcViewToMusicActivity ,作為LrcView的屬性,進行回調出是否有歌詞;在MusicActivity中實現該接口,使用有沒有歌詞;在初始化LrcView的時候,setLrcViewToMusicActivity(this)就可以實現;
### ? ? (3)MusicActivity -> LrcView?
? ? ? ? ? ? ?作用 : 判斷歌曲有沒接觸,當結束后出發,LrcView進行初始化操作;
? ? ? ? ? ? ?實現過程:
? ? ? ? ? ? ? ? 1)使得LrcView實現LrcViewToEnd 接口;
? ? ? ? ? ? ? ? 2)在MusicActivity中使用LrcViewtoEnd ,作為屬性使用,初始化的時候直接將
~~~
lrc = (LrcView) findViewById(R.id.lrc);
//初始化接口,多態實現
LrcViewToEnd lrcplaytoend = lrc;
~~~
# 4.總結
? ? 在使用過程中,最糾結的就是當音樂播放完畢的時候進行初始化歌詞了,當初使用了很多方法,比如觀察者模式,應該也是可以解決的,但無意當中,想到了這個簡單的方法,可以實現這個功能,得益于面向對象的多態的再次學習;所以基礎不能忘記,要時常學習,則會事半功倍。
- 前言
- Android實戰 - 音心音樂播放器 (開啟篇)
- Android實戰 - 音心音樂播發器 (主界面實現)
- Android實戰 - 音心播放器 (Music Service 實現)
- Android實戰 - 音心播放器 (通知實現音樂的播放/暫停/下一曲控制)
- Android實戰 - 音心播發器 (MusicService ,Notification, MainActivity 總結)
- Android實戰 - 音心播放器 (MusicActivity-音樂播放頁面界面實現)
- Android實戰 - 音心播放器 (MusciActivity-專輯圖片獲得,基本控制實現)
- Android實戰 - 音心播放器(MusicActivity - 歌詞實現)
- Android實戰 - 音心播放器 (MusicActivity - 倒計時 ,進度條實現)
- Android實戰 - 音心播放器 (MusicActivity ,MusicNotification,MusicService總結)
- Android實戰 - 音心播放器 (MusicListActivity - 分類信息界面實現)
- Android實戰 - 音心播放器 (MusicListActivity - 音樂播放和MainActivity的一個問題)
- Android實戰 - 音心播放器 (啟動頁與社交分享(ShareSDK))
- Android實戰 - 音心播放器 (優化Service退出,按兩下退出應用實現)
- Android實戰 - 音心播放器 (項目總結,應用打包發布)