文章目录
前言
本篇以简单的音乐播放器为例, 练习 前台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
基础的播放器比较简单, 后面还要带上通知, 以及双向控制通知;
在 onCreate 回调中初始化 MediaPlayer;
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: 三、小窗口(浮窗) 播放视频