✏️
Android应用开发
  • 简介
  • 目录
  • 第一章 熟悉Android Studio开发环境
    • 第一个项目——Hello World!
    • 第二个项目——计数器
    • 第三个项目——新闻阅读器
  • 第二章 活动、组件及布局
    • 第四个项目——消息发送
    • 第五个项目——计数器2
    • 第六个项目——登陆界面
    • 第七个项目——新闻列表
  • 第三章 数据持久化
    • 第八个项目——登陆界面2
    • 第九个项目——新闻列表2
  • 第四章 内容提供器与数据共享
    • 第十个项目——音乐播放器1
  • 第五章 多媒体与服务
    • 第十一个项目——音乐播放器2
    • 第十二个项目——音乐播放器3
  • 第六章 绑定服务与自定义广播
    • 第十三个项目——音乐播放器4
  • 第七章 网络与多线程
    • 第十四个项目——新闻列表3
  • 附录 扩展项目
    • 第十五个项目——使用ViewHolder提升ListView显示性能
    • 第十六个项目——使用SwipeRefreshLayout实现下拉刷新
    • 第十七个项目——使用接口回调实现删除ListView列表中的Item数据
    • 第十八个项目——使用RecyclerView显示列表数据
    • 第十九个项目——使用Fragment组织UI页面
    • 第二十个项目——使用CursorAdapter进行数据绑定及渲染
    • 第二十一个项目——使用Loader实现异步数据加载操作
    • 第二十二个项目——使用TimerTask及Handler实现秒表计时器
    • 第二十三个项目——使用AsyncTask进行网络异步请求
Powered by GitBook
On this page
  • 实验目的
  • 实验要求
  • 实验内容
  • 步骤一,打开GGMusic项目
  • 步骤二,实现绑定服务的方法及对外暴露接口
  • 步骤三,在MainActivity中绑定MusicService服务
  • 步骤四,使用绑定服务
  • 步骤五,自定义MusicReceiver广播
  • 实验小结

Was this helpful?

  1. 第六章 绑定服务与自定义广播

第十三个项目——音乐播放器4

Previous第六章 绑定服务与自定义广播Next第七章 网络与多线程

Last updated 4 years ago

Was this helpful?

实验目的

  • 掌握绑定服务(Bound service)的定义方式及用法;

  • 掌握自定义广播及其用法;

  • 掌握线程间通信的方法;

实验要求

  • 使用绑定服务进行音乐播放控制;

  • 使用自定义广播发送音乐播放状态;

  • 使用Handler进行线程间消息通信;

实验内容

在第十二个Android项目的基础上,通过多线程及Handler线程间通信机制更新BottomNavigationView音乐播放进度状态,通过绑定服务形式为BottomNavigationView音乐播放控制栏增加音乐播放控制(暂停播放、继续播放)功能,项目最终运行效果如所示。

图1. 音乐播放器4运行效果

步骤一,打开GGMusic项目

打开第十二个Android项目GGMusic,在此项目的基础上完成本次实验的内容。

在第十二个项目中,在MusicService使用MediaPlayer播放音乐,音乐的控制操作被封装在MusicService中。此时存在两个问题,其一、MainActivity中无法对音乐播放状态(暂停、播放)进行控制。其二、无法获取到当前音乐播放时间进度。

对于问题一,可通过stopService()方法中的Intent对象增加播放控制标志的方式进行区分判断;对于问题二,则需要通过绑定服务(Bound service)的形式将当前MusicService服务实例及其接口(音乐状态接口)暴露给Activity等其他组件,以便其他组件进行查询获取。

综上在本项目中通过绑定服务的形式在MusicService中定义暴露给其他组件查询使用的接口。在此基础上,通过多线程及Handler线程间通信机制以及自定义广播等形式实现当前音乐播放时间进度的查询及在UI主界面中更新的操作。

步骤二,实现绑定服务的方法及对外暴露接口

