【达内课程】音乐播放器v1.0(下)

增加播放进度条

思路: 修改页面布局,在控制按钮上增加 SeekBar 来展示进度,再增加两个 TextView 来分别展示歌曲播放时间和歌曲时长。

在播放方法 play() 中通过 MediaPlayer 的 getDuration() 方法获取歌曲时长,格式化后第二个 TextView 就能显示歌曲时长了。

随着歌曲播放,Seekbar 的进度条是不断变化的,所以 play() 方法中,开启一个线程来随着歌曲播放改变 Seekbar 进度。

给 Seekbar 增加监听事件,当拖动结束时,根据拖动进度和歌曲时长计算出拖动到的时间,继续播放。

其中还需要处理一些细节,包括如果从未播放过歌曲不允许拖拽进度条、当人为拖拽进度条时Seekbar不允许自动更新进度等。

关于 Seekbar 的使用可以查看:Android SeekBar:拖动条控件

先修改布局 activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<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{

        @Override
        public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
            //当进度发生改变时【不适用】
            // 第三个参数boolean fromUser,区分是程序代码改变了进度,还是人为改变了进度
            //在人为拖拽时,进度在不停的改变,但是不需要持续相应
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
            //当开始拖拽进度条时
            isTrackingTouch = true;
        }

        @Override
        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;
        }

        @Override
        public void run() {
            Runnable runnable = new Runnable() {
                @Override
                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() {
            @Override
            public void onCompletion(MediaPlayer mediaPlayer) {
                next();
            }
        });
    }

效果图:
在这里插入图片描述
到这里,音乐播放器1.0版本就做完了。

你可以添加自己的功能,比如随机播放,随机播放很容易实现,只需要把next()方法中的currentMusicIndex改为随机数即可。

优化

在学习完【达内课程】Activity 详解,关于生命周期的部分可以进行优化。

因为开启了子线程,即使把界面关掉了,主线程没有了,子线程依旧会运行,但是子线程已经没有运行的必要了,所以增加

@Override
    protected void onDestroy() {
        //停止更新进度的线程
        stopThread();
        //释放资源
        mediaPlayer.release();
        mediaPlayer = null;

        super.onDestroy();
    }

当回到桌面时,音乐在播放,但我们看不到界面,所以没有必要让子线程一直工作,所以增加

@Override
    protected void onPause() {
        //停止更新进度的线程
        stopThread();
        super.onPause();
    }

当再回到播放器界面时,需要恢复子线程

@Override
    protected void onRestart() {
        if (mediaPlayer.isPlaying()) {
            //开启更新进度的线程
            startThread();
        }
        super.onRestart();
    }

代码中的super语句不能删除,自己要实现的代码写在super语句之前。但是这样做有 1 个bug,播放音乐时,切换到桌面,音乐播放完切换到下一首,因为play()方法中有startThread()方法,所以线程又开启了。所以再定义一个值 isInBackground,当切回桌面时,值应该置为 true。

//当前activity是否在后台,解决在后台播放时,下一首开启线程的bug
private boolean isInBackground = false;

@Override
    protected void onStop() {
        //程序进入后台
        isInBackground = true;
        super.onStop();
    }

切回程序时应该置为 false,因为要在播放方法play()中进行判断,所以onRestart()也把之前的isPlaying的判断条件去掉。

@Override
    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();
        }
    }

源码下载

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值