Service: 二、简单的音乐播放器


前言

本篇以简单的音乐播放器为例, 练习 前台Service 的用法; 功能仅包括 暂停,播放及进度条; 本地音乐单曲循环.
从真正音乐播放器的角度看,本篇并不专业.

本篇涉及内容:

  • Service 的基本用法;
  • MediaPlayer 播放本地音乐
  • 通过 RemoteViews 自定义通知栏
  • 通过 bindService 及广播实现 Activity和通知栏 的双向UI更新;

一、先来张效果图


在这里插入图片描述


二、使用步骤

1.配置清单文件

<!-- SD卡读写权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> 
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

<!-- 8.0 以上 前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />


<application
	...
	<!-- 读取本地文件需要 -->
	 android:requestLegacyExternalStorage="true" >
	
	<!-- 自定义的音乐播放 Service -->
	<service android:name=".test.textservice.MusicService"/>

2.编写基础 Service

基础的播放器比较简单, 后面还要带上通知, 以及双向控制通知;

  1. 在 onCreate 回调中初始化 MediaPlayer;
  2. onBind() 返回自定义 Binder; 供Activity调用Service;
class MusicService: Service() {
    private lateinit var mediaPlayer: MediaPlayer
    private var remoteViews: RemoteViews? = null
    
	override fun onCreate() {
        super.onCreate()
        initMediaPlayer()	// 初始化 播放器
    }
    
    override fun onBind(intent: Intent?) = MusicBinder()	// 返回自定义 Binder

	override fun onDestroy() {
        super.onDestroy()
        mediaPlayer.stop()
        mediaPlayer.release()
    }

	/**
     * 初始化播放器
     */
	private fun initMediaPlayer() {	
        mediaPlayer = MediaPlayer()
        try {
            val file = File(
                Environment.getExternalStorageDirectory(),
                "My Songs Know What You Did In The Dark.mp3"
            )
            mediaPlayer.setDataSource(file.path)    //指定音频文件路径
            mediaPlayer.isLooping = true    // 循环播放
            mediaPlayer.prepare()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
	
	/**
     * 自定义 Binder; 将控制播放器的功能暴露给 Activity
     */
	inner class MusicBinder : Binder() {
        fun isPlaying() = mediaPlayer.isPlaying
        fun start() = mediaPlayer.start()	// 这里一会儿要改
        fun pause() = mediaPlayer.pause()	// 这里一会儿要改
        fun currentPosition() = mediaPlayer.currentPosition
        fun duration() = mediaPlayer.duration
    }
}

3.基础 Activity

其中 binding 对象是使用的 DataBinding 方式的页面引用; 替代了 fingViewById

private var mMusicService: MusicService.MusicBinder? = null
private var mConn: MyConnection? = null
private var job: Job? = null

// onCreate()
mConn = MyConnection()
bindService(Intent(this, MusicService::class.java), mConn!!, Context.BIND_AUTO_CREATE)

// 点击事件
fun onClick(v: View?) {
    when (v?.id) {
        R.id.iv_play -> {
            mMusicService?.let {	 // 根据当前状态, 切换播放或者暂停
                if (it.isPlaying()) {
                    it.pause()
                } else {
                    it.start()
                }
                switchMusicUi()
            }
        }
    }
}

// 根据播放状态, 切换页面 UI, 及定时更新播放时间和进度条
private fun switchMusicUi(){
   if(mMusicService?.isPlaying() == true){
        if(job == null){
            binding.ivPlay.setImageResource(R.mipmap.video_icon_suspend)
            startListener() //启动定时器, 更新页面播放进度
        }
    }else{
        binding.ivPlay.setImageResource(R.mipmap.video_icon_play)
        job?.cancel()   // 取消定时器
        job = null
    }
}

// 用协程 写简单的定时器; 每次执行 更新播放时间 及 进度条
private fun startListener() {
    job = lifecycleScope.launchWhenResumed {
        while (true) {
            fillUiProgress()
            delay(1000)
        }
    }
}

// 更新播放时间 及 进度条
private fun fillUiProgress (){
    val current = mMusicService!!.currentPosition() / 1000
    val duration = mMusicService!!.duration() / 1000
    val progress = if(duration == 0) 0 else current * 100 / duration
    binding.pbProgress.progress = progress
    binding.tvTime.text = "${formatTime(current)}/${formatTime(duration)}"
}

// format 时间
private fun formatTime(seconds: Int) = if (seconds < 60)
    "00:${jointZero(seconds)}"
else
    "${jointZero(seconds / 60)}:${jointZero(seconds % 60)}"

private fun jointZero(time: Int) = if (time < 10) "0$time" else time.toString()

inner class MyConnection : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
        mMusicService = service as MusicService.MusicBinder
        // 判断一下播放状态;  多页面监听 service 的情况下, 可能进页面前 就已经在播放中..
        if(mMusicService!!.isPlaying()){
            binding.ivPlay.setImageResource(R.mipmap.video_icon_suspend)
            startListener()
        }else{
            binding.ivPlay.setImageResource(R.mipmap.video_icon_play)
        }
        fillUiProgress()
    }

