MusicPlayer Lrc歌词控件的实现
最近在做一个音乐播放器,关于其中歌词控件,上网查过了一些资料,然后进行修改,也算完整的实现了其功能。先看看实现后的效果。
实现的原理实际上是自定义一个View来显示歌词,然后利用View.invalidate()方法来不断的调用onDraw方法,重绘该View。
至于歌词的读取,实际上按照路径读取歌词文件,然后将其时间和每一句歌词分解开并给每一句一个index,通过处理当前播放时间来得到index以此来显示。
具体实现分别如下:
首先创建一个IrcContent类,相当于结构体。每一个IrcContent的实例实际上就是一句歌词,而真个Irc文件就是个List<IrcContent>.
package com.example.zhsmusicplayer;
/*
* 歌词实体类
* 每一句带时间的歌词就是一个LrcContent实例
*
* */
public class LrcContent {
private String lrcStr; //歌词内容
private int lrcTime; //歌词当前时间
public String getLrcStr() {
return lrcStr;
}
public void setLrcStr(String lrcStr) {
this.lrcStr = lrcStr;
}
public int getLrcTime() {
return lrcTime;
}
public void setLrcTime(int lrcTime) {
this.lrcTime = lrcTime;
}
}
然后创建一个LrcProcess类用来读取歌词并将歌词中的时间Time转化为 MediaPlayer类中所使用的getCurrentPosition的毫秒。
package com.example.zhsmusicplayer;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import android.util.Log;
/*
* 用来读取歌词并且解析类
*
* */
public class LrcProcess {
private List<LrcContent> lrcList;//List集合用来储存歌词对象(每一句歌词)
private LrcContent mLrcContent;
/* 无参数构造函数*/
public LrcProcess() {
mLrcContent = new LrcContent();
lrcList = new ArrayList<LrcContent>();
}
/* 读取某个地址下的歌词文件*/
public String readLRC(String path){
StringBuilder stringBuilder = new StringBuilder();
//这里是直接将MP3的地址直接赋予lrc,及要求lrc的地址与MP3地址一致且名称与MP3一致
File f = new File(path.replace(".mp3", ".lrc"));
//创建一个文件输入流
try {
//创建一个文件输入流对象
FileInputStream fis = new FileInputStream(f);
InputStreamReader isr = new InputStreamReader(fis, "utf-8");
BufferedReader br = new BufferedReader(isr);
String s = "";
while((s = br.readLine()) != null) {
//替换字符
s = s.replace("[", "");
s = s.replace("]", "@");
//分离“@”字符
String splitLrcData[] = s.split("@");
if(splitLrcData.length > 1) {
mLrcContent.setLrcStr(splitLrcData[1]);
//处理歌词取得歌曲的时间
int lrcTime = time2Str(splitLrcData[0]);
mLrcContent.setLrcTime(lrcTime);
//添加进列表数组
lrcList.add(mLrcContent);
//新创建歌词内容对象
mLrcContent = new LrcContent();
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
stringBuilder.append("木有歌词文件,赶紧去下载!...");
} catch (IOException e) {
e.printStackTrace();
stringBuilder.append("木有读取到歌词哦!");
}
return stringBuilder.toString();
}
/**
* 解析歌词时间
* 就是将读取到的Time转换为duration
*/
public int time2Str(String timeStr) {
timeStr = timeStr.replace(":", ".");
timeStr = timeStr.replace(".", "@");
String timeData[] = timeStr.split("@"); //将时间分隔成字符串数组
//分离出分、秒并转换为整型
int minute = Integer.parseInt(timeData[0]);
int second = Integer.parseInt(timeData[1]);
int millisecond = Integer.parseInt(timeData[2]);
//计算上一行与下一行的时间转换为毫秒数
int currentTime = (minute * 60 + second) * 1000 + millisecond * 10;
return currentTime;
}
public List<LrcContent> getLrcList() {
return lrcList;
}
}
<span style="font-family:Arial, Helvetica, sans-serif;"><span style="white-space: normal;"></span></span> 关于File f = new File(path.replace(".mp3", ".lrc")); 这一句是用来读取某地址下的歌词文件,我这里path参数实际上MP3文件的地址,这就意味着,我打开歌词文件的地址必须和MP3地址相同,在同一个文件夹且文件名也相同。
接下来就是自定义View作为IrcView了,这里继承了TextView,复写了onDraw()函数来显示需要的信息。
<pre name="code" class="java">package com.example.zhsmusicplayer;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
class CustomBar extends RelativeLayout
{
private TextView musicName;
private ImageView musicImg;
private TextView musicArtist;
private TextView musicTime;
private ImageView iconPlay;
private ImageView iconNext;
private ImageView iconPrev;
private ProgressBar timeBar;
public CustomBar(Context context)
{
super(context);
}
public CustomBar(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
LayoutInflater inflater=(LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.custom_music_play, this);
if(isInEditMode()){return;}
musicName=(TextView)findViewById(R.id.name_text);
musicArtist=(TextView)findViewById(R.id.author_text);
musicTime=(TextView)findViewById(R.id.time_text);
musicImg=(ImageView)findViewById(R.id.music_img);
iconPlay=(ImageView)findViewById(R.id.play_img);
iconNext=(ImageView)findViewById(R.id.next_img);
iconPrev=(ImageView)findViewById(R.id.prev_img);
timeBar=(ProgressBar)findViewById(R.id.time_progressBar);
}
public void setMusicName(String name){
musicName.setText(name);
}
public void setMusicArtist(String Author){
musicArtist.setText(Author);
}
public void setMusicTime(String time){
musicTime.setText(time);
}
public void setOnMusicPlayListener(OnClickListener playListener){
iconPlay.setOnClickListener(playListener);
}
public void setOnMusicPlayNextListener(OnClickListener playNextListener){
iconNext.setOnClickListener(playNextListener);
}
public void setOnMusicPlayPrevListener(OnClickListener playPrevListener){
iconPrev.setOnClickListener(playPrevListener);
}
public void setPlayImg(boolean isPause){
if(isPause)
iconPlay.setImageResource(R.drawable.icon_pause);
else
iconPlay.setImageResource(R.drawable.icon_play);
}
public void setTimeBarMax(int max){
timeBar.setMax(max);
}
public void setTimeBarProgress(int progress){
timeBar.setProgress(progress);
}
}
class LrcView extends TextView{
private float width; //歌词视图宽度
private float height; //歌词视图高度
private Paint currentPaint; //当前画笔对象
private Paint notCurrentPaint; //非当前画笔对象
private float textHeight = 25; //文本高度
private float textSize = 18; //文本大小
private int index = 0; //list集合下标
private List<LrcContent> mLrcList = new ArrayList<LrcContent>();
public void setmLrcList(List<LrcContent> mLrcList) {
this.mLrcList = mLrcList;
}
public LrcView(Context context) {
super(context);
init();
}
public LrcView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public LrcView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
setFocusable(true); //设置可对焦
//高亮部分
currentPaint = new Paint();
currentPaint.setAntiAlias(true); //设置抗锯齿,让文字美观饱满
currentPaint.setTextAlign(Paint.Align.CENTER);//设置文本对齐方式
//非高亮部分
notCurrentPaint = new Paint();
notCurrentPaint.setAntiAlias(true);
notCurrentPaint.setTextAlign(Paint.Align.CENTER);
}
/**
* 绘画歌词
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(canvas == null) {
return;
}
//Log.e("zhs","Index in LrcView Ondraw====="+index);
/*if(index!=0)
Log.e("zhs",mLrcList.get(index).getLrcStr()); */
//设置字体的大小以及绘制出来的画笔
currentPaint.setColor(Color.argb(210, 251, 248, 29));
notCurrentPaint.setColor(Color.argb(140, 255, 255, 255));
currentPaint.setTextSize(24);
currentPaint.setTypeface(Typeface.SERIF);
notCurrentPaint.setTextSize(textSize);
notCurrentPaint.setTypeface(Typeface.DEFAULT);
try {
setText("");
canvas.drawText(mLrcList.get(index).getLrcStr(), width / 2, height / 2, currentPaint);
float tempY = height / 2;
//画出本句之前的句子
for(int i = index - 1; i >= 0; i--) {
//向上推移
tempY = tempY - textHeight;
canvas.drawText(mLrcList.get(i).getLrcStr(), width / 2, tempY, notCurrentPaint);
}
tempY = height / 2;
//画出本句之后的句子
for(int i = index + 1; i < mLrcList.size(); i++) {
//往下推移
tempY = tempY + textHeight;
canvas.drawText(mLrcList.get(i).getLrcStr(), width / 2, tempY, notCurrentPaint);
}
} catch (Exception e) {
setText("异常了");
}
}
/**
* 当view大小改变的时候调用的方法
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
this.width = w;
this.height = h;
}
public void setIndex(int index) {
this.index = index;
}
}
绘制的原理就是当前index指示的即为当前播放的歌词为一种Paint,而利用另外一种Paint来绘制出其余的歌词。利用setmLrcList(List<LrcContent> mLrcList)函数与setIndex(int index)函数来设置歌词文件以及当前index。
然后就是创建一个用来承载LrcView的MusicPlayActivity.xml文件如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.zhsmusicplayer.MusicPlayActivity"
android:background="#ffffff"
tools:ignore="MergeRootFrame" >
<com.example.zhsmusicplayer.LrcView
android:id="@+id/lrcShowView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#486432"
android:layout_centerHorizontal="true" />
</RelativeLayout>
接下来要做的就是在播放音乐的service中处理以获取到Index与当前播放音乐的地址并将其传给activity.
获取index,如下:
/**
* 根据时间获取歌词显示的索引值
* @return
*/
public int lrcIndex() {
if(mediaPlayer.isPlaying()) {
currentTime = mediaPlayer.getCurrentPosition();
duration = mediaPlayer.getDuration();
}
if(currentTime < duration) {
for (int i = 0; i < lrcList.size(); i++) {
if (i < lrcList.size() - 1) {
if (currentTime < lrcList.get(i).getLrcTime() && i == 0) {
index = i;
}
if (currentTime > lrcList.get(i).getLrcTime()
&& currentTime < lrcList.get(i + 1).getLrcTime()) {
index = i;
}
}
if (i == lrcList.size() - 1
&& currentTime > lrcList.get(i).getLrcTime()) {
index = i;
}
}
}
return index;
}
初始化歌词,开启线程不断的向Activity中传递数据。
/**
* 初始化歌词配置
*/
public void initLrc(){
handler.post(mRunnable);
}
Runnable mRunnable = new Runnable() {
@Override
public void run() {
intentForLrc = new Intent("message_for_lrc");
intentForLrc.putExtra("path",path);
intentForLrc.putExtra("LrcIndext",lrcIndex());
sendBroadcast(intentForLrc);
handler.postDelayed(mRunnable, 100);
}
};
runnable线程会不断地向Activity发送包括地址与Index信息的广播。
在播放音乐的同时调用initLrc()函数来不断获取并发送歌词的信息。
/**
* 播放音乐
* @param position
*/
private void play(int position) {
try {
initLrc();
mediaPlayer.reset();//把各项参数恢复到初始状态
mediaPlayer.setDataSource(path);
mediaPlayer.prepare(); //进行缓冲
// mediaPlayer.setOnPreparedListener(new PreparedListener(position));//注册一个监听器
mediaPlayer.start();
isPlaying = true;
}
catch (Exception e) {
e.printStackTrace();
}
}
最后就是Activity中进行广播的接收以及控件的重绘了。
package com.example.zhsmusicplayer;
import java.util.List;
import com.example.zhsmusicplayer.MusicListActivity.widgetReceiver;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.animation.AnimationUtils;
public class MusicPlayActivity extends Activity {
LrcView lrcView;
ServiceReceiver LrcReceiver;
IntentFilter intentFilter;//接收来自Service的广播,用来跟新歌词信息
Handler handler;//handler用来执行实现LrcIndex的检测的接收
Runnable mRunnable;
int currentTime,duration;
String path =null;//当前歌词的path
int Index=-1;//歌词每一句的索引
LrcProcess mLrcProcess;
List<LrcContent> lrcList;
boolean IsRunnable=false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_music_play);
lrcView = (LrcView) findViewById(R.id.lrcShowView);
//广播的注册
LrcReceiver=new ServiceReceiver();
intentFilter = new IntentFilter();
intentFilter.addAction("message_for_lrc");
registerReceiver(LrcReceiver, intentFilter);
//handler
handler=new Handler(){};
//该标志位的目的就是每次进入到该activity执行一次handler.post(mRunnable)
IsRunnable=false;
//Runnable
mRunnable = new Runnable() {
@Override
public void run() {
lrcView.setIndex(Index);
lrcView.invalidate();
handler.postDelayed(mRunnable, 100);
}
};
}
@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
unregisterReceiver(LrcReceiver);
}
@Override
protected void onPause() {
// TODO Auto-generated method stub
super.onPause();
}
//接收来自widget的broadcast
public class ServiceReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// TODO Auto-generated method stub
String action = intent.getAction();
if(action.equals("message_for_lrc")){
if(intent.hasExtra("path")){
path=intent.getStringExtra("path");
mLrcProcess = new LrcProcess();
//读取歌词文件
mLrcProcess.readLRC(path);
//传回处理后的歌词文件
lrcList = mLrcProcess.getLrcList();
lrcView.setmLrcList(lrcList);
if(IsRunnable){}
else{
handler.post(mRunnable);
IsRunnable=true;
}
}
if(intent.hasExtra("LrcIndext")){
if(intent.getIntExtra("LrcIndext",-1)!=Index){
Index=intent.getIntExtra("LrcIndext",-1);
}
}
}
}
}
}
动态注册广播接收器同时创建ruannable线程来不断更新LrcView。实际上由于只需要在第一次获取到path信息的时候启动Runnable,故设立了标志位IsRunnable
来进行控制。同时保证了当你每次跳到该Activity时,可以调用Runnable来继续刷新IrcView。
IrcView的基本实现就是这样了。