android开发歌词滑动效果_Android 歌词同步滚动效果

本文详细介绍了如何在Android应用中实现歌词显示与播放进度同步的效果。通过解析LRC歌词文件,创建LyricObject类存储歌词信息,并根据播放器的进度动态更新歌词显示。在UI线程中使用Handler和Runnable实现平滑滚动动画,同时提供了关键代码片段以供参考。
摘要由CSDN通过智能技术生成

歌词是播放器类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]

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

Java代码   

package com.music.lyricsync;

public class LyricObject {

public int begintime; // 开始时间

public int endtime; // 结束时间

public int timeline; // 单句歌词用时

public String lrc; // 单句歌词

}

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

Java代码   

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的代码如下:

Java代码   

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

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();

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

LyricObject lrcStrLength=lrc_map.get(i);

if(max

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 

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

LyricObject temp=lrc_map.get(i);

if(temp.begintime

++index;

}

}

lrcIndex=index-1;

if(lrcIndex<0){

lrcIndex=0;

}

return lrcIndex;

}

/**

* 读取歌词文件

* @param file 歌词的路径

*

*/

public static void read(String file) {

TreeMap lrc_read =new TreeMap();

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

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

{

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 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代码   

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:background="#FFFFFF" >

android:id="@+id/mylrc"

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:layout_marginBottom="50dip"

android:layout_marginTop="50dip" />

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" >

android:id="@+id/button"

android:layout_width="wrap_content"

android:layout_height="wrap_content" />

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" />

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值