一、流程分析

1.点击播放按钮,会根据lrc名调用LrcProcessor的process()分析歌词文件,得到时间队列和歌词队列

2.new一个hander,把时间队列和歌词队列传给自定义的线程类UpdateTimeCallback,调用handler.postDelayed(updateTimeCallback, 5);启动线程

3.UpdateTimeCallback会在线程执行时用当前时间减去成员变量begin,则可知歌曲播放了多久,再根据此时间与时间队列的时间比较,就是知道此时要显示什么歌词,从而把歌词队列的一个message设置给lrcTextView以显示

4.UpdateTimeCallback最后会自己调用handler.postDelayed(updateTimeCallback, 100);,所以线程会每0.1秒判断一次歌词的显示

PS:此代码有一个不足之处,即使app后台播放,更新歌词的线程仍会执行,浪费资源,一个版本会通过broastreciever来解决此问题

二、简介

在linux用apk处理歌词

三、代码
1.xml

2.java
(1)PlayerActivity.java

 package tony.mp3player;

 import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.List;
import java.util.Queue; import tony.model.Mp3Info;
import tony.mp3player.service.PlayerService;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageButton;
import android.widget.TextView; public class PlayerActivity extends Activity { private ImageButton beginBtn = null;
private ImageButton pauseBtn = null;
private ImageButton stopBtn = null; private List<Queue> queues = null;
private TextView lrcTextView = null;
private Mp3Info info = null;
private Handler handler = new Handler();
private UpdateTimeCallback updateTimeCallback = null;
private long begin = 0;
private long nextTimeMill = 0;
private long currentTimeMill = 0;
private String msg = null;
private long pauseTimeMills = 0;
private boolean isPlaying = false; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.player);
Intent intent = getIntent();
info = (Mp3Info) intent.getSerializableExtra("mp3Info");
beginBtn = (ImageButton) findViewById(R.id.begin);
pauseBtn = (ImageButton) findViewById(R.id.pause);
stopBtn = (ImageButton) findViewById(R.id.stop);
lrcTextView = (TextView) findViewById(R.id.lrcText); beginBtn.setOnClickListener(new BeginListener());
pauseBtn.setOnClickListener(new PauseListener());
stopBtn.setOnClickListener(new StopListener());
} /**
* 根据歌词文件的名字,来读取歌词文件当中的信息
* @param lrcName
*/
private void prepareLrc(String lrcName) {
try {
InputStream inputStream;
inputStream = new FileInputStream(Environment.getExternalStorageDirectory().getAbsolutePath() +
File.separator + "mp3" + File.separator + info.getLrcName());
LrcProcessor lrcProcessor = new LrcProcessor();
queues = lrcProcessor.process(inputStream);
updateTimeCallback = new UpdateTimeCallback(queues);
begin = 0;
currentTimeMill = 0;
nextTimeMill = 0;
} catch (Exception e) {
e.printStackTrace();
}
} class BeginListener implements OnClickListener {
@Override
public void onClick(View v) {
if(!isPlaying) {
//创建一个Intent对象,用于通知Service开始播放MP3
Intent intent = new Intent();
intent.putExtra("mp3Info", info);
intent.putExtra("MSG", AppConstant.PlayerMsg.PLAY_MSG);
intent.setClass(PlayerActivity.this, PlayerService.class);
//读取LRC文件,放于startservice前,是为了防止歌曲已播放,但歌词没读完,造成不同步
prepareLrc(info.getLrcName());
startService(intent);
begin = System.currentTimeMillis();
handler = new Handler();
handler.postDelayed(updateTimeCallback, 5);//5毫秒是试验得出的
isPlaying = true;
}
}
} class PauseListener implements OnClickListener {
@Override
public void onClick(View v) {
//通知Service暂停播放MP3
Intent intent = new Intent();
intent.putExtra("MSG", AppConstant.PlayerMsg.PAUSE_MSG);
intent.setClass(PlayerActivity.this, PlayerService.class);
startService(intent);
if(isPlaying) {
//不再更新歌词
handler.removeCallbacks(updateTimeCallback);
//用来下面代码计算暂停了多久
pauseTimeMills = System.currentTimeMillis();
} else {
handler.postDelayed(updateTimeCallback, 5);
//因为下面的时间偏移是这样计算的offset = System.currentTimeMillis() - begin;
//所以要把暂停的时间加到begin里去,
begin = System.currentTimeMillis() - pauseTimeMills + begin;
}
isPlaying = !isPlaying;
}
} class StopListener implements OnClickListener {
@Override
public void onClick(View v) {
//通知Service停止播放MP3文件
Intent intent = new Intent();
intent.putExtra("MSG", AppConstant.PlayerMsg.STOP_MSG);
intent.setClass(PlayerActivity.this, PlayerService.class);
startService(intent);
//从Handler当中移除updateTimeCallback
handler.removeCallbacks(updateTimeCallback);
isPlaying = false;
}
} class UpdateTimeCallback implements Runnable{
Queue<Long> times = null;
Queue<String> msgs = null; public UpdateTimeCallback(List<Queue> queues) {
this.times = queues.get(0);
this.msgs = queues.get(1);
} @Override
public void run() {
//计算偏移量,也就是说从开始播放MP3到现在为止,共消耗了多少时间,以毫秒为单位
long offset = System.currentTimeMillis() - begin;
if(currentTimeMill == 0) {//刚开始播放时,调用prepareLrc(),在其中设置currentTimeMill=0
nextTimeMill = times.poll();
msg = msgs.poll();
}
//歌词的显示是如下:例如
//[00:01.00]Look
//[00:03.00]Up
//[00:06.00]Down
//则在第1~3秒间是显示“look”,在第3~6秒间是显示"Up",在第6秒到下一个时间点显示"Down"
if(offset >= nextTimeMill) {
lrcTextView.setText(msg);
msg = msgs.poll();
nextTimeMill = times.poll();
}
currentTimeMill = currentTimeMill + 100;
//在run方法里调用postDelayed,则会形成循环,每0.01秒执行一次线程
handler.postDelayed(updateTimeCallback, 100);
}
}
}

