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.总结     在使用过程中,最纠结的就是当音乐播放完毕的时候进行初始化歌词了,当初使用了很多方法,比如观察者模式,应该也是可以解决的,但无意当中,想到了这个简单的方法,可以实现这个功能,得益于面向对象的多态的再次学习;所以基础不能忘记,要时常学习,则会事半功倍。
';