首先需要明确,MusicService需对外暴露的信息,从而确定其需定义的接口:

  • 当前音乐播放进度;

  • 当前音乐总时长;

  • 当前MediaPlayer播放状态;

  • 控制MediaPlayer继续播放;

  • 控制MediaPlayer暂停播放;

上述5个信息及接口是其他组件(在本项目中是MainActivity)需要调用的查询或控制的接口。

Android系统中,组件除了通过startService()方法启动活动外,还可通过bindService()方法绑定服务,绑定服务后,组件可以获得一个IBinder实例,该实例通常向其他组件提供当前绑定服务对象实例的方法,从而使得其他组件可直接访问绑定服务对象的信息。如果在绑定服务中定义了公有的方法或属性,则这些公有方法或属性就可被其他组件访问。

在本项目中实现上述描述的功能,需要以下几个步骤:

  • 在MusicService中定义Binder子类MusicServiceBinder,并在该子类中定义返回MusicService实例的方法;

  • 重写MusicService中的onBind()方法,返回MusicServiceBinder类实例;

  • 在MusicSerice中定义公有方法作为暴露给其他组件使用的接口;

1. 在MusicService中定义MusicServiceBinder内部类

打开MusicService.java文件,在MusicService类中定义名为MusicServiceBinder的内部类,该类继承至Binder类。

在Java中,内部类可访问其外部类的实例对象及定义的成员及方法。因此在MusicServiceBinder类中,定义了名为getService的方法,该方法返回当前MusicService类实例,从而为其他组件提供了访问MusicService实例的可能,具体代码如下所示。

public class MusicService extends Service {

    private final IBinder mBinder = new MusicServiceBinder();

    ...

    public class MusicServiceBinder extends Binder {
        MusicService getService() {
            return MusicService.this;
        }
    }
    ...
}

2. 重写MusicService中的onBind()方法

实现绑定服务,需要重写服务中的onBind()方法,该方法范围一个IBinder接口,由于Binder实现了IBinder接口,因此在该方法中可直接返回在MusicService中定义的私有成员mBinder,具体代码如下所示。

public class MusicService extends Service {

    private final IBinder mBinder = new MusicServiceBinder();

    ...

    public class MusicServiceBinder extends Binder {
        MusicService getService() {
            return MusicService.this;
        }
    }
    ...


    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

3. 实现MusicService暴露给其他组件使用的接口

MainActivity等其他组件在调用bindService()方法时可获得Service的实例,因此将需暴露给这些组件使用的接口以公有方法的形式进行定义即可,如下代码中定义了5个方法:

  • pause(),暂停MediaPlayer音乐播放;

  • play(),继续MediaPlayer音乐播放;

  • getDuration(),获取当前播放音乐总时长;

  • getCurrentPosition(),获取当前音乐播放进度;

  • isPlaying(),获取MediaPlayer音乐播放状态;

public class MusicService extends Service {

    ...
      private final IBinder mBinder = new MusicServiceBinder();

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        return mBinder;
    }

    /** method for clients */
    public void pause() {
        if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
            mMediaPlayer.pause();
        }
    }

    public void play() {
        if (mMediaPlayer != null) {
            mMediaPlayer.start();
        }
    }

    public int getDuration() {
        int duration = 0;

        if (mMediaPlayer != null) {
            duration = mMediaPlayer.getDuration();
        }

        return duration;
    }

    public int getCurrentPosition() {
        int position = 0;

        if (mMediaPlayer != null) {
            position = mMediaPlayer.getCurrentPosition();
        }

        return position;
    }

    public boolean isPlaying() {

        if (mMediaPlayer != null) {
            return mMediaPlayer.isPlaying();
        }
        return false;
    }
}

步骤三,在MainActivity中绑定MusicService服务

1. 实现ServiceConnection接口

组件中通过bindService()方法进行服务绑定,该方法接收一个ServiceConnection接口对象作为参数,服务绑定及解绑时会回调该接口的onServiceConnected()及onServiceDisconnected()方法。

