`
superonion
  • 浏览: 126972 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

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目录下即可。

 

 

  • 大小: 58.9 KB
  • src.zip (179.2 KB)
  • 下载次数: 602
7
1
分享到:
评论
4 楼 qq_31941407 2015-12-24  
请问怎么设置才不是自动播放啊,为什么我一运行就是自动播放啊。。
3 楼 whl_1990 2015-06-05  
这个有点抖动,不知道怎么去抖动啊?将线程睡眠时间降低也不行,可能要改成SurfaceView
2 楼 翼扬gg 2015-05-16  
      
1 楼 blue飞扬 2013-09-12  
求教那个歌词速度和文字大小设定是怎么完成的啊?新手,有点看不懂

相关推荐

Global site tag (gtag.js) - Google Analytics