当前位置: 代码迷 >> Android >> Android 歌词同步滚动成效
  详细解决方案

Android 歌词同步滚动成效

热度:66   发布时间:2016-05-01 13:43:25.0
Android 歌词同步滚动效果

歌词是播放器类App必不可少的组件,而一般的歌词组件都需要做到歌词的显示与播放进度同步。我们知道,歌词是如下所示的文件:

?

lrc?
[ti:原来爱情这么伤]
[ar:梁咏琪]
[al:给自己的情歌]


[00:00.55]梁咏琪 - 原来爱情这么伤
[00:05.43]作词:彭学斌
[00:06.68]作曲:彭学斌
[00:09.63]
[00:22.27]我睁开眼睛 却感觉不到天亮
[00:29.74]东西吃一半 莫名其妙哭一场
[00:37.06]我忍住不想 时间变得更漫长
[00:44.09]也与你有关 否则又开始胡思乱想
[00:53.81]我日月无光 忙得不知所以然
[00:59.96]找朋友交谈 其实全帮不上忙
[01:07.49]以为会习惯 有你在才是习惯
[01:14.62]你曾住在我心上 现在空了一个地方
[01:21.89]原来爱情这么伤 比想象中还难
[01:29.90]泪水总是不听话 幸福躲起来不声不响
[01:37.43]太多道理太牵强 道理全是一样
[01:44.34]说的时候很简单 爱上后却正巧打乱
[02:00.00]我日月无光 忙得不知所以然
[02:07.41]找朋友交谈 其实全帮不上忙
[02:15.07]以为会习惯 有你在才是习惯
[02:21.88]你曾住在我心上 现在空了一个地方
[02:29.38]原来爱情这么伤 比想象中还难
[02:36.60]泪水总是不听话 幸福躲起来不声不响
[02:44.22]太多道理太牵强 道理全是一样
[02:50.78]说的时候很简单 爱上后却正巧打乱
[03:00.32]只想变的坚强 强到能够去忘
[03:07.29]无所谓悲伤 只要学会抵抗
[03:14.19]原来爱情这么伤
[03:20.78]原来爱情是这样 这样峰回路转
[03:28.12]泪水明明流不干 瞎了眼还要再爱一趟
[03:35.83]有一天终于打完 思念的一场战
[03:43.45]回过头再看一看 原来爱情那么伤
[03:54.76]下次还会不会这样
[88:88.88]

?

我们需要读取以上歌词文件的每一行转换成成一个个歌词实体:

?