在MainActivity中定义ServiceConnection接口对象mConn,并通过匿名类形式对mConn进行赋值。在MainActivity中定义MusicService对象mService,并在ServiceConnection接口中的onServiceConnected()及onServiceDisconnected()方法中对mService进行初始化。

onServiceConnected()方法中第二个参数IBinder是由MusicService的onBind()方法返回的IBinder对象,由于我们在onBind()方法中返回的是MusicServiceBinder类实例,因此可以通过MusicServiceBinder类的getService方法获得MusicService实例,并将MusicService实例赋值给mService对象,从而确保在MainActivity中获得MusicService的对象引用,这样就可以在MainActivity中使用MusicService所暴露出来的几个接口(pause()、play()等),具体代码如下所示。

public class MainActivity extends AppCompatActivity 
        implements View.OnClickListener, ...{

    ...
    private MusicService mService;
    private boolean mBound = false;

    ...

    private ServiceConnection mConn = new ServiceConnection() {
        @Override
        public void onServiceConnected(
                 ComponentName componentName, IBinder iBinder) {
            MusicService.MusicServiceBinder binder =
                     (MusicService.MusicServiceBinder) iBinder;

            mService = binder.getService();
            mBound = true;
        }

        @Override
        public void onServiceDisconnected(
                 ComponentName componentName) {
            mService = null;
            mBound = false;
        }
    };
    ...
}

2. 绑定及解绑MusicService服务

绑定MusicService服务可在MainActivity活动的onStart()及onStop()方法中调用bindService()、unbindService()方法。 bindService()方法需要三个参数:

  • Intent,表示绑定服务的Intent对象,与startService()方法中的Intent对象相同;

  • ServiceConnection,表示服务连接接口,在绑定与解绑服务时,接口的相应方法被回调;

  • int,表示绑定服务的动作行为,在本例中使用BIND_AUTO_CREATE,表示自动创建服务;

