Android开发:服务与多线程--简单音乐播放器

实验内容:
1. 播放、暂停、停止、退出功能
2. 后台播放功能
3. 进度条显示播放进度、拖动进度条改变进度功能
4. 播放时图片旋转,显示当前播放时间功能

一. 参考网站

https://zhidao.baidu.com/question/1770591672437913940.html android开发 mp3文件放在哪个文件夹

http://blog.csdn.net/kjunchen/article/details/50429694 Android应用开发按下返回键退向后台运行

http://www.cnblogs.com/menlsh/archive/2013/06/07/3125341.html Android使用Handler实时更新UI

http://blog.csdn.net/hahawhyha/article/details/12783643 Error: start called in state 64

http://www.oschina.net/question/54100_32914 Android MediaPlayer 播放prepareAsync called in state 8解决办法

http://stackoverflow.com/questions/17054000/cannot-resolve-symbol-r-in-android-studio “cannot resolve symbol R” in Android Studio

二. 实验步骤

  1. 实现播放器主页面。主要控件有ImageView, TextView, SeekBar和Button。实现思路是整体页面是一个RelativeLayout, 音乐背景图ImageView和显示文件路径和当前状态的TextView是RelativeLayout下的控件。

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/cover"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="25dp"
        android:scaleType="center"/>
    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/path"
        android:textColor="@color/colorBlack"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="300dp" />
    
    <TextView
        android:id="@+id/state"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/initial"
        android:layout_marginTop="320dp"
        android:layout_marginLeft="10dp"
        android:textSize="20sp"
        android:textColor="@color/colorBlack"/>    

    拖动条SeekBar和两边显示时间的TextView放在主布局下的一个LinearLayout里面

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="360dp"
        android:layout_marginLeft="10dp"
        android:layout_marginRight="10dp"
        android:orientation="horizontal"
        android:gravity="center_vertical">
    
        <TextView
            android:id="@+id/currentTime"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/begin"
            android:textColor="@color/colorBlack"/>
    
        <SeekBar
            android:id="@+id/seekBar"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_marginLeft="2dp"
            android:layout_marginRight="2dp" />
    
        <TextView
            android:id="@+id/endTime"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/end"
            android:textColor="@color/colorBlack"/>
    
    LinearLayout>
    

    最下面的三个Button放在另一个LinearLayout下。

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="400dp"
        android:layout_centerHorizontal="true">
    
        <Button
            android:id="@+id/play"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/play"/>
    
        <Button
            android:id="@+id/stop"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/stop"/>
    
        <Button
            android:id="@+id/quit"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/quit"/>
    
    LinearLayout>
    
  2. 创建一个名为MusicService的Service,自动生成MusicService.java。并在AndroidManifest.xml中注册该service。

    <service
        android:name=".MusicService"
        android:enabled="true"
        android:exported="true">
    service>
    

    其中MusicService.java为继承Service的子类,一个公有成员MediaPlayer和一个私有成员用来记录状态,并通过一个Binder来保持Activity和Service的通信。

    // 这里设置为public属性,以便activity里面能直接获取
    public MediaPlayer mediaPlayer;
    // 记录当前状态
    private String curState = "";
    
    // 通过Binder来保持Activity和Service的通信
    public final IBinder binder = new MyBinder();
    public class MyBinder extends Binder {
        MusicService getService() {
            return MusicService.this;
        }
    }
    

    重写里面的方法具体如下:

    @Override
    public IBinder onBind(Intent intent) {  // 返回IBinder对象
        // TODO: Return the communication channel to the service.
        return binder;
    }
    
    @Override
    public void onCreate() {
        super.onCreate();
    }
    
    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mediaPlayer != null) {  // 停止并释放资源
            mediaPlayer.stop();
            mediaPlayer.release();
        }
    }
    

    play方法的具体实现思路是:判断当前mediaPlayer是否为空,若为空则表明还未创建,此时初始化mediaPlayer并设置其循环播放。否则就判断当前音乐是否正在播放,如果正在播放就暂停音乐,修改状态为“暂停”,否则当前状态是音乐没在播放,在这种情况下还要判断是否是点击stop造成的暂停,如果不是则直接调用start方法播放音乐,如果是则必须在调用start方法前调用prepare(),不然点击stop之后再点击play会报错。

    public void play() {
        if (mediaPlayer == null) {
            try {
                // 创建mediaPlayer
                mediaPlayer = MediaPlayer.create(this, R.raw.a);
                // 通过MediaPlayer.create方法创建,已经初始化,不需要prepare
                mediaPlayer.setLooping(true);  // 设置循环播放
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        if (mediaPlayer != null) {
            if (mediaPlayer.isPlaying()) {  // 当前状态是播放,点击按钮即暂停
                mediaPlayer.pause();
                curState = "pause";
            } else {
                try {
                    if (curState.equals("stop")) {  // 在点击stop之后点击播放
                        mediaPlayer.prepare();      // 这次就需要调用prepare()了
                        mediaPlayer.seekTo(0);      // 跳转到开始处
                    }
                    mediaPlayer.start();        // 还未开始播放,点击按钮即播放
                    curState = "play";
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public void stop() {
        if (mediaPlayer != null) {
            mediaPlayer.stop();
            curState = "stop";
        }
    }   
  3. 实现MainActivity.java

    a. 创建一个serviceConnection。其中onServiceConnected方法的输入参数IBinder就是Service中onBind返回的Binder对象。通过IBinder对象获取Service对象,就可以实现Activity和Service的绑定和它们的进程间通讯。

    private ServiceConnection serviceConnection = new ServiceConnection() {
        // bindService成功后回调onServiceConnected函数
        // 通过IBinder获取Service对象,实现Activity与Service的绑定
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            musicService = ((MusicService.MyBinder)service).getService();
        }
        // 解除绑定
        @Override
        public void onServiceDisconnected(ComponentName name) {
            musicService = null;
        }
    };
    
    

    b. 定义Hanlder来更新UI。因为在Android中,更新UI只可以在主线程中进行,所以想要在子线程中更新UI,就必须得子线程通知主线程,然后主线程来更新UI。这一过程正是通过Handler来实现的。这里子线程通过Runnable与Handler进行通信。这里,只需要实现Runnable接口,并重写该接口的run()方法。在run()方法中写入拖动条进度、播放时间等更新操作。

    // 定义handler来更新UI
    private Handler handler = new Handler();
    // 重写Runnable接口的run方法
    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            if (musicService != null && musicService.mediaPlayer != null) {
                // 更新当前播放时间
                currentTime.setText(time.format(musicService.mediaPlayer.getCurrentPosition()));
                // 更新结束时间
                endTime.setText(time.format(musicService.mediaPlayer.getDuration()));
                // 更新拖动条的当前进度
                seekBar.setProgress(musicService.mediaPlayer.getCurrentPosition());
                // 更新拖动条最大值
                seekBar.setMax(musicService.mediaPlayer.getDuration());
                // 若当前音乐在播放,更新图片的rotation,令其随着音乐播放旋转
                if (musicService.mediaPlayer.isPlaying()) {
                    imageView.setRotation((imageView.getRotation() + 1) % 360);
                }
                // 重复执行该方法,延迟时间为10ms
                handler.postDelayed(this, 10);
            }
        }
    };
    

    c. 重写onCreate方法:启动activity时就通过Context.bindService()方法启动service。然后通过findViewById方法获取UI界面的主要控件,并在按钮的点击监听函数里面实现音乐播放、暂停等基本逻辑操作。

    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    // connection: activity启动时绑定service
    Intent intent = new Intent(this, MusicService.class);
    // 通过Context.bindService方法启动service
    bindService(intent, serviceConnection, BIND_AUTO_CREATE);
    
    // find view
    playBtn = (Button)findViewById(R.id.play);
    stopBtn = (Button)findViewById(R.id.stop);
    quitBtn = (Button)findViewById(R.id.quit);
    seekBar = (SeekBar)findViewById(R.id.seekBar);
    state = (TextView)findViewById(R.id.state);
    currentTime = (TextView)findViewById(R.id.currentTime);
    endTime = (TextView)findViewById(R.id.endTime);
    imageView = (ImageView)findViewById(R.id.imageView);
    seekBar.setEnabled(false);
    

    点击播放按钮,调用service的play方法播放音乐。另外需要对状态进行分情况讨论,来更新界面UI。详情见下面代码解释。

    // 点击播放按钮
    playBtn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            seekBar.setEnabled(true);
            // 调用postDelayed方法更新UI
            handler.postDelayed(runnable, 10);
            // 获取点击按钮的文本
            String text = playBtn.getText().toString();
            if (text.equals("Play")) {  // 按钮显示play表明未播放
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        state.setText(R.string.playing);  // 状态变为Playing
                        playBtn.setText(R.string.pause);  // 按钮文本由play变pause
                    }
                });
            } else if (text.equals("Pause")) {  // 按钮显示pause表明正在播放
                handler.removeCallbacks(runnable);  // 暂停时不更新进度条和图片变化的UI
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        state.setText(R.string.pause);   // 状态变为pause
                        playBtn.setText(R.string.play);  // 按钮文本由pause变为play 
                    }
                });
            }
            // 调用service的play方法
            musicService.play();
        }
    });
    

    点击stop按钮,删除上面定义的runnable对象,使对象线程停止运行,并调用service的stop方法,使得音乐停止。在此之前要先判断service是否为空,避免出错。

      // 点击停止按钮
      stopBtn.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View v) {
              handler.removeCallbacks(runnable);
              if (musicService != null) {
                  handler.post(new Runnable() {
                      @Override
                      public void run() {
                          state.setText(R.string.stop);  // 状态变为stop
                          playBtn.setText(R.string.play);  // 按钮文本变为play
                      }
                  });
                  // 调用service的stop方法
                  musicService.stop();
              }
          }
      });
    

    点击退出按钮,解除与service的绑定,停止服务,并结束activity的生命周期。

        // 点击退出按钮
        quitBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 停止服务,解除绑定
                handler.removeCallbacks(runnable);
                unbindService(serviceConnection);
                try {
                    MainActivity.this.finish();
                    System.exit(0);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    
    

    重写onProgressChanged方法,判断进度条改变是否来自用户拖动的操作,如果是则更新当前播放时间和进度条的进度,并通过seekTo方法让音乐跳转到该进度。

    // 拖动进度条
    seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
        @Override
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            if (fromUser) {   // 判断是否来自用户
                currentTime.setText(time.format(progress));
                seekBar.setProgress(progress);
                musicService.mediaPlayer.seekTo(progress); // 跳转音乐
            }
        }
    
        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {}
    
        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {}
    });
    
    

    12月15日更新:上面的做法有问题,拖动进度条的时候,音乐也会跟着播放。较好的用户体验是拖动结束时才播放那个进度的音乐。修改如下:

    // 设置拖动条的监听器
    seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
        @Override
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            if (fromUser) {   // 判断是否来自用户
                currentTime.setText(time.format(progress));
            }
        }
    
        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
        }
    
        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
            int progress = seekBar.getProgress();
            musicService.mediaPlayer.seekTo(progress);
        }
    });
    

    这样的话就只有手动拖动进度条结束的时候才会播放音乐,但还是有一个小问题。如果手拖动后还没离开进度条,进度圆形图标会在当前播放进度和手动拖到的进度来回跳转。有点迷,时间关系,先这样。