(2)PlayService.java

 package tony.mp3player.service;

 import java.io.File;

 import tony.model.Mp3Info;
import tony.mp3player.AppConstant;
import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Environment;
import android.os.IBinder; public class PlayerService extends Service { private boolean isPlaying = false;
private boolean isPause = false;
private boolean isReleased = false;
private MediaPlayer mediaPlayer = null; @Override
public IBinder onBind(Intent intent) {
return null;
} @Override
public int onStartCommand(Intent intent, int flags, int startId) {
Mp3Info info = (Mp3Info) intent.getSerializableExtra("mp3Info");
int MSG = intent.getIntExtra("MSG", 0);
if(info != null) {
if(MSG == AppConstant.PlayerMsg.PLAY_MSG) {
play(info);
}
} else {
if(MSG == AppConstant.PlayerMsg.PAUSE_MSG) {
pause();
}
else if(MSG == AppConstant.PlayerMsg.STOP_MSG) {
stop();
}
}
return super.onStartCommand(intent, flags, startId);
} private void stop() {
if(mediaPlayer != null) {
if(isPlaying) {
if(!isReleased) {
mediaPlayer.stop();
mediaPlayer.release();
isReleased = true;
isPlaying = false;
}
}
}
} private void pause() {
if(mediaPlayer != null) {
if(!isReleased){
if(!isPause) {
mediaPlayer.pause();
isPause = true;
} else {
mediaPlayer.start();
isPause = false;
}
}
}
} private void play(Mp3Info info) {
if(!isPlaying) {
String path = getMp3Path(info);
mediaPlayer = MediaPlayer.create(this, Uri.parse("file://" + path));
mediaPlayer.setLooping(false);
mediaPlayer.start();
isPlaying = true;
isReleased = false;
}
} private String getMp3Path(Mp3Info mp3Info) {
String SDCardRoot = Environment.getExternalStorageDirectory()
.getAbsolutePath();
String path = SDCardRoot + File.separator + "mp3" + File.separator
+ mp3Info.getMp3Name();
return path;
}
}

