一、流程分析
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来解决此问题
二、简介
三、代码
1.xml
2.java
(1)PlayerActivity.java
1 package tony.mp3player; 2 3 import java.io.File; 4 import java.io.FileInputStream; 5 import java.io.InputStream; 6 import java.util.List; 7 import java.util.Queue; 8 9 import tony.model.Mp3Info; 10 import tony.mp3player.service.PlayerService; 11 import android.app.Activity; 12 import android.content.Intent; 13 import android.os.Bundle; 14 import android.os.Environment; 15 import android.os.Handler; 16 import android.view.View; 17 import android.view.View.OnClickListener; 18 import android.widget.ImageButton; 19 import android.widget.TextView; 20 21 public class PlayerActivity extends Activity { 22 23 24 private ImageButton beginBtn = null; 25 private ImageButton pauseBtn = null; 26 private ImageButton stopBtn = null; 27 28 private List<Queue> queues = null; 29 private TextView lrcTextView = null; 30 private Mp3Info info = null; 31 private Handler handler = new Handler(); 32 private UpdateTimeCallback updateTimeCallback = null; 33 private long begin = 0; 34 private long nextTimeMill = 0; 35 private long currentTimeMill = 0; 36 private String msg = null; 37 private long pauseTimeMills = 0; 38 private boolean isPlaying = false; 39 40 @Override 41 protected void onCreate(Bundle savedInstanceState) { 42 super.onCreate(savedInstanceState); 43 setContentView(R.layout.player); 44 Intent intent = getIntent(); 45 info = (Mp3Info) intent.getSerializableExtra("mp3Info"); 46 beginBtn = (ImageButton) findViewById(R.id.begin); 47 pauseBtn = (ImageButton) findViewById(R.id.pause); 48 stopBtn = (ImageButton) findViewById(R.id.stop); 49 lrcTextView = (TextView) findViewById(R.id.lrcText); 50 51 beginBtn.setOnClickListener(new BeginListener()); 52 pauseBtn.setOnClickListener(new PauseListener()); 53 stopBtn.setOnClickListener(new StopListener()); 54 } 55 56 /** 57 * 根据歌词文件的名字,来读取歌词文件当中的信息 58 * @param lrcName 59 */ 60 private void prepareLrc(String lrcName) { 61 try { 62 InputStream inputStream; 63 inputStream = new FileInputStream(Environment.getExternalStorageDirectory().getAbsolutePath() + 64 File.separator + "mp3" + File.separator + info.getLrcName()); 65 LrcProcessor lrcProcessor = new LrcProcessor(); 66 queues = lrcProcessor.process(inputStream); 67 updateTimeCallback = new UpdateTimeCallback(queues); 68 begin = 0; 69 currentTimeMill = 0; 70 nextTimeMill = 0; 71 } catch (Exception e) { 72 e.printStackTrace(); 73 } 74 } 75 76 class BeginListener implements OnClickListener { 77 @Override 78 public void onClick(View v) { 79 if(!isPlaying) { 80 //创建一个Intent对象,用于通知Service开始播放MP3 81 Intent intent = new Intent(); 82 intent.putExtra("mp3Info", info); 83 intent.putExtra("MSG", AppConstant.PlayerMsg.PLAY_MSG); 84 intent.setClass(PlayerActivity.this, PlayerService.class); 85 //读取LRC文件,放于startservice前,是为了防止歌曲已播放,但歌词没读完,造成不同步 86 prepareLrc(info.getLrcName()); 87 startService(intent); 88 begin = System.currentTimeMillis(); 89 handler = new Handler(); 90 handler.postDelayed(updateTimeCallback, 5);//5毫秒是试验得出的 91 isPlaying = true; 92 } 93 } 94 } 95 96 class PauseListener implements OnClickListener { 97 @Override 98 public void onClick(View v) { 99 //通知Service暂停播放MP3 100 Intent intent = new Intent(); 101 intent.putExtra("MSG", AppConstant.PlayerMsg.PAUSE_MSG); 102 intent.setClass(PlayerActivity.this, PlayerService.class); 103 startService(intent); 104 if(isPlaying) { 105 //不再更新歌词 106 handler.removeCallbacks(updateTimeCallback); 107 //用来下面代码计算暂停了多久 108 pauseTimeMills = System.currentTimeMillis(); 109 } else { 110 handler.postDelayed(updateTimeCallback, 5); 111 //因为下面的时间偏移是这样计算的offset = System.currentTimeMillis() - begin; 112 //所以要把暂停的时间加到begin里去, 113 begin = System.currentTimeMillis() - pauseTimeMills + begin; 114 } 115 isPlaying = !isPlaying; 116 } 117 } 118 119 class StopListener implements OnClickListener { 120 @Override 121 public void onClick(View v) { 122 //通知Service停止播放MP3文件 123 Intent intent = new Intent(); 124 intent.putExtra("MSG", AppConstant.PlayerMsg.STOP_MSG); 125 intent.setClass(PlayerActivity.this, PlayerService.class); 126 startService(intent); 127 //从Handler当中移除updateTimeCallback 128 handler.removeCallbacks(updateTimeCallback); 129 isPlaying = false; 130 } 131 } 132 133 class UpdateTimeCallback implements Runnable{ 134 Queue<Long> times = null; 135 Queue<String> msgs = null; 136 137 public UpdateTimeCallback(List<Queue> queues) { 138 this.times = queues.get(0); 139 this.msgs = queues.get(1); 140 } 141 142 @Override 143 public void run() { 144 //计算偏移量,也就是说从开始播放MP3到现在为止,共消耗了多少时间,以毫秒为单位 145 long offset = System.currentTimeMillis() - begin; 146 if(currentTimeMill == 0) {//刚开始播放时,调用prepareLrc(),在其中设置currentTimeMill=0 147 nextTimeMill = times.poll(); 148 msg = msgs.poll(); 149 } 150 //歌词的显示是如下:例如 151 //[00:01.00]Look 152 //[00:03.00]Up 153 //[00:06.00]Down 154 //则在第1~3秒间是显示“look”,在第3~6秒间是显示"Up",在第6秒到下一个时间点显示"Down" 155 if(offset >= nextTimeMill) { 156 lrcTextView.setText(msg); 157 msg = msgs.poll(); 158 nextTimeMill = times.poll(); 159 } 160 currentTimeMill = currentTimeMill + 100; 161 //在run方法里调用postDelayed,则会形成循环,每0.01秒执行一次线程 162 handler.postDelayed(updateTimeCallback, 100); 163 } 164 } 165 }
(2)PlayService.java
1 package tony.mp3player.service; 2 3 import java.io.File; 4 5 import tony.model.Mp3Info; 6 import tony.mp3player.AppConstant; 7 import android.app.Service; 8 import android.content.Intent; 9 import android.media.MediaPlayer; 10 import android.net.Uri; 11 import android.os.Environment; 12 import android.os.IBinder; 13 14 public class PlayerService extends Service { 15 16 private boolean isPlaying = false; 17 private boolean isPause = false; 18 private boolean isReleased = false; 19 private MediaPlayer mediaPlayer = null; 20 21 @Override 22 public IBinder onBind(Intent intent) { 23 return null; 24 } 25 26 @Override 27 public int onStartCommand(Intent intent, int flags, int startId) { 28 Mp3Info info = (Mp3Info) intent.getSerializableExtra("mp3Info"); 29 int MSG = intent.getIntExtra("MSG", 0); 30 if(info != null) { 31 if(MSG == AppConstant.PlayerMsg.PLAY_MSG) { 32 play(info); 33 } 34 } else { 35 if(MSG == AppConstant.PlayerMsg.PAUSE_MSG) { 36 pause(); 37 } 38 else if(MSG == AppConstant.PlayerMsg.STOP_MSG) { 39 stop(); 40 } 41 } 42 return super.onStartCommand(intent, flags, startId); 43 } 44 45 private void stop() { 46 if(mediaPlayer != null) { 47 if(isPlaying) { 48 if(!isReleased) { 49 mediaPlayer.stop(); 50 mediaPlayer.release(); 51 isReleased = true; 52 isPlaying = false; 53 } 54 } 55 } 56 } 57 58 private void pause() { 59 if(mediaPlayer != null) { 60 if(!isReleased){ 61 if(!isPause) { 62 mediaPlayer.pause(); 63 isPause = true; 64 } else { 65 mediaPlayer.start(); 66 isPause = false; 67 } 68 } 69 } 70 } 71 72 private void play(Mp3Info info) { 73 if(!isPlaying) { 74 String path = getMp3Path(info); 75 mediaPlayer = MediaPlayer.create(this, Uri.parse("file://" + path)); 76 mediaPlayer.setLooping(false); 77 mediaPlayer.start(); 78 isPlaying = true; 79 isReleased = false; 80 } 81 } 82 83 private String getMp3Path(Mp3Info mp3Info) { 84 String SDCardRoot = Environment.getExternalStorageDirectory() 85 .getAbsolutePath(); 86 String path = SDCardRoot + File.separator + "mp3" + File.separator 87 + mp3Info.getMp3Name(); 88 return path; 89 } 90 }
3.LrcProcessor.java
1 package tony.mp3player; 2 3 import java.io.BufferedReader; 4 import java.io.InputStream; 5 import java.io.InputStreamReader; 6 import java.util.ArrayList; 7 import java.util.LinkedList; 8 import java.util.Queue; 9 import java.util.regex.Matcher; 10 import java.util.regex.Pattern; 11 12 public class LrcProcessor { 13 14 public ArrayList<Queue> process(InputStream inputStream) { 15 Queue<Long> timeMills = new LinkedList<Long>(); 16 Queue<String> messages = new LinkedList<String>(); 17 ArrayList<Queue> queues = new ArrayList<Queue>(); 18 try { 19 InputStreamReader inputReader = new InputStreamReader(inputStream); 20 BufferedReader bufferReader = new BufferedReader(inputReader); 21 //创建一个正则表达式对象,寻找两边都带中括号的文本 22 Pattern p = Pattern.compile("\\[([^\\]]+)\\]"); 23 String temp = null; 24 String result = null; 25 while((temp = bufferReader.readLine()) != null) { 26 Matcher m = p.matcher(temp); 27 if(m.find()) { 28 if(result != null) {//正则第一次到时是,此时result还没值,到下一次循环时,就会把第一次计算出的result加到队列里 29 messages.add(result); 30 } 31 String timeStr = m.group(); 32 Long timeMill = time2Long(timeStr.substring(1, timeStr.length() - 1)); 33 timeMills.offer(timeMill);//和add相比,offer不会抛异常 34 //取出时间串后面的歌词,如[00:02.31]Lose Yourself,得到“Lose Yourself” 35 String msg = temp.substring(timeStr.length()); 36 result = "" + msg + "\n";//防止msg为null时抛nullpoint 37 } else { 38 result = result + temp + "\n"; 39 //比如歌词如下:则上面的if会得到result = a + "\n", 40 //而else里会使result = a + "\n" + b + "\n" + c + "\n" 41 //[00:32.42]a 42 //b 43 //c 44 } 45 } 46 messages.add(result);//把最后一次循环的result加到quenue里 47 queues.add(timeMills);//把时间队列和歌词队列都加到list里 48 queues.add(messages); 49 } catch (Exception e) { 50 e.printStackTrace(); 51 } 52 return queues; 53 } 54 55 private Long time2Long(String timeStr) { 56 //eg : 00:02.31 57 String [] s = timeStr.split(":"); 58 int min = Integer.parseInt(s[0]); 59 String ss[] = s[1].split("\\."); 60 int sec = Integer.parseInt(ss[0]); 61 int mill = Integer.parseInt(ss[1]); 62 return min * 60 * 1000 + sec * 1000 + mill * 10L; 63 } 64 65 }
4.AppConstant.java
1 package tony.mp3player; 2 3 public interface AppConstant { 4 5 public class PlayerMsg { 6 public static final int PLAY_MSG = 1; 7 public static final int PAUSE_MSG = 2; 8 public static final int STOP_MSG =3; 9 } 10 public class URL { 11 public static final String BASE_URL = "http://192.168.1.104:8080/mp3/"; 12 } 13 }