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;  
		    }  
		      
		
	}





然后就是创建一个用来承载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的基本实现就是这样了。