Android实战 – 音心播放器(MusicActivity – 歌词实现)
最后更新于:2022-04-01 10:52:54
### 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)一张图看明白
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-19_5715764dde417.jpg)
### (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.总结
在使用过程中,最纠结的就是当音乐播放完毕的时候进行初始化歌词了,当初使用了很多方法,比如观察者模式,应该也是可以解决的,但无意当中,想到了这个简单的方法,可以实现这个功能,得益于面向对象的多态的再次学习;所以基础不能忘记,要时常学习,则会事半功倍。