3.LrcProcessor.java

 package tony.mp3player;

 import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;
import java.util.regex.Matcher;
import java.util.regex.Pattern; public class LrcProcessor { public ArrayList<Queue> process(InputStream inputStream) {
Queue<Long> timeMills = new LinkedList<Long>();
Queue<String> messages = new LinkedList<String>();
ArrayList<Queue> queues = new ArrayList<Queue>();
try {
InputStreamReader inputReader = new InputStreamReader(inputStream);
BufferedReader bufferReader = new BufferedReader(inputReader);
//创建一个正则表达式对象,寻找两边都带中括号的文本
Pattern p = Pattern.compile("\\[([^\\]]+)\\]");
String temp = null;
String result = null;
while((temp = bufferReader.readLine()) != null) {
Matcher m = p.matcher(temp);
if(m.find()) {
if(result != null) {//正则第一次到时是,此时result还没值,到下一次循环时,就会把第一次计算出的result加到队列里
messages.add(result);
}
String timeStr = m.group();
Long timeMill = time2Long(timeStr.substring(1, timeStr.length() - 1));
timeMills.offer(timeMill);//和add相比,offer不会抛异常
//取出时间串后面的歌词,如[00:02.31]Lose Yourself,得到“Lose Yourself”
String msg = temp.substring(timeStr.length());
result = "" + msg + "\n";//防止msg为null时抛nullpoint
} else {
result = result + temp + "\n";
//比如歌词如下:则上面的if会得到result = a + "\n",
//而else里会使result = a + "\n" + b + "\n" + c + "\n"
//[00:32.42]a
//b
//c
}
}
messages.add(result);//把最后一次循环的result加到quenue里
queues.add(timeMills);//把时间队列和歌词队列都加到list里
queues.add(messages);
} catch (Exception e) {
e.printStackTrace();
}
return queues;
} private Long time2Long(String timeStr) {
//eg : 00:02.31
String [] s = timeStr.split(":");
int min = Integer.parseInt(s[0]);
String ss[] = s[1].split("\\.");
int sec = Integer.parseInt(ss[0]);
int mill = Integer.parseInt(ss[1]);
return min * 60 * 1000 + sec * 1000 + mill * 10L;
} }

4.AppConstant.java

 package tony.mp3player;

 public interface AppConstant {

     public class PlayerMsg {
public static final int PLAY_MSG = 1;
public static final int PAUSE_MSG = 2;
public static final int STOP_MSG =3;
}
public class URL {
public static final String BASE_URL = "http://192.168.1.104:8080/mp3/";
}
}

