本地音乐播放器——播放界面和服务的通信

Posted xingxing_yan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了本地音乐播放器——播放界面和服务的通信相关的知识,希望对你有一定的参考价值。

这一篇是整个项目的重点,讲解界面如果通过按钮来控制服务中音乐的播放状态。
首先,来分析界面上哪些按钮的操作都是跟服务有关的:
播放/暂停, 上一首,下一首,切换播放模式以及进度的快进快退。来看MainActivity中的onClick方法中这个按钮触发的动作:

    @Override
    public void onClick(View v) 
        switch (v.getId()) 
            case R.id.main_music_list:
                Intent intent = new Intent(MainActivity.this, MusicTypeActivity.class);
                MainActivity.this.startActivity(intent);
                break;
            case R.id.main_play_mode:   //切换播放模式
                sendBroadcastToServer(Contants.UPDATE_MUSCI_PLAY_MODE);
                break;
            case R.id.main_music_next:  //下一首
                sendBroadcastToServer(Contants.PLAY_NEXT);
                break;
            case R.id.main_music_play:  //播放暂停
                sendBroadcastToServer(Contants.UPDATE_MUSIC_STATE);
                break;
            case R.id.main_music_previous:  //上一首
                sendBroadcastToServer(Contants.PLAY_PREVIOUS);
                break;
        
    

很明显,当按钮按下时,只是发送了一个广播,告诉服务器当前触发的是什么动作,并没有额外的其他数据,我们逐个分析每一个按钮的功能。

一. 音乐的播放暂停功能:
触发音乐播放的入口都有哪些呢?当从列表选择要播放的音乐时,当点击播放按钮时,当点击下一首按钮时,当点击上一首按钮时,一共有四个入口,我们先看前两个。
(1) 当从列表选择播放音乐时:
从列表选择时,会将当前音乐在集合中的position发送给MainActivity,而MainActivity再通过广播发送给服务,服务端在接收器中作如下处理:

 case Contants.REPLAY:
    mMusicPlayPos = 0;  //重新播放时,播放位置置0
    Bundle bun = intent.getExtras();
    mMusicPosition = bun.getInt(Contants.MUSIC_POS, 0);
    replay();
    break;
 /**
     * 重新播放
     */
    private void replay() 
        if (mMusicPosition >= 0 && mMusicPosition < mMusicFiles.size()) 
            mPlayingMusic = mMusicFiles.get(mMusicPosition);
            mMediaPlayer.reset();
            try 
                mMediaPlayer.setDataSource(mPlayingMusic.getUrl());
                mMediaPlayer.prepare();
                mMediaPlayer.start();
                mMusicState = Contants.PLAY;
                mMediaPlayer.seekTo(mMusicPlayPos);

                MusicInfo.setMusicPosition(MusicServer.this, mMusicPosition);

                sendBroadcastToActivity();
             catch (IOException e) 
                e.printStackTrace();
            
         else 
            mMusicState = Contants.STOP;
            sendBroadcastToActivity();
        

    

先将播放位置置0,然后将传递过来的position赋给mMusicPosition,然后调用replay方法重新播放音乐。
在replay方法中,先根据mMusicPosition获取当前音乐mPlayingMusic,然后在重新设置MediaPlayer播放音乐,且需要将当前的音乐状态mMusicState设置为播放。将mMusicPosition存入缓存中,并给界面发送广播更新界面信息。

(2)点击播放暂停按钮:
当点击播放暂停按钮时,服务端在onReceive中的处理:

 case Contants.UPDATE_MUSIC_STATE:
    updateMusicState();
    break;
 /**
     * 更新音乐状态
     */
    private void updateMusicState() 
     switch (mMusicState) 
                case Contants.PLAY:
                    mMediaPlayer.pause();
                    mMusicState = Contants.PAUSE;
                    sendBroadcastToActivity();
                    break;
                case Contants.PAUSE:
                    mMediaPlayer.start();
                    mMusicState = Contants.PLAY;
                    sendBroadcastToActivity();
                    break;
                case Contants.STOP:
                    replay();
                    break;
            
    

如果mMusicState是停止状态,则重新播放,如果是播放状态,则暂停播放, 如果时暂停状态,就播放音乐,最后需要发送广播更新界面。

二. 上一首,下一首功能:
(1)当点击上一首按钮时,服务在onReceive中的处理:

