栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

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

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

文章目录
  • 前言
  • 一、先来张效果图
  • 二、使用步骤
    • 1.配置清单文件
    • 2.编写基础 Service
    • 3.基础 Activity
    • 4.帖一下页面吧
    • 5.改造 Service
    • 6.通知栏布局:
    • 7.Activity 加入广播监听
  • 总结


前言

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

本篇涉及内容:

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

一、先来张效果图


二、使用步骤 1.配置清单文件
 








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()
        }
    }
	
	
	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, 播放时间



    
        
            
                
                
                
                
            
        
    

到现在为止, 简单的音乐播放 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部件 有限, 不了解的 自行度娘;




    
    
    
    

在这里插入代码片

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: 三、小窗口(浮窗) 播放视频

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/458616.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号