Android实战 - 音心播放器(MusicActivity - 歌词实现)

1.背景

    歌词是音乐软件必备的,没有它的存在就感觉少点什么,故实现了歌曲歌词的显示,使用LrcView实现,当然是在GitHub上找到的,是一个自定义View :

    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-音乐播放页面界面实现,网络模块实现

                  几个重要的方法 说明:

                   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.总结

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


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值