package com.music.lyricsync;public class LyricObject {	public int begintime; // 开始时间	public int endtime; // 结束时间	public int timeline; // 单句歌词用时	public String lrc; // 单句歌词}

?

可根据当前播放器的播放进度与每句歌词的开始时间,得到当前屏幕中央高亮显示的那句歌词。在UI线程中另起线程,通过回调函数 onDraw() 每隔100ms重新绘制屏幕,实现歌词平滑滚动的动画效果。MainActivity代码如下:

?

package com.music.lyricsync;import java.io.IOException;import android.app.Activity;import android.media.MediaPlayer;import android.net.Uri;import android.os.Bundle;import android.os.Environment;import android.os.Handler;import android.view.View;import android.view.View.OnClickListener;import android.widget.Button;import android.widget.SeekBar;import android.widget.SeekBar.OnSeekBarChangeListener;public class MainActivity extends Activity {	/** Called when the activity is first created. */	private LyricView lyricView;	private MediaPlayer mediaPlayer;	private Button button;	private SeekBar seekBar;	private String mp3Path;	private int INTERVAL=45;//歌词每行的间隔	@Override	public void onCreate(Bundle savedInstanceState) {		super.onCreate(savedInstanceState);		// this.requestWindowFeature(Window.FEATURE_NO_TITLE);		// getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);		setContentView(R.layout.main);		mp3Path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/LyricSync/1.mp3";		lyricView = (LyricView) findViewById(R.id.mylrc);		mediaPlayer = new MediaPlayer();		// this.requestWindowFeature(Window.FEATURE_NO_TITLE);		ResetMusic(mp3Path);		SerchLrc();		lyricView.SetTextSize();		button = (Button) findViewById(R.id.button);		button.setText("播放");		seekBar = (SeekBar) findViewById(R.id.seekbarmusic);		seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {			@Override			public void onStopTrackingTouch(SeekBar seekBar) {				// TODO Auto-generated method stub			}			@Override			public void onStartTrackingTouch(SeekBar seekBar) {				// TODO Auto-generated method stub			}			@Override			public void onProgressChanged(SeekBar seekBar, int progress,					boolean fromUser) {				// TODO Auto-generated method stub				if (fromUser) {					mediaPlayer.seekTo(progress);					lyricView.setOffsetY(220 - lyricView.SelectIndex(progress) 							* (lyricView.getSIZEWORD() + INTERVAL-1));				}			}		});		button.setOnClickListener(new OnClickListener() {			@Override			public void onClick(View v) {				// TODO Auto-generated method stub				if (mediaPlayer.isPlaying()) {					button.setText("播放");					mediaPlayer.pause();				} else {					button.setText("暂停");					mediaPlayer.start();					lyricView.setOffsetY(220 - lyricView.SelectIndex(mediaPlayer.getCurrentPosition())							* (lyricView.getSIZEWORD() + INTERVAL-1));				}			}		});		mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {			@Override			public void onCompletion(MediaPlayer mp) {				ResetMusic(mp3Path);				lyricView.SetTextSize();				lyricView.setOffsetY(200);				mediaPlayer.start();			}		});		seekBar.setMax(mediaPlayer.getDuration());		new Thread(new runable()).start();	}	public void SerchLrc() {		String lrc = mp3Path;		lrc = lrc.substring(0, lrc.length() - 4).trim() + ".lrc".trim();		LyricView.read(lrc);		lyricView.SetTextSize();		lyricView.setOffsetY(350);	}	public void ResetMusic(String path) {		mediaPlayer.reset();		try {			mediaPlayer.setDataSource(mp3Path);			mediaPlayer.prepare();		} catch (IllegalArgumentException e) {			// TODO Auto-generated catch block			e.printStackTrace();		} catch (IllegalStateException e) {			// TODO Auto-generated catch block			e.printStackTrace();		} catch (IOException e) {			// TODO Auto-generated catch block			e.printStackTrace();		}	}	class runable implements Runnable {		@Override		public void run() {			// TODO Auto-generated method stub			while (true) {				try {					Thread.sleep(100);					if (mediaPlayer.isPlaying()) {						lyricView.setOffsetY(lyricView.getOffsetY() - lyricView.SpeedLrc());						lyricView.SelectIndex(mediaPlayer.getCurrentPosition());						seekBar.setProgress(mediaPlayer.getCurrentPosition());						mHandler.post(mUpdateResults);					}				} catch (InterruptedException e) {					// TODO Auto-generated catch block					e.printStackTrace();				}			}		}	}	Handler mHandler = new Handler();	Runnable mUpdateResults = new Runnable() {		public void run() {			lyricView.invalidate(); // 更新视图		}	};}

?

歌词View的代码如下:

?

package com.music.lyricsync;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.Iterator;import java.util.TreeMap;import java.util.regex.Matcher;import java.util.regex.Pattern;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.view.View;public class LyricView extends View{		private static TreeMap<Integer, LyricObject> lrc_map;	private float mX;		//屏幕X轴的中点,此值固定,保持歌词在X中间显示	private float offsetY;		//歌词在Y轴上的偏移量,此值会根据歌词的滚动变小	private static boolean blLrc=false;	private float touchY;	//当触摸歌词View时,保存为当前触点的Y轴坐标	private float touchX;	private boolean blScrollView=false;	private int lrcIndex=0;	//保存歌词TreeMap的下标	private  int SIZEWORD=0;//显示歌词文字的大小值	private  int INTERVAL=45;//歌词每行的间隔	Paint paint=new Paint();//画笔,用于画不是高亮的歌词	Paint paintHL=new Paint();	//画笔,用于画高亮的歌词,即当前唱到这句歌词		public LyricView(Context context){		super(context);		init();	}		public LyricView(Context context, AttributeSet attrs) {		super(context, attrs);		init();	}		/* (non-Javadoc)	 * @see android.view.View#onDraw(android.graphics.Canvas)	 */	@Override	protected void onDraw(Canvas canvas) {		if(blLrc){			paintHL.setTextSize(SIZEWORD);			paint.setTextSize(SIZEWORD);			LyricObject temp=lrc_map.get(lrcIndex);			canvas.drawText(temp.lrc, mX, offsetY+(SIZEWORD+INTERVAL)*lrcIndex, paintHL);			// 画当前歌词之前的歌词			for(int i=lrcIndex-1;i>=0;i--){				temp=lrc_map.get(i);				if(offsetY+(SIZEWORD+INTERVAL)*i<0){					break;				}				canvas.drawText(temp.lrc, mX, offsetY+(SIZEWORD+INTERVAL)*i, paint);			}			// 画当前歌词之后的歌词			for(int i=lrcIndex+1;i<lrc_map.size();i++){				temp=lrc_map.get(i);				if(offsetY+(SIZEWORD+INTERVAL)*i>600){					break;				}				canvas.drawText(temp.lrc, mX, offsetY+(SIZEWORD+INTERVAL)*i, paint);			}		}		else{			paint.setTextSize(25);			canvas.drawText("找不到歌词", mX, 310, paint);		}		super.onDraw(canvas);	}	/* (non-Javadoc)	 * @see android.view.View#onTouchEvent(android.view.MotionEvent)	 */	@Override	public boolean onTouchEvent(MotionEvent event) {		// TODO Auto-generated method stub		System.out.println("bllll==="+blScrollView);		float tt=event.getY();		if(!blLrc){			//return super.onTouchEvent(event);			return super.onTouchEvent(event);		}		switch(event.getAction()){		case MotionEvent.ACTION_DOWN:			touchX=event.getX();			break;		case MotionEvent.ACTION_MOVE:			touchY=tt-touchY;						offsetY=offsetY+touchY;			break;		case MotionEvent.ACTION_UP:			blScrollView=false;			break;				}		touchY=tt;		return true;	}	public void init(){		lrc_map = new TreeMap<Integer, LyricObject>();		offsetY=320;					paint=new Paint();		paint.setTextAlign(Paint.Align.CENTER);		paint.setColor(Color.GREEN);		paint.setAntiAlias(true);		paint.setDither(true);		paint.setAlpha(180);						paintHL=new Paint();		paintHL.setTextAlign(Paint.Align.CENTER);				paintHL.setColor(Color.RED);		paintHL.setAntiAlias(true);		paintHL.setAlpha(255);	}		/**	 * 根据歌词里面最长的那句来确定歌词字体的大小	 */		public void SetTextSize(){		if(!blLrc){			return;		}		int max=lrc_map.get(0).lrc.length();		for(int i=1;i<lrc_map.size();i++){			LyricObject lrcStrLength=lrc_map.get(i);			if(max<lrcStrLength.lrc.length()){				max=lrcStrLength.lrc.length();			}		}		SIZEWORD=320/max;		}		protected void onSizeChanged(int w, int h, int oldw, int oldh) {		mX = w * 0.5f;		super.onSizeChanged(w, h, oldw, oldh);	}		/**	 *  歌词滚动的速度	 * 	 * @return 返回歌词滚动的速度	 */	public Float SpeedLrc(){		float speed=0;		if(offsetY+(SIZEWORD+INTERVAL)*lrcIndex>220){			speed=((offsetY+(SIZEWORD+INTERVAL)*lrcIndex-220)/20);		} else if(offsetY+(SIZEWORD+INTERVAL)*lrcIndex < 120){			Log.i("speed", "speed is too fast!!!");			speed = 0;		}//		if(speed<0.2){//			speed=0.2f;//		}		return speed;	}		/**	 * 按当前的歌曲的播放时间,从歌词里面获得那一句	 * @param time 当前歌曲的播放时间	 * @return 返回当前歌词的索引值	 */	public int SelectIndex(int time){		if(!blLrc){			return 0;		}		int index=0;		for(int i=0;i<lrc_map.size();i++){			LyricObject temp=lrc_map.get(i);			if(temp.begintime<time){				++index;			}		}		lrcIndex=index-1;		if(lrcIndex<0){			lrcIndex=0;		}		return lrcIndex;		}		/**	 * 读取歌词文件	 * @param file 歌词的路径	 * 	 */	public static void read(String file) {		TreeMap<Integer, LyricObject> lrc_read =new TreeMap<Integer, LyricObject>();		String data = "";		try {		  File saveFile=new File(file);		 // System.out.println("是否有歌词文件"+saveFile.isFile());		  if(!saveFile.isFile()){			  blLrc=false;			  return;		  }		  blLrc=true;		  		  //System.out.println("bllrc==="+blLrc);		  FileInputStream stream = new FileInputStream(saveFile);//  context.openFileInput(file);		  		  		  BufferedReader br = new BufferedReader(new InputStreamReader(stream,"GB2312"));   		  int i = 0;		  Pattern pattern = Pattern.compile("\\d{2}");		  while ((data = br.readLine()) != null) {   			 // System.out.println("++++++++++++>>"+data);			    data = data.replace("[","");//将前面的替换成后面的			    data = data.replace("]","@");			    String splitdata[] =data.split("@");//分隔			    if(data.endsWith("@")){			    	for(int k=0;k<splitdata.length;k++){				    	String str=splitdata[k];				    					    	str = str.replace(":",".");				    	str = str.replace(".","@");					    String timedata[] =str.split("@");					    Matcher matcher = pattern.matcher(timedata[0]);					    if(timedata.length==3 && matcher.matches()){						    int m = Integer.parseInt(timedata[0]);  //分						    int s = Integer.parseInt(timedata[1]);  //秒						    int ms = Integer.parseInt(timedata[2]); //毫秒					    	int currTime = (m*60+s)*1000+ms*10;					    	LyricObject item1= new LyricObject();							item1.begintime = currTime;							item1.lrc       = "";							lrc_read.put(currTime,item1);					    }			    	}				    			    }			    else{				    String lrcContenet = splitdata[splitdata.length-1]; 							    for (int j=0;j<splitdata.length-1;j++)				    {					    String tmpstr = splitdata[j];					    					    tmpstr = tmpstr.replace(":",".");					    tmpstr = tmpstr.replace(".","@");					    String timedata[] =tmpstr.split("@");					    Matcher matcher = pattern.matcher(timedata[0]);					    if(timedata.length==3 && matcher.matches()){						    int m = Integer.parseInt(timedata[0]);  //分						    int s = Integer.parseInt(timedata[1]);  //秒						    int ms = Integer.parseInt(timedata[2]); //毫秒					    	int currTime = (m*60+s)*1000+ms*10;					    	LyricObject item1= new LyricObject();							item1.begintime = currTime;							item1.lrc       = lrcContenet;							lrc_read.put(currTime,item1);// 将currTime当标签  item1当数据 插入TreeMap里							i++;					    }				    }			    }			    		  } 		 stream.close();		}		catch (FileNotFoundException e) {		}		catch (IOException e) {		}				/*		 * 遍历hashmap 计算每句歌词所需要的时间		*/		lrc_map.clear();		data ="";		Iterator<Integer> iterator = lrc_read.keySet().iterator();		LyricObject oldval  = null;		int i =0;		while(iterator.hasNext()) {			Object ob =iterator.next();						LyricObject val = (LyricObject)lrc_read.get(ob);						if (oldval==null)				oldval = val;			else			{				LyricObject item1= new LyricObject();				item1  = oldval;				item1.timeline = val.begintime-oldval.begintime;				lrc_map.put(new Integer(i), item1);				i++;				oldval = val;			}			if (!iterator.hasNext()) {				lrc_map.put(new Integer(i), val);			}					}	}			/**	 * @return the blLrc	 */	public static boolean isBlLrc() {		return blLrc;	}	/**	 * @return the offsetY	 */	public float getOffsetY() {		return offsetY;	}	/**	 * @param offsetY the offsetY to set	 */	public void setOffsetY(float offsetY) {		this.offsetY = offsetY;	}	/**	 * @return 返回歌词文字的大小	 */	public int getSIZEWORD() {		return SIZEWORD;	}	/**	 * 设置歌词文字的大小	 * @param sIZEWORD the sIZEWORD to set	 */	public void setSIZEWORD(int sIZEWORD) {		SIZEWORD = sIZEWORD;	}}
?

xml布局文件如下:

?

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="fill_parent"    android:layout_height="fill_parent"    android:background="#FFFFFF" >    <com.music.lyricsync.LyricView        android:id="@+id/mylrc"        android:layout_width="fill_parent"        android:layout_height="fill_parent"        android:layout_marginBottom="50dip"        android:layout_marginTop="50dip" />    <LinearLayout        xmlns:android="http://schemas.android.com/apk/res/android"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_alignParentBottom="true"        android:orientation="horizontal" >        <Button            android:id="@+id/button"            android:layout_width="wrap_content"            android:layout_height="wrap_content" />        <SeekBar            android:id="@+id/seekbarmusic"            android:layout_width="205px"            android:layout_height="wrap_content"            android:layout_gravity="center_vertical"            android:layout_marginBottom="5px"            android:progress="0" />    </LinearLayout></RelativeLayout>
?

程序运行后如下图所示:


?

?

?

运行程序前,先在SDCard根目录下新建LyricSync目录,将 歌曲和歌词.zip 中的 1.mp3 和 1.lrc 文件解压放入LyricSync目录下即可。

?

?

  相关解决方案