    override fun onServiceDisconnected(name: ComponentName?) {}
}

4.帖一下页面吧

页面比较简单, 背景图, 进度条, 控制icon, 播放时间

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingStart="30dp"
        tools:context=".test.textservice.TestServiceActivity">
        <androidx.cardview.widget.CardView
            android:id="@+id/cv_radio"
            android:layout_width="240dp"
            android:layout_height="120dp"
            android:layout_marginTop="20dp"
            app:cardCornerRadius="8dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="@id/tv_four">
            <androidx.constraintlayout.widget.ConstraintLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content">
                <ImageView
                    style="@style/img_wrap"
                    android:src="@mipmap/img_three"
                    app:layout_constraintTop_toTopOf="parent"
                    app:layout_constraintStart_toStartOf="parent"/>
                <ProgressBar
                    android:id="@+id/pb_progress"
                    style="?android:attr/progressBarStyleHorizontal"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="8dp"
                    android:layout_marginHorizontal="16dp"
                    android:max="100"
                    app:layout_constraintBottom_toTopOf="@id/iv_play"/>
                <TextView
                    android:id="@+id/tv_time"
                    style="@style/tv_base_14_gray"
                    android:textColor="@color/white"
                    android:text="00:00"
                    app:layout_constraintTop_toBottomOf="@id/pb_progress"
                    app:layout_constraintEnd_toEndOf="@id/pb_progress"/>
                <ImageView
                    android:id="@+id/iv_play"
                    style="@style/img_wrap"
                    android:layout_width="40dp"
                    android:layout_height="40dp"
                    android:layout_marginBottom="8dp"
                    android:src="@mipmap/video_icon_play"
                    android:onClick="onClick"
                    app:layout_constraintBottom_toBottomOf="parent"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"/>
            </androidx.constraintlayout.widget.ConstraintLayout>
        </androidx.cardview.widget.CardView>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

到现在为止, 简单的音乐播放 Service 已经实现了. 可以从 Activity 控制 Service 的播放状态;
接下来我们 加入通知, 广播, 双向控制


5.改造 Service

在 Service 中 加入或替换 以下代码:

private val TAG = "MusicService"
private val noticeId = 1
private val mScope = MainScope()	// 自定义协程作用域
private var job: Job? = null	// 协程 job
private var remoteViews: RemoteViews? = null	// 自定义通知栏样式

private val mNotificationManager by lazy {
    getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}

override fun onCreate() {
    super.onCreate()
    Log.d(TAG, "onCreate")
    initMediaPlayer()   // 初始化 播放器
	initNotification()	// 加入前台Service 的通知
}

// 接受 自定义通知栏中 icon 的点击事件
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    Log.d(TAG, "onStartCommand")
    intent?.let {
        if(it.getStringExtra("source") == "notice"){ // 来自通知栏的消息, 我们写死
            if(mediaPlayer.isPlaying){	// 因为只有一个按钮, 只是切换播放状态
                mPause()
            }else{
                mStart()
            }
            sendBroadcast()	// 当通知栏按钮点击时, 发送广播, 告知绑定的页面更新 UI
        }
    }
    return super.onStartCommand(intent, flags, startId)
}

override fun onDestroy() {
    super.onDestroy()
    mediaPlayer.stop()
    mediaPlayer.release()
    mScope.cancel()    // 取消协程定时器
    Log.d(TAG, "onDestroy")
    stopForeground(true)    // 取消通知
}

// 发送广播
private fun sendBroadcast(){
    val intent = Intent("包名.MusicAction")
    sendBroadcast(intent)
}

// 初始化 自定义的通知
private fun initNotification() {
    startForeground(noticeId, getNotification())
}

