文章目录
增加播放进度条
思路: 修改页面布局,在控制按钮上增加 SeekBar 来展示进度,再增加两个 TextView 来分别展示歌曲播放时间和歌曲时长。
在播放方法 play()
中通过 MediaPlayer 的 getDuration()
方法获取歌曲时长,格式化后第二个 TextView 就能显示歌曲时长了。
随着歌曲播放,Seekbar 的进度条是不断变化的,所以 play()
方法中,开启一个线程来随着歌曲播放改变 Seekbar 进度。
给 Seekbar 增加监听事件,当拖动结束时,根据拖动进度和歌曲时长计算出拖动到的时间,继续播放。
其中还需要处理一些细节,包括如果从未播放过歌曲不允许拖拽进度条、当人为拖拽进度条时Seekbar不允许自动更新进度等。
关于 Seekbar 的使用可以查看:Android SeekBar:拖动条控件
先修改布局 activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/ll_music_info" />
<LinearLayout
android:id="@+id/ll_music_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/rl_music_progress"
android:padding="10dp">
<TextView
android:id="@+id/tv_current_music_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="请选择播放歌曲" />
</LinearLayout>
<RelativeLayout
android:id="@+id/rl_music_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/ll_buttons"
android:padding="10dp">
<SeekBar
android:id="@+id/sk_music"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tv_music_current_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/sk_music"
android:text="00:00" />
<TextView
android:id="@+id/tv_music_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/sk_music"
android:layout_alignParentRight="true"
android:text="00:00" />
</RelativeLayout>
<LinearLayout
android:id="@+id/ll_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="center">
<ImageButton
android:id="@+id/ib_previous"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_media_previous" />
<ImageButton
android:id="@+id/ib_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_media_play"
android:text="Play" />
<ImageButton
android:id="@+id/ib_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_media_next"
android:text="Next" />
</LinearLayout>
</RelativeLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity {
......
private SeekBar seekBar;
private TextView tvCurrentTime;
private TextView tvDuration;
//是否正在拖拽进度条
private boolean isTrackingTouch;
//播放器是否在工作
private boolean isPlayerWorking;
......
private void initView() {
......
seekBar = findViewById(R.id.sk_music);
tvCurrentTime = findViewById(R.id.tv_music_current_time);
tvDuration = findViewById(R.id.tv_music_duration);
}
private void initListener() {
......
//为Seekbar添加监听
OnSeekBarChangeListener onSeekBarChangeListener = new OnSeekBarChangeListener();
seekBar.setOnSeekBarChangeListener(onSeekBarChangeListener);
}
......
//seekbar监听
private class OnSeekBarChangeListener implements SeekBar.OnSeekBarChangeListener{
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
//当进度发生改变时【不适用】
// 第三个参数boolean fromUser,区分是程序代码改变了进度,还是人为改变了进度
//在人为拖拽时,进度在不停的改变,但是不需要持续相应
}
public void onStartTrackingTouch(SeekBar seekBar) {
//当开始拖拽进度条时
isTrackingTouch = true;
}
public void onStopTrackingTouch(SeekBar seekBar) {
//当结束拖拽进度条时
playFromProgress(seekBar.getProgress());
isTrackingTouch = false;
}
}
//播放
private void play() {
try {
mediaPlayer.reset();
mediaPlayer.setDataSource(musics.get(currentMusicIndex).getPath());
mediaPlayer.prepare();
mediaPlayer.seekTo(pausePosition);
mediaPlayer.start();
ibPlay.setImageResource(android.R.drawable.ic_media_pause);
tvCurrentMusicTitle.setText("当前歌曲:" + musics.get(currentMusicIndex).getTitle());
//设置歌曲总时长,前提必须要有 setDataResource和prepare 方法的调用
int duration = mediaPlayer.getDuration();
//注意,不能直接写以下代码,因为setText传入int值以后,程序会寻找id是duration这个int值的资源,找不到就会报错
//tvCurrentTime.setText(duration);
tvDuration.setText(getFormattedTime(duration));
//开启更新进度的线程
startThread();
//只要放过歌,就修改isPlayerWorking,进度条允许拖拽
isPlayerWorking = true;
} catch (IOException e) {
e.printStackTrace();
}
}
......
//从指定进度开始放
private void playFromProgress(int progress){
//这里还需要解决一个bug,如果从来没有放过歌,那么拖拽进度条后
//由于一开始就需要mediaPlayer.getDuration()方法
//而getDuration()方法执行的前提条件是prepare()之后
//只有加载歌曲之后才能拿到歌曲的总时长
//因此,当没有播放歌曲,拖拽进度条后,会从第一首歌曲的0位置开始播放
//播放器是否已经工作,即是否可以拖拽进度条
if(isPlayerWorking){
//计算开始播放的时间点,并赋值给pausePosition,根据下面的式子算出pausePosition
//int percent = currentPosition * 100 /duration;
pausePosition = mediaPlayer.getDuration()*progress/100;
play();
}
}
//更新进度的线程
private class UpdateProgressThread extends Thread{
//线程的循环条件,一旦该值运行至false,则会导致整个线程运行结束
private boolean isRunning;
//设置线程是否循环
public void setRunning(boolean isRunning){
this.isRunning = isRunning;
}
public void run() {
Runnable runnable = new Runnable() {
public void run() {
//当前播放时间
int currentPosition = mediaPlayer.getCurrentPosition();
//歌曲时长
int duration = mediaPlayer.getDuration();
//播放到的百分比
int percent = currentPosition * 100 /duration;
//判断是否正在拖拽,仅当没有拖拽时更新进度条
//增加这个判断是因为,当拖拽进度条时,线程也在更新进度条,所以出现的bug是,人为拖拽住进度条,1s后,进度条会跳回原来位置
if(!isTrackingTouch){
//更新progressbar
seekBar.setProgress(percent);
}
//更新当前播放的时间的TextView
tvCurrentTime.setText(getFormattedTime(currentPosition));
}
};
//循环更新
while (true){
//更新进度
runOnUiThread(runnable);
//休眠
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//更新进度的线程
private UpdateProgressThread updateProgressThread;
//开启线程
private void startThread(){
if(updateProgressThread == null){
updateProgressThread = new UpdateProgressThread();
updateProgressThread.setRunning(true);
updateProgressThread.start();
}
}
//停止更新线程
private void stopThread(){
if(updateProgressThread != null){
updateProgressThread.setRunning(false);
//设置null,线程并不会停
updateProgressThread = null;
}
}
//格式化工具
SimpleDateFormat sdf = new SimpleDateFormat("mm:ss");
//格式化的时间对象
Date date = new Date();
//获取格式化后的时间字符串
private String getFormattedTime(long timeMillis){
//因为会被频繁调用,所以放到全局变量
/*SimpleDateFormat sdf = new SimpleDateFormat("mm:ss");
Date date = new Date();*/
date.setTime(timeMillis);
return sdf.format(date);
}
}
运行程序:
播放完歌曲自动下一首
思路: 为 MediaPlayer 添加监听器,监听播放器完成播放时的状态,在播放完的当前歌曲的时候,调用 next()
方法来播放下一首
private void initListener() {
......
//为mediaPlayer添加监听器
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
public void onCompletion(MediaPlayer mediaPlayer) {
next();
}
});
}
效果图:
到这里,音乐播放器1.0版本就做完了。
你可以添加自己的功能,比如随机播放,随机播放很容易实现,只需要把next()
方法中的currentMusicIndex
改为随机数即可。
优化
在学习完【达内课程】Activity 详解,关于生命周期的部分可以进行优化。
因为开启了子线程,即使把界面关掉了,主线程没有了,子线程依旧会运行,但是子线程已经没有运行的必要了,所以增加
protected void onDestroy() {
//停止更新进度的线程
stopThread();
//释放资源
mediaPlayer.release();
mediaPlayer = null;
super.onDestroy();
}
当回到桌面时,音乐在播放,但我们看不到界面,所以没有必要让子线程一直工作,所以增加
protected void onPause() {
//停止更新进度的线程
stopThread();
super.onPause();
}
当再回到播放器界面时,需要恢复子线程
protected void onRestart() {
if (mediaPlayer.isPlaying()) {
//开启更新进度的线程
startThread();
}
super.onRestart();
}
代码中的super
语句不能删除,自己要实现的代码写在super
语句之前。但是这样做有 1 个bug,播放音乐时,切换到桌面,音乐播放完切换到下一首,因为play()
方法中有startThread()
方法,所以线程又开启了。所以再定义一个值 isInBackground
,当切回桌面时,值应该置为 true。
//当前activity是否在后台,解决在后台播放时,下一首开启线程的bug
private boolean isInBackground = false;
protected void onStop() {
//程序进入后台
isInBackground = true;
super.onStop();
}
切回程序时应该置为 false,因为要在播放方法play()
中进行判断,所以onRestart()
也把之前的isPlaying
的判断条件去掉。
protected void onRestart() {
isInBackground = false;
//开启更新进度的线程
startThread();
super.onRestart();
}
然后在开启线程的方法中增加这个值的判断
//开启线程
private void startThread() {
if (updateProgressThread == null && !isInBackground && mediaPlayer.isPlaying()) {
updateProgressThread = new UpdateProgressThread();
updateProgressThread.setRunning(true);
updateProgressThread.start();
}
}
源码下载