public class MainActivity extends AppCompatActivity
         implements View.OnClickListener, ...{

    ...

    private MusicService mService;
    private boolean mBound = false;

    private ServiceConnection mConn = new ServiceConnection() {
       ...
    }

    ...

    @Override
    protected void onStart() {
        super.onStart();

        Intent intent = new Intent(MainActivity.this, 
                MusicService.class);
        bindService(intent, mConn, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onStop() {
        unbindService(mConn);
        mBound = false;

        super.onStop();
    }
  ...
}

步骤四,使用绑定服务

在MainActivity中绑定了MusicService服务后,就可引用绑定服务对象对绑定服务进行操作。 在本项目中,页面底部音乐控制栏中的暂停、继续播放按钮可以使用绑定服务的pause()、play()方法。 而音乐播放进度则需要更为复杂的代码逻辑来完成。

1. 调用MusicService的pause()、play()方法控制音乐的播放

在MainActivity中,使用View.OnClickListener接口实现音乐控制栏中的暂停、继续播放按钮事件,因此直接在 该接口回调中根据音乐播放状态调用绑定服务pause()、play()方法即可,代码如下所示。

public class MainActivity extends AppCompatActivity
            implements View.OnClickListener, ... {

    ...

    private Boolean mPlayStatus = true;
    private ImageView ivPlay;

    ...

   @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ...

        if (ivPlay != null) {
            ivPlay.setOnClickListener(MainActivity.this);
        }
        ...
    }

    ...

    @Override
    public void onClick(View view) {
        if (view.getId() == R.id.iv_play) {
            mPlayStatus = !mPlayStatus;
            if (mPlayStatus == true) {
                mService.play();
                ivPlay.setImageResource(
                    R.drawable.ic_pause_circle_outline_24dp);
            } else {
                mService.pause();
                ivPlay.setImageResource(
                    R.drawable.ic_play_circle_outline_24dp);
            }
        }
    }
    ...
}

2. 更新音乐播放进度状态

在bottom_media_toolbar.xml中,我们使用ProgressBar控件表示音乐播放进度,该控件有2个属性需要进行设置,一个是Max属性表示进度最大值,另外一个是Progress属性表示当前进度,这两个属性分别使用setMax()、setProgress()方法进行设置。

用ProgressBar控件表示音乐播放进度,Max属性可以对应当前MediaPlayer的getDuration()方法获取的音乐总时长(以毫秒为单位),而Progress属性则对应MediaPlayer的getCurrentPosition()方法获取的当前播放位置(以毫秒为单位),且这两个方法均通过MusicService同名的对外接口进行暴露,因此可在MainActivity活动中直接调用。

由于Android系统中,子线程无法操作UI控件,因此更新音乐播放进度状态的一个合理方案是:

  • 首先,在子线程中定时调用MusicService的getCurrentPosition()接口查询播放进度;

  • 其次,子线程将查询的播放进度数据传递给主线程;

  • 最后,主线程根据子线程传递的播放进度数据更新ProgressBar的Progress属性;

方案中子线程与主线程间的数据传递可以使用Handler机制实现。

2.1 定义Handler对象

首先在MainActivity中定义Handler类型的私有成员mHandler,在通过new关键字实例化其对象时,需重写其handleMessage(Message)方法。mHandler对象将在子线程中使用,并通过Handler.sendMessage(Message)方法将Message对象通过MessageQueue发送回主线程由mHandler对象的handleMessage(Message)方法进行处理,该方法的Message参数是由子线程发送而来,包含了当前音乐播放进度信息,从而实现子线程与主线程的通信,且handlerMessage(Message)是在主线程中,因而其可直接访问UI控件。

2.2 定义MusicProgressRunnable类

其次在MainActivity中定义私有的MusicProgressRunnable私有类,该类实现了Runnable接口,可在Thread线程类中进行运行。在本项目中之所以将MusicProgressRunnable定义为MainActivity的私有类,主要是简化项目功能实现工程,但这一做法耦合度过高,不利于项目维护。 MusicProgressRunnable类主要重写了Runnable.run()方法,在线程启动后,该方法中的代码将在线程中执行。该方法定时查询mService绑定的服务实例的getCurrentPosition()方法,获得当前音乐播放进度,并定于Message对象,将音乐播放进度赋值给Message.arg1,并通过mHandler对象发送该Message对象交由mHandler对象的handleMessage(Message)方法进行处理,具体代码如下所示。

需要注意的是,定义Message.what为UPDATE_PROGRESS用于区分Message的类别,UPDATE_PROGRESS在MainActivity中进行了定义。

public class MainActivity extends AppCompatActivity
            implements View.OnClickListener, ...{

    public static final int UPDATE_PROGRESS = 1;
    ...
    private ProgressBar pbProgress;
    ...
    private Handler mHandler = new Handler(Looper.getMainLooper()) {
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case UPDATE_PROGRESS:
                    int position = msg.arg1;
                    pbProgress.setProgress(position);
                    break;
                default:
                    break;
            }
        }
    };

    ...

   private class MusicProgressRunnable implements Runnable {
        public MusicProgressRunnable() {
        }

        @Override
        public void run() {
            boolean mThreadWorking = true;
            while (mThreadWorking) {
                try {
                    if (mService != null) {
                        int position = 
                                    mService.getCurrentPosition();
                        Message message = new Message();
                        message.what = UPDAE_PROGRESS;
                        message.arg1 = position;
                        mHandler.sendMessage(message);
                    }
                    mThreadWorking = mService.isPlaying();
                    Thread.sleep(100);
                } catch (InterruptedException ie) {
                    ie.printStackTrace();
                }
            }
        }
    }
    ...
}

步骤五,自定义MusicReceiver广播

在上一步中,定义了MusicProgressRunnable类,用于在子线程中读取MusicService当前音乐播放进度信息。因此还需要通过实例化Thread类并执行MusicProgressRunnable.run()方法,而何时执行该方法则是需要探讨的问题。