ANDROID_MARS学习笔记_S01原始版_023_MP3PLAYER004_同步显示歌词的更多相关文章

  1. ANDROID_MARS学习笔记_S01原始版_005_RadioGroup\CheckBox\Toast

    一.代码 1.xml(1)radio.xml <?xml version="1.0" encoding="utf-8"?> <LinearLa ...

  2. ANDROID_MARS学习笔记_S01原始版_004_TableLayout

    1.xml <?xml version="1.0" encoding="utf-8"?> <TableLayout xmlns:android ...

  3. ANDROID_MARS学习笔记_S01原始版_003_对话框

    1.AndroidManifest.xml <?xml version="1.0" encoding="utf-8"?> <manifest ...

  4. ANDROID_MARS学习笔记_S01原始版_002_实现计算乘积及menu应用

    一.代码 1.xml(1)activity_main.xml <RelativeLayout xmlns:android="http://schemas.android.com/apk ...

  5. ANDROID_MARS学习笔记_S01原始版_001_Intent

    一.Intent简介 二.代码 1.activity_main.xml <RelativeLayout xmlns:android="http://schemas.android.co ...

  6. ANDROID_MARS学习笔记_S01原始版_023_MP3PLAYER005_用广播BroacastReciever实现后台播放不更新歌词

    一.代码流程1.自定义一个AppConstant.LRC_MESSAGE_ACTION字符串表示广播"更新歌词" 2.在PlayerActivity的onResume()注册Bro ...

  7. ANDROID_MARS学习笔记_S01原始版_023_MP3PLAYER003_播放mp3

    一.简介 1.在onListItemClick中实现点击条目时,跳转到PlayerActivity,mp3info通过Intent传给PlayerActivity 2.PlayerActivity通过 ...

  8. ANDROID_MARS学习笔记_S01原始版_022_MP3PLAYER002_本地及remote标签

    一.简介 1.在main.xml中用TabHost.TabWidget.FrameLayout标签作布局 2.在MainActivity中生成TabHost.TabSpec,调用setIndicato ...

  9. ANDROID_MARS学习笔记_S01原始版_021_MP3PLAYER001_下载mp3文件

    一.简介 1.在onListItemClick()中new Intent,Intent以存储序列化后的mp2Info对象作为参数,启动serivce 2.DownloadService在onStart ...

随机推荐

  1. 【C#4.0图解教程】笔记(第9章~第18章)

    第9章 语句 1.标签语句 ①.标签语句由一个标识符后面跟着一个冒号再跟着一条语句组成 ②.标签语句的执行完全如同标签不存在一样,并仅执行冒号后的语句. ③.给语句添加一个标签允许控制从代码的另一部分 ...

  2. listview使用checkbox批量删除出现的问题

    1.选中前面的checkbox导致后的checkbox被选中 2.选中后下滑listview,再上滑时被选中的checkbox又变成未选中状态. 问题大都是因为对listview进行代码优化重用con ...

  3. 【HTTPS】Https和SSL学习笔记(二)

    此文讲述证书的相关信息,参考文章链接http://www.guokr.com/post/116169/ 一. 证书的类型 常用的几种证书如下: (1) SSL证书,用于加密HTTP (2) 代码签名证 ...

  4. 第一篇、C_高精度加法

    简介: C语言中,整型占4字节,现在要计算两个100(假设)位以内的数想加,如果只是用整型去存储,明显就会越界.那么,我们有什么好的方法去完成这一操作呢? 1.用数组实现 数组中可以可以存储一定长度的 ...

  5. iOS获取webview高度

    int webHeight = [[webView stringByEvaluatingJavaScriptFromString:@"document.documentElement.scr ...

  6. nodejs-fs使用

    (1)读取文本文件时须添加上'encoding'才能输出可读的内容. 02.txt hello,world! nodejs_readfile.js var fs = require('fs'); fs ...

  7. 376. Wiggle Subsequence

    A sequence of numbers is called a wiggle sequence if the differences between successive numbers stri ...

  8. NHibernate多对多关联映射的实现

    上次用EF演示了数据库多对多关系的操作,这次我们还是引用上次的案例,来演示如何在C#当中使用NHibernate. 首先介绍一下NHibernate框架的来源.熟悉Java编程的读者肯定知道Hiber ...

  9. rpm方式安装gcc缺少依赖项的解决方法

    使用rpm方式安装gcc时,有时会报缺少依赖项: libmpfr.so.1 is needed by cpp-4.4.4-13.el6.i686 libppl.so.7 is needed by cl ...

  10. 4个好用的JS联动选择插件

    jQuery City Select 一个简单的jQuery省市联动插件,可以自定义JSON字典实现其他内容的联动选择菜单. PCAS省.市.地区联动选择JS封装类 PCAS可能是国内使用人数最多的J ...