三. 实验截图

  1. 主页面。

    Android开发:服务与多线程--简单音乐播放器_第1张图片

  2. 点击播放。

    Android开发:服务与多线程--简单音乐播放器_第2张图片

  3. 点击暂停。

    Android开发:服务与多线程--简单音乐播放器_第3张图片

  4. 点击停止。

    Android开发:服务与多线程--简单音乐播放器_第4张图片

  5. 拖动进度条,实现音乐跳转。

    Android开发:服务与多线程--简单音乐播放器_第5张图片

四. 实验中遇到的问题

  1. 一开始直接把KWill-Melt.Mp3文件复制粘贴在/res/raw文件夹下,报错如下:

    这里写图片描述

    由提示可知,文件名只能包含小写字母和数字,这里重名为a.mp3后就可以了。

  2. 在实验的过程中,遇到了如下图所示的报错:

    Android开发:服务与多线程--简单音乐播放器_第6张图片

    百度错误Cannot resolve symbol ‘R’ 得知解决方法有clean project and Sync Project with Gradle。

  3. 实验过程中遇到了报错:prepareAsync called in state 8。 查阅资料得知,这时因为当前mediaPlayer资源已被占用,而这时我又想尝试调用prepare()。但根据实验文档的提示,在start()之前要先调用prepare()方法。这时候就有点迷,后来查阅资料得知如果是通过new MediaPlayer()创建、setDataResource()来设置资源的话就要先调用prepare(),而如果是通过MediaPlayer.create()方法配置数据源的话就已经完成了初始化,在start()之前不需要调用prepare()。

  4. 点击stop按钮再点击play会报错,如下所示:

    这里写图片描述

    查阅资料得知是通过MediaPlayer.create()创建的话,第一次create的时候自动prepare,但是第二次需要自己调用prepare了。因此在play()方法里面进行判断是否点击过stop按钮就行了,如果点击过stop按钮则在start()之前prepare()。

  5. 在实现图片旋转的时候,一开始参照博客http://blog.csdn.net/dl10210950/article/details/52175873 里面的做法,通过Animation来实现图片的旋转。代码如下:

    // 图片匀速旋转
    final Animation operatingAnim = AnimationUtils.loadAnimation(this, R.anim.tip);
    LinearInterpolator lin = new LinearInterpolator();
    operatingAnim.setInterpolator(lin);
    

    看到图片旋转起来的时候很开心,但是这种方法似乎不能实现暂停图片的旋转后继续从当前的位置处继续旋转。于是只能另寻他路,最后通过setRotation()方法设置旋转度数来实现。通过getRotation()获取当前度数,并令其加1,再作为setRotation()的参数就可以实现从暂停处继续旋转了。

    if (musicService.mediaPlayer.isPlaying()) {
        imageView.setRotation((imageView.getRotation() + 1) % 360);
    }
  6. time.format()设置日期格式的时候,一开始不知道怎么用,以为要创建一个Date对象,在将其作为参数传进去,但通过getCurrentPosition方法获取此时进度返回值为int。尝试用下面方法实现

    int cur = musicService.mediaPlayer.getCurrentPosition() / 1000;
    currentTime.setText(cur/60 + ":" + cur % 60);
    

    但是该方法在本该显示00:00的时候却显示成0:0。最后发现time.format()传入的参数可以是一个Object,而int肯定是一个Object,直接作为其参数即可实现。

    currentTime.setText(time.format(musicService.mediaPlayer.getCurrentPosition()));
    

五. 思考与总结

  1. 本次实验难度一般,主要是学习MediaPlayer的使用、学习使用Hanlder更新UI、使用service与activity进行通信以及后台工作。通过这次实验,自己对MediaPlayer的使用和如何通过hanlder改变UI有了一定程度的掌握。

  2. 虽然有时候遇到问题会感到些许的迷茫,但是经过自己在搜索引擎上的一番努力之后找到问题解决的方法时,就会感觉很有成就感。这种感觉是直接问别人所不能体会到的。我想每一次实验都是在锻炼我们发现问题并解决问题的能力。

  3. 有待改善的地方还很多,努力前进的脚步不能停下来。

你可能感兴趣的:(android开发)