case Contants.PLAY_PREVIOUS:
   mMusicPlayPos = 0;
   playPrevious();
   break;
  /**
     * 播放上一首
     */
    private void playPrevious() 
        mMusicPosition--;
        if (mMusicPosition < 0) 
            mMusicPosition = mMusicFiles.size() - 1;
        
        replay();
    

可以看到,这次的处理逻辑非常简单,首先我们将音乐的播放位置置0,然后在playPrevious方法中将mMusicPosition–,如果小于0,就让回到集合中最后一个位置,然后重新播放音乐。

(2)当点击下一首按钮时,服务中onReceive的处理:

 case Contants.PLAY_NEXT:
    mMusicPlayPos = 0;
    playNext();
    break;
 /**
     * 播放下一首
     */
    private void playNext() 
        mMusicPosition++;
        if (mMusicPosition >= mMusicFiles.size()) 
            mMusicPosition = 0;
        
        replay();
    

播放下一首的逻辑同样很简单,只需将mMusicPosition++,判断其值得范围是否合法,如果超出集合,则从第一个位置重新开始,然后调用replay重新播放音乐。

三. 切换播放模式:
在服务onReceive中的处理:

case Contants.UPDATE_MUSCI_PLAY_MODE:
      updateMusicMode();
      break;
    /**
     * 更新播放模式
     */
    private void updateMusicMode() 
        switch (mMusicMode) 
            case Contants.MODE_MUSIC_LIST_ORDER:    //顺序播放
                mMusicMode = Contants.MODE_MUSIC_RANDOM;
                break;
            case Contants.MODE_MUSIC_LOOP:  //单曲循环
                mMusicMode = Contants.MODE_MUSIC_LIST_ORDER;
                break;
            case Contants.MODE_MUSIC_LIST_LOOP:     //列表循环
                mMusicMode = Contants.MODE_MUSIC_LOOP;
                break;
            case Contants.MODE_MUSIC_RANDOM:    //随机播放
                mMusicMode = Contants.MODE_MUSIC_LIST_LOOP;
                break;
        
        MusicInfo.setMusicMode(MusicServer.this, mMusicMode);
        sendBroadcastToActivity();
    

播放模式一共有四种,列表顺序->随机播放->列表循环->单曲播放,从当前的播放模式切到下一个播放模式,并且给mMusicMode重新赋值。最后将当前播放模式存入缓存中,发送广播更新界面。

四. 音乐自动播放完成时:
还记得我们在将初始时服务的时候,有一个setListener方法吗?这个里边就是对音乐自动播放完成的监听:

private void setListener() 
        mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() 
            @Override
            public void onCompletion(MediaPlayer mp) 
                mMusicState = Contants.STOP;
                    mMusicPlayPos = 0;  //自动播放下一首时,播放位置置0
                autoPlayNext();
            
        );
    
    /**
     * 自动播放下一首
     */
    private void autoPlayNext() 
        switch (mMusicMode) 
            case Contants.MODE_MUSIC_LIST_ORDER:    //顺序播放
                if (mMusicPosition == mMusicFiles.size() - 1) 
                    sendBroadcastToActivity();
                 else 
                    playNext();
                
                break;
            case Contants.MODE_MUSIC_LOOP:  //单曲循环
                replay();
                break;
            case Contants.MODE_MUSIC_LIST_LOOP:     //列表循环
                playNext();
                break;
            case Contants.MODE_MUSIC_RANDOM:    //随机播放
                Random rand = new Random();
                int max = mMusicFiles.size() - 1;
                mMusicPosition = rand.nextInt(max);
                replay();
                break;
        
    

为mMediaPlayer设置音乐播放完成的监听,将mMusicPlayPos 置0,然后调用autoPlayNext方法。自动播放下一首和当前的播放模式是有关系的。如果当前是顺序播放,则最后一首音乐播放完成后,就停止播放;如果播放模式为随机播放,则在0~集合大小-1之间产生一个随机值,赋给mMusicPosition,并播放这首音乐;如果播放模式为列表循环,则最后一首播放完后再次播放第一首;如果播放模式为单曲循环,则继续播放当前音乐。

四. 音乐进度条的更新以及快进快退功能:
android系统有自带的更新音乐播放的控件——SeekBar。当然,我们在使用时需要重新设置seekbar的样式,因为自带的还是样式还是比较丑陋的。
(1) seekbar的样式更改:
使用seekbar的thumb属性来设置滑块的图片,使用progressDrawable设置背景样式和滑过后的样式:

 <SeekBar
     android:id="@+id/main_music_seekbar"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:layout_below="@+id/main_music_total_time"
     android:layout_gravity="bottom"
     android:maxHeight="2dp"
     android:minHeight="2dp"
     android:progressDrawable="@drawable/seekbar_style"
     android:thumb="@mipmap/seekbar_thumb_icon" />

