Android学习之路4——音乐播放器后台播放

Android学习之路4——音乐播放器后台播放

一、实验题目

  • 简单音乐播放器

二、实现内容

  • 实现一个简单的播放器,要求功能有:
    • 播放、暂停、停止、退出功能,按停止键会重置封面转角,进度条和播放按钮;按退出键将停止播放并退出程序。
    • 后台播放功能,按手机的返回键和home键都不会停止播放,而是转入后台进行播放。
    • 进度条显示播放进度、拖动进度条改变进度功能。
    • 播放时图片旋转,显示当前播放时间功能,圆形图片的实现使用的是一个开源控件CircleImageView
  • 加分项
    • 用户可以点击选歌按钮自己选择歌曲进行播放,要求换歌后不仅能正常实现上述的全部功能,还要求选歌成功后不自动播放,重置播放按钮,重置进度条,重置歌曲封面转动角度,最重要的一点:需要解析mp3文件,并更新封面图片。

三、实验结果:

  • 进入后的界面:
    Android学习之路4——音乐播放器后台播放_第1张图片

  • 点击播放后:

  • 点击file图标选择文件:
    Android学习之路4——音乐播放器后台播放_第2张图片

  • 选择歌曲后:

  • 拖动进度条播放:

  • 点击退出图标退出后再进入界面也能正确显示。