当用户点击ListView某一Item时,对应的OnItemClickListener.onClick (View)方法被执行,而在该方法中,我们仍然使用startForegroundService(Intent)方法启动前台服务执行onStartCommand (Intent, int, int)方法从而初始化MediaPlayer对象播放音乐,如果在该方法后通过Thread类执行MusicProgressRunnable.run()方法,理论上是可以获得当前音乐播放进度信息的。但是由于MediaPlayer.prepare()方法执行需要时间,此时MusicService所在线程以及MusicProgressRunnable线程并发执行,有可能出现MusicProgressRunnable中调用mService.isPlaying()方法时,MusicService中的MediaPlayer.prepare()方法仍未执行完成,从而出现mThreadWorking=false导致子线程直接退出的情况。

可通过自定义广播形式,实现Service所在线程与MusicProgressRunnable所在线程同步。

1. 自定义MusicReceiver广播

在MainActivity中定义MusicReceiver类,该类继承至Broadcast类。需要重写其onReceive(Context, Intent)方法。 MusicReceiver类用于监听音乐开始广播(对应ACTION_MUSIC_START动作),当音乐开始播放后,在MusicReceiver.onReceive(Contet, Intent)方法中开启MusicProgressRunnable线程执行查询音乐播放状态。 MusicService服务在MediaPlayer类完成prepare()、start()方法调用后发送该广播,从而实现MusicService所在线程与MusicProgressRunnable所在线程同步。

public class MainActivity extends AppCompatActivity
        implements View.OnClickListener,... {

    ...
    public static final String ACTION_MUSIC_START = 
            "com.glriverside.xgqin.ggmusic.ACTION_MUSIC_START";

    private MusicReceiver musicReceiver;

    ...

    public class MusicReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {

            if (mService != null) {
                pbProgress.setMax(mService.getDuration());

                new Thread(new MusicProgressRunnable()).start();
            }
        }
    }
    ...
}

2. 在MainActivity中注册MusicReceiver

定义好MusicReceiver后需要在MainActivity中通过registerReceiver (BroadcastReceiver, IntentFilter)方法注册MusicReceiver广播。

public class MainActivity extends AppCompatActivity
        implements View.OnClickListener,... {

    ...
    public static final String ACTION_MUSIC_START = 
            "com.glriverside.xgqin.ggmusic.ACTION_MUSIC_START";

    private MusicReceiver musicReceiver;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ...

        musicReceiver = new MusicReceiver();
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(ACTION_MUSIC_START);
        intentFilter.addAction(ACTION_MUSIC_STOP);
        registerReceiver(musicReceiver, intentFilter);

    }

    @Override
    protected void onDestroy() {

        unregisterReceiver(musicReceiver);
        super.onDestroy();
    }    
    ...   
}

3. 在MusicService中发送ACTION_MUSIC_START广播

当MusicService.onStartCommand(Intent, int, int)方法被调用时,MediaPlayer执行prepare()、start()方法后,通过sendBroadcast(Intent)方法发送ACTION_MUSIC_START广播。

public class MusicService extends Service {

    ...

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        ...

        if (mMediaPlayer != null) {
            try {
                ...

                mMediaPlayer.prepare();
                mMediaPlayer.start();

                Intent musicStartIntent = 
                        new Intent(MainActivity.ACTION_MUSIC_START);
                sendBroadcast(musicStartIntent);

            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
}

由此,MainActivity中启动MusicService播放音乐,MusicService发送ACTION_MUSIC_START广播,MainActivity接收该广播,启动MusicProgressRunnable子线程获取音乐播放进度,Handler处理子线程消息更新ProgressBar进度条显示播放进度。

实验小结

通过本次实验,你应该掌握了如下知识内容:

  • 使用Bound Service进行多媒体音乐播放;

  • 使用Thread、Runnable启动子线程;

  • 使用Handler进行线程间消息通信;

  • 使用BroadcastReceiver发送广播接收广播;

图1. 音乐播放器4运行效果