seekbar_style:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@android:id/background">
        <shape>
            <corners android:radius="5dip" />
            <gradient
                android:startColor="#ff9d9e9d"
                android:centerColor="#ff9d9e9d"
                android:centerY="0.2"
                android:endColor="#ff9d9e9d"
                android:angle="270"/>
        </shape>
    </item>
    <item android:id="@android:id/secondaryProgress">
        <clip>
            <shape>
                <corners android:radius="5dip" />
                <gradient
                    android:startColor="#80ffd300"
                    android:centerColor="#80ffb600"
                    android:centerY="0.2"
                    android:endColor="#a0ffcb00"
                    android:angle="270"/>
            </shape>
        </clip>
    </item>
    <item android:id="@android:id/progress">
        <clip>
            <shape>
                <corners android:radius="5dip" />
                <gradient
                    android:startColor="#27BEC5"
                    android:centerColor="#27BEC5"
                    android:centerY="0.2"
                    android:endColor="#27BEC5"
                    android:angle="270"/>
            </shape>
        </clip>
    </item>
</layer-list>

这样基本上就设置了我们想要的效果,这里我们并不详细讲seekbar的样式,因为这不是我们的重点,不太懂得可以自行百度,网上有很多,效果图如下:

(2) seekbar的更新:
当音乐开始播放后,就会随着时间一直播放下去,而进度条也应该随着播放不停的更新,这点怎么实现?还记得在初始化MusicServer时初始化的定时器吗?它就是干这个用的。
首先,在服务中,我们需要写一个线程来不断的获取当前音乐的播放位置并发送广播更新界面。
在onCreate中初始化定时器:

 mTimer = new Timer();
 mTimer.schedule(new UpdateProgressTask(), 0);

更新进度条的线程

class UpdateProgressTask extends TimerTask 
        @Override
        public void run() 
            while (true) 
                try 
                    Thread.sleep(1000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
                if (isPlaying() && !isSeekbarChanged) 
                    mMusicPlayPos = mMediaPlayer.getCurrentPosition();
                    sendBroadcastToActivity();
                    MusicInfo.setMusicPlayPosition(MusicServer.this, mMusicPlayPos);
                
            

        
    

可以看到,在UpdateProgressTask 中,我们每隔1秒,获取一次播放位置,发送广播更新界面,并且将当前的播放位置存入缓存中。当然,条件是音乐正在播放并且滑块没有在手动滑动的过程中。如果滑块正在滑动,则不更新界面。那么,这个isSeekbarChanged的值是哪里来的呢?往下看。

(3) 音乐的快进快退:
通过手动滑动seekbar上的滑块,可以控制音乐的播放位置,已达到快进快退的效果,这个需要对seekbar的滑动做监听:

mMusicSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() 
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) 
                mPlayedTime.setText(Utils.formatToString(progress));
            

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) 
                isSeekbarChanged = true;
                Bundle bundle = new Bundle();
                bundle.putInt(Contants.MUSIC_PLAY_POS, -1);
                bundle.putBoolean(Contants.IS_CHANGED, isSeekbarChanged);
                sendBroadcastToServer(Contants.UPDATE_MUSIC_SEEKBAR, bundle);
            

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) 
                isSeekbarChanged = false;
                int currentPosition = seekBar.getProgress();
                Bundle bundle = new Bundle();
                bundle.putInt(Contants.MUSIC_PLAY_POS, currentPosition);
                bundle.putBoolean(Contants.IS_CHANGED, isSeekbarChanged);
                sendBroadcastToServer(Contants.UPDATE_MUSIC_SEEKBAR, bundle);
            
        );
    

首先来介绍一下这三个方法:onProgressChanged,此方法在进度条改变的时候会调用,onStartTrackingTouch方法是在滑块开始滑动的时候会调用,而onStopTrackingTouch方法在滑块停止滑动后会调用。
在onProgressChanged方法中,去处理当前已播放的时间的数据,而在onStartTrackingTouch方法中,将isSeekbarChanged置为true,并发送广播告诉服务暂时不要更新进度条。在onStopTrackingTouch方法中,又将isSeekbarChanged置为false,说明滑动结束了,将滑动的位置和此标志发送给服务,告诉服务可以更新进度条了,来看服务中onReceive中的处理:

 case Contants.UPDATE_MUSIC_SEEKBAR:
    Bundle bundle = intent.getExtras();
    mMusicPlayPos = bundle.getInt(Contants.MUSIC_PLAY_POS, -1);
    isSeekbarChanged = bundle.getBoolean(Contants.IS_CHANGED, false);
    if (mMusicPlayPos != -1) 
       mMediaPlayer.seekTo(mMusicPlayPos);
      
    break;