// 根据播放进度, 生成 Notification
private fun getNotification(progress: Int = 0): Notification{
    val channelId = "my_channel_music"

    if(remoteViews == null){
    	// 因为只有一首歌, 歌名, 图片都写死的;
        remoteViews = RemoteViews(packageName, R.layout.view_music_notice)
        remoteViews?.setTextViewText( R.id.tv_music_title,
            "My Songs Know What You Did In The Dark") //歌名
        remoteViews?.setImageViewResource(R.id.iv_music, R.mipmap.music)   // 图片
		
		// 点击通知栏 icon 的时候, 通知 Service 暂停和播放
        val intent = Intent(this, MusicService::class.java).putExtra("source", "notice")
        val pendingIntent = PendingIntent.getService(this, 0, intent,
            PendingIntent.FLAG_UPDATE_CURRENT
        )
        remoteViews?.setOnClickPendingIntent(R.id.iv_play, pendingIntent)
    }
    remoteViews?.setProgressBar(R.id.pb_progress, 100, progress, false)

    val playIcon = if(mediaPlayer.isPlaying) R.mipmap.video_icon_suspend else R.mipmap.video_icon_play
    remoteViews?.setImageViewResource(R.id.iv_play, playIcon)   // 按钮

    return NotificationCompat.Builder(this, channelId)
        .setSmallIcon(R.mipmap.ic_launcher)
        .setContent(remoteViews)
        .build()
}

// Binder 得改一下, 启动或暂停 播放的时候 要更新通知栏; 
inner class MusicBinder : Binder() {
    fun isPlaying() = mediaPlayer.isPlaying
    fun start() = mStart()
    fun pause() = mPause()
    fun currentPosition() = mediaPlayer.currentPosition
    fun duration() = mediaPlayer.duration
}

// 启动播放, 并启动定时器 定时更新通知栏
private fun mStart(){
    mediaPlayer.start()
    startTimer()
    mNotificationManager.notify(noticeId, getNotification(getProgress()))
}

// 暂停播放, 并取消定时器
private fun mPause(){
    mediaPlayer.pause()
    job?.cancel()
    job = null
    mNotificationManager.notify(noticeId, getNotification(getProgress()))
}

// 启动定时器, 定时更新通知栏的进度条
private fun startTimer(){
    job = mScope.launch {
        while (true){
            delay(2000)
            mNotificationManager.notify(noticeId, getNotification(getProgress()))
        }
    }
}

// 计算当前播放进度
private fun getProgress(): Int{
    val current = mediaPlayer.currentPosition / 1000
    val duration = mediaPlayer.duration / 1000
    return if(duration == 0) 0 else current * 100 / duration
}

6.通知栏布局:

注意: RemoteViews 支持的布局, 和 View部件 有限, 不了解的 自行度娘;

<?xml version="1.0" encoding="utf-8"?>
<!-- view_music_notice.xml -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#383838"
    android:padding="16dp">
    <ImageView
        android:id="@+id/iv_music"
        style="@style/img_wrap"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:src="@mipmap/music"
        android:layout_centerVertical="true"/>
    <TextView
        android:id="@+id/tv_music_title"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:textColor="@color/white"
        android:layout_marginHorizontal="12dp"
        android:ellipsize="end"
        android:lines="1"
        android:text="123213123123123123"
        android:start="@id/iv_music"
        android:layout_toEndOf="@id/iv_music"
        android:layout_toStartOf="@id/iv_play" />
    <ProgressBar
        android:id="@+id/pb_progress"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="2dp"
        android:max="100"
        android:layout_alignStart="@id/tv_music_title"
        android:layout_alignEnd="@id/tv_music_title"
        android:layout_below="@id/tv_music_title"/>
    <ImageView
        android:id="@+id/iv_play"
        style="@style/img_wrap"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:layout_marginEnd="8dp"
        android:src="@mipmap/video_icon_play"
        android:layout_alignParentEnd="true"
        android:layout_centerVertical="true"/>
</RelativeLayout>
在这里插入代码片

7.Activity 加入广播监听

private var mReceiver: MusicBroadcastReceiver? = null

// onCreate()
ContextCompat.startForegroundService(this, Intent(this, MusicService::class.java))
mConn = MyConnection()
bindService(Intent(this, MusicService::class.java), mConn!!, Context.BIND_AUTO_CREATE)

// 注册广播
mReceiver = MusicBroadcastReceiver()
val filter = IntentFilter()
filter.addAction("包名.MusicAction")
registerReceiver(mReceiver, filter)

override fun onDestroy() {
    super.onDestroy()
    mReceiver?.let { unregisterReceiver(it) }
}

// 自定义广播接收器, 动态注册;
inner class MusicBroadcastReceiver : BroadcastReceiver(){
    override fun onReceive(context: Context?, intent: Intent?) {
        switchMusicUi()
    }
}

至此, 简单的音乐播放 Service 完成 😀


总结

没有总结…

上一篇: Service: 一、简介,分类,生命周期, 简单用法
下一篇: Service: 三、小窗口(浮窗) 播放视频

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值