四、实验步骤及主要代码:

  • Step1:布局

    • 整体用的是线性布局,比较适合。

    • 音乐专辑图片用到圆形图片空间CircleImageView

      <de.hdodenhof.circleimageview.CircleImageView
              xmlns:app="http://schemas.android.com/apk/res-auto"
              android:id="@+id/image"
              android:layout_width="wrap_content"
              android:layout_height="0dp"
              android:src="@drawable/img"
              app:civ_border_color="#FF000000"
              android:layout_margin="20dp"
              android:layout_weight="10"
              android:layout_gravity="center"/>
      
    • 进度条用的是SeekBar:

      <SeekBar
                  android:id="@+id/seek_bar"
                  android:layout_width="0dp"
                  android:layout_height="wrap_content"
                  app:layout_constraintLeft_toRightOf="@id/start_time"
                  app:layout_constraintRight_toLeftOf="@id/end_time"
                  android:layout_marginHorizontal="2dp" />
      
    • 其它的就是布局的一些基本功了。

  • Step2:创建一个服务Service

    • File->New->Service->Service,通过这种方法创建AS会自动配置好,比如创建名叫MusicService的服务,在AndroidManifest.xml会生成以下代码:

      <service
                  android:name=".MusicService"
                  android:enabled="true"
                  android:exported="true">service>
      
    • 在MusicService新创一个MyBinder继承Binder用于在主活动和服务之间联系:

      	private IBinder myBinder = new MyBinder();
          public class MyBinder extends Binder{
              public MusicService getService(){
                  return MusicService.this;
              }
          }
          @Override
          public IBinder onBind(Intent intent) {
              return myBinder;
          }
      
    • 点击返回键活动会销毁,但如果没有指定销毁服务,服务还是不会销毁的,所以变量MediaPlayer要在服务MusicService类里面创建,并且要设为static,由此至终只有一个MediaPlayer实例,个人感觉MusicService可以设成单例模式,但查了关于音乐播放器的其它例子,没见到有用单例模式的,所以最后还是没采用了。把MediaPlayer的相关操作封装在MusicService里面:

          //MusicService.java
       	public static MediaPlayer mediaPlayer = null;
          private static String mediaPath;
          public MusicService(){
              //Log.d("MusicService","Gouzhao");
              if(mediaPlayer == null){
                  mediaPlayer= new MediaPlayer();
                  initMediaPlayer();
              }
          }
          //TODO :播放或暂停
          public boolean playOrPause(){
              Log.d("MusicService","playOrPause");
              if(!mediaPlayer.isPlaying()){
                  mediaPlayer.start();
      
                  return true;
              }else {
                  mediaPlayer.pause();
                  return false;
              }
          }
      
          //TODO :设置播放位置
          public void seekTo(int pos){
              mediaPlayer.seekTo(pos);
          }
      
          //TODO : 返回正在播放的音频的路径
          public String getMediaPath(){
              return mediaPath;
          }
      
          //TODO: 停止播放音乐并将播放位置设为0
          public void stop(){
              mediaPlayer.stop();
              try {
                  mediaPlayer.prepare();
              }catch (Exception e){
                  e.printStackTrace();
              }
      
              mediaPlayer.seekTo(0);
          }
          @Override
          public void onDestroy(){
              super.onDestroy();
              mediaPlayer.stop();
              mediaPlayer.release();
          }
      
          //TODO :初始化播放设置
          private void initMediaPlayer(){
              try{
                  //mediaPlayer.reset();
                  mediaPath = Environment.getExternalStorageDirectory()+"/data/山高水长.mp3";
                  mediaPlayer.setDataSource(mediaPath);
                  mediaPlayer.prepare();
              }catch (Exception e){
                  e.printStackTrace();
              }
          }
      
          //TODO :设置播放的音频路径
          public void setPlaySource(String path){
              try{
                  mediaPlayer.reset();
                  mediaPlayer.setDataSource(path);
                  Log.d("MusicService",path);
                  mediaPath = path;
                  mediaPlayer.prepare();
              }catch (Exception e){
                  e.printStackTrace();
              }
          }
      
  • Step3:圆形专辑图片的旋转

    • 有了上面的MusicService对MediaPlayer的封装,音乐的播放,暂停,停止应该不是问题了,说一下图片的旋转,介绍几个资料:ObjectAnimator实现图片旋转,官网Animatior, 官网ObjectAnimator.

      //TODO:用ObjectAnimator实现专辑图片的旋转.
              objectAnimator = ObjectAnimator.ofFloat(circleImageView,"rotation",0,360);
              objectAnimator.setDuration(5000);   //设置旋转一周的时间
              objectAnimator.setInterpolator(new LinearInterpolator());  //匀速旋转
              objectAnimator.setRepeatCount(Animation.INFINITE);         //设置重复旋转的次数
              objectAnimator.setRepeatMode(ValueAnimator.RESTART);       //设置重复模式
              objectAnimator.setCurrentPlayTime(MusicService.mediaPlayer.getCurrentPosition());//设置当前播放的时间
              if(MusicService.mediaPlayer.isPlaying()){
                  objectAnimator.start();
              }
      

      其中setCurrentPlayTime和下面的条件判断是为了按“返回”按钮后再次进入页面的时候根据播放进度重设一下旋转信息。

  • Step4:音乐的播放,暂停,停止以及以及相关的操作:

            //TODO:播放或暂停按钮的监听事件
            playImage.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Intent bindIntent = new Intent(MainActivity.this,MusicService.class);
                    startService(bindIntent);
                    //bindService(bindIntent,connection,BIND_AUTO_CREATE);
                    if(musicService.playOrPause()){
                        if(!objectAnimator.isStarted()){
                            objectAnimator.start();
                        }else{
                            objectAnimator.resume();
                        }
                        playImage.setImageResource(R.drawable.pause);
                    }else{
                        playImage.setImageResource(R.drawable.play);
                        objectAnimator.pause();
                    }
                }
            });
    
            //TODO:停止播放并重置UI界面相关信息
            stopImage.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    musicService.stop();
                    objectAnimator.end();
                    init();
                }
            });
    
  • Step5:步骤4中的init函数主要是在onCreate函数里也即刚进入主页面或者按“返回”键退出再次进入主页面后对seekBar,专辑图片,歌曲信息的正确显示。其中要用到MediaMedataRetriever解释mp3歌曲信息。

    	//TODO: 根据服务里的MeidaPlayer对象内容初始化活动的界面
        public void init(){
            seekBar.setMax(MusicService.mediaPlayer.getDuration());
            seekBar.setProgress(MusicService.mediaPlayer.getCurrentPosition());
            startTimeText.setText(time.format(MusicService.mediaPlayer.getCurrentPosition()));
            endTimeText.setText(time.format(MusicService.mediaPlayer.getDuration()));
    
            MediaMetadataRetriever mmr = new MediaMetadataRetriever();
            mmr.setDataSource(musicService.getMediaPath());
            nameText.setText(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE));  //歌名
            authorText.setText(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST)); //歌手名字
            byte[] pictureData = mmr.getEmbeddedPicture();
            if(pictureData != null){
                Bitmap bitmap = BitmapFactory.decodeByteArray(pictureData,0,pictureData.length);
                circleImageView.setImageBitmap(bitmap);
            }else{
                circleImageView.setImageResource(R.drawable.img);
            }
    
            mmr.release();//要记得release释放
        }
    
    
  • Step6:创建ServiceConnection实例并开启服务:

           //TODO:ServiceConnection实例,用于连接主活动和服务。
            connection = new ServiceConnection() {
                @Override
                public void onServiceConnected(ComponentName name, IBinder service) {
                    musicService = ((MusicService.MyBinder)service).getService();
                }
    
                @Override
                public void onServiceDisconnected(ComponentName name) {
                    musicService = null;
                }
            };
    

    开启服务用startService,我是在点击播放按钮里面开启服务的

    Intent bindIntent = new Intent(MainActivity.this,MusicService.class);
    startService(bindIntent);
    

    结束服务用stopService,在退出按钮点击事件里面实现:

    		//TODO:退出按钮,终止服务并结束进程
            backImage.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    musicService.stop();
                    Intent bindIntent = new Intent(MainActivity.this,MusicService.class);
                    stopService(bindIntent);
                    //unbindService(connection);
                    finish();
                    System.exit(0);
                }
            });
    

    用bindService实现后台播放好像不行,有资料说bindService后服务的生命周期是依赖于开启服务的活动的生命周期的,也就是说但点击“返回”按钮退出后,服务也结束了。

  • Step7:进度条的监听函数,很简单:

           //TODO:设置SeekBar的进度条变化监听函数
            seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
                @Override
                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                    if(fromUser){
                        musicService.seekTo(progress);
                    }
                }
                @Override
                public void onStartTrackingTouch(SeekBar seekBar) {
                }
                @Override
                public void onStopTrackingTouch(SeekBar seekBar) {
                }
            });
    
  • Step8:开启一个线程,当播放时对进度条和时间进行更新:

    	new Thread(new SeekBarThread()).start();
    	//TODO:开启一个线程,当播放音乐时更新进度条的进度,由于要涉及到UI的修改,所以用到Handler
        class SeekBarThread implements Runnable{
            @Override
            public void run(){
                while(musicService!=null){
                        Message message = new Message();
                        message.what = 0;
                        message.obj = MusicService.mediaPlayer.getCurrentPosition();
                        handler.sendMessage(message);
    
                    try{
                        Thread.sleep(800);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        }
    
        //TODO:Handler实例,对UI进行修改
        @SuppressLint("HandlerLeak")
        private Handler handler = new Handler(){
            @Override
            public void handleMessage(Message msg){
                super.handleMessage(msg);
                switch (msg.what){
                    case 0:
                        seekBar.setProgress((int)msg.obj);
                        startTimeText.setText(time.format(msg.obj));
                        break;
                    default:break;
                }
            }
        };
    
  • 加分项: 本地选择音频播放,和选择本地图片库的图片差不多,对于歌曲信息读取上面有介绍:

           //TODO:选择播放音频文件,要先申请权限
            fileImage.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
                        ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},RESULT_LOAD_AUDIO);
                    }
                    Intent i = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
                    startActivityForResult(i,RESULT_LOAD_AUDIO);
                }
            });
             @Override
        protected void onActivityResult(int requestCode,int resultCode,Intent data){
            super.onActivityResult(requestCode,resultCode,data);
    
            if(requestCode == RESULT_LOAD_AUDIO && resultCode == RESULT_OK && data != null){
                Uri selectAudioUri = data.getData();
                String[] filePathColumn = {MediaStore.Audio.Media.DATA};
                Cursor cursor = getContentResolver().query(selectAudioUri,filePathColumn,null,null,null);
                cursor.moveToLast();
                String audioPath = cursor.getString(cursor.getColumnIndex(filePathColumn[0]));
                cursor.close();
    
               // Log.d("MainActivity",audioPath);
                musicService.setPlaySource(audioPath);
                objectAnimator.end();  //停止图片旋转
                init(); //重设界面
            }
        }
    

五、实验遇到的困难以及解决思路

  • 1.觉得难点还是在怎么实现后台播放的功能

    查了不少资料,实现的方法与实现的程度也是参差不齐,最后还是自己总结了一下:

    • 首先MediaPlayer实例要放在服务的类里,这样当活动销毁时还能在后台播放
    • 并且MediaPlayer要声明为static,这样才能保证即使多次实例化服务的类也只有一个MediaPlayer实例,不然会出现MediaPlayer finalized without being released的错误。
    • 也理解和学习了MediaPlayer实例的各种状态,只有在对的状态下调用合适的方法才能正确执行,不然会java.lang.IllegalStateException at android.media.MediaPlayer的错误。
  • 2.另一个小难点是图片的旋转,用到ObjectAnimator来实现

查看的资料总结:

  • 实现简单播放器
  • startService与bindService的区别
  • MediaPlayer的状态图
  • ObjectAnimator实现图片转动
  • Animator官网
  • ObjectAnimator官网

我的代码on Github

  • 完成项目代码见我的Github

你可能感兴趣的:(Android学习之路)