服务的接收器中获取界面传递过来的值,并使用MediaPlayer的seekTo方法将当前音乐定位到指定位置播放。这样,就达到了快进快退的效果。

五. 主界面信息的更新:
上边我们将的所有的都是界面给服务发送的广播以及服务的逻辑处理,但是服务给界面发送广播后,界面怎么处理的呢?我们可以看到,服务给界面发送的广播只是调用sendBroadcastToActivity()方法,其他的并没有管。之前我们所服务将要更新的界面信息都封装在一起,不论界面执行哪个动作,发送给界面的数据都是一样,所以,界面也会统一处理服务发过来的所有广播,MainActivity中的onReceive如下:

@Override
        public void onReceive(Context context, Intent intent) 
            Bundle bundle = intent.getExtras();
            int activitySignal = bundle.getInt(Contants.ACTIVITY_SIGNAL, -1);
            switch (activitySignal) 
                case Contants.UPDATE_PLAY_ACTIVITY:
                    mMusicState = bundle.getInt(Contants.MUSIC_STATE);
                    mPlayingMusic = (Music) bundle.getSerializable(Contants.CURRENT_MUSIC);
                    mMusicMode = bundle.getInt(Contants.MUSIC_MODE);
                    mMusicPlayPosition = bundle.getInt(Contants.MUSIC_PLAY_POS, 0);
                    updateMusicInfo(mMusicMode, mMusicPlayPosition);
                    break;
            
        

可以看到,我们获取了服务发过来的当前音乐状态mMusicState,当前播放的音乐mPlayingMusic, 当前播放模式mMusicMode,当前播放位置mMusicPlayPosition,然后调用updateMusicInfo方法更新界面。

    /**
     * 更新当前音乐的相关信息
     */
    @TargetApi(Build.VERSION_CODES.KITKAT)
    private void updateMusicInfo(int musicMode, int musicPlayPos) 
        mTvMusicName.setText(mPlayingMusic.getName());
        mTotalTime.setText(Utils.formatToString(mPlayingMusic.getTotalTime()));
        if (mMusicState == Contants.PLAY) 
            mMusicPlay.setImageResource(R.mipmap.img_music_pause);
            if (!mAnimator.isStarted())    //启动动画
                mAnimator.start();
            
            if (mAnimator.isPaused())  //如果动画暂停,播放动画
                mAnimator.resume();
            
         else 
            mMusicPlay.setImageResource(R.mipmap.img_music_play);
            if (mAnimator.isRunning()) //如果动画播放,暂停动画
                mAnimator.pause();
            
        
        updateMusicMode(musicMode);
        updateMusicSeekBar(musicPlayPos);
    

在此方法中,首先根据mPlayingMusic设置音乐名称和总时长。 然后根据mMusicState设置播放按钮的图片和旋转动画的播放/暂停。最后,调用updateMusicMode更新播放模式,调用updateMusicSeekBar更新播放进度条。updateMusicMode就是根据当前的播放模式更换图片而已,主要来看看updateMusicSeekBar代码:

 /**
     * 更新进度条
     */
    private void updateMusicSeekBar(int currentPosition) 
        mMusicSeekBar.setMax(mPlayingMusic.getTotalTime());
        mMusicSeekBar.setProgress(currentPosition);
        String playedTime = Utils.formatToString(currentPosition);
        mPlayedTime.setText(playedTime);
    

看起来也并不是很复杂,先将当前音乐的总时长设置为seekbar的最大值,保证播放位置和进度条1:1的关系。然后将当前的播放位置设给seekbar,最后更新已播放的时间。

至此,界面和服务基本的功能已经完成了,其实主要就是两者之间利用广播互相发送数据,然后处理数据。这一篇是最主要的,所有下的比较长,下一篇在将一些额外功能和做一下总结。

以上是关于本地音乐播放器——播放界面和服务的通信的主要内容,如果未能解决你的问题,请参考以下文章

本地音乐播放器——界面和服务的初始化

微信公众号文章音乐自动播放

iOS 简单音乐播放器 界面搭建

Linux项目:音乐播放器

Android初级教程进程间的通信AIDL

AIDL