音频播放器浮窗+通知栏播放器控制

编程入门 行业动态 更新时间:2024-10-20 07:58:52

音频<a href=https://www.elefans.com/category/jswz/34/1769718.html style=播放器浮窗+通知栏播放器控制"/>

音频播放器浮窗+通知栏播放器控制

一、最终效果如图

二、音频播放器浮窗实现

原理:

1、创建单例类FloatPlayer,内部创建浮窗播放器的布局,通过MediaPlayer去实现音频的播放。并暴露出开启、显示、隐藏、关闭浮窗播放器的方法供外部调用。

2、因为是通过Window.addView()、removeView()方法在每个页面去显示、隐藏浮窗播放器(这种方法优点:不用申请系统弹窗权限。缺点:每个页面都要处理。),需要在页面的基类BaseActivity里边的onResume()、onPause()方法里调用FloatPlayer的显示(判断是否启动了播放器,若启动则显示,否则不显示)、隐藏方法。

0、布局文件

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=""xmlns:app=""android:id="@+id/csRootFloatPlayer"android:layout_width="168dp"android:layout_height="48dp"><com.example.floatplayer.PlayerBgViewandroid:id="@+id/bgViewPlayer"android:layout_width="168dp"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"android:layout_height="48dp"/><com.google.android.material.imageview.ShapeableImageViewandroid:id="@+id/sivPlayerCover"android:layout_width="@dimen/player_icon_width"android:layout_height="0dp"android:layout_marginStart="8dp"android:scaleType="centerCrop"android:src="@mipmap/ic_player_cover"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintDimensionRatio="1"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:shapeAppearance="@style/imgStyleCircle" /><ImageViewandroid:id="@+id/ivPlayerControl"android:layout_width="@dimen/player_icon_width"android:layout_height="0dp"android:layout_marginStart="@dimen/player_icon_margin_start"android:contentDescription="@null"android:src="@drawable/ic_baseline_play_arrow_24"app:layout_constraintBottom_toBottomOf="@+id/sivPlayerCover"app:layout_constraintDimensionRatio="1"app:layout_constraintStart_toEndOf="@+id/sivPlayerCover"app:layout_constraintTop_toTopOf="@+id/sivPlayerCover"app:tint="@color/float_play_icon" /><ImageViewandroid:id="@+id/ivPlayerNext"android:layout_width="@dimen/player_icon_width"android:layout_height="0dp"android:layout_marginStart="@dimen/player_icon_margin_start"android:contentDescription="@null"android:src="@drawable/ic_baseline_skip_next_24"app:layout_constraintBottom_toBottomOf="@+id/sivPlayerCover"app:layout_constraintDimensionRatio="1"app:layout_constraintStart_toEndOf="@+id/ivPlayerControl"app:layout_constraintTop_toTopOf="@+id/sivPlayerCover"app:tint="@color/float_play_icon" /><ImageViewandroid:id="@+id/ivPlayerClose"android:layout_width="@dimen/player_icon_width"android:layout_height="@dimen/player_icon_width"android:layout_marginStart="@dimen/player_icon_margin_start"android:contentDescription="@null"android:src="@drawable/ic_baseline_close_24"app:layout_constraintBottom_toBottomOf="@+id/sivPlayerCover"app:layout_constraintStart_toEndOf="@+id/ivPlayerNext"app:layout_constraintTop_toTopOf="@+id/sivPlayerCover"app:tint="@color/float_play_icon" /></androidx.constraintlayout.widget.ConstraintLayout>

圆形封面图样式style:imgStyleCircle

<?xml version="1.0" encoding="utf-8"?>
<resources><!--ShapeImageView圆形图--><style name="imgStyleCircle"><item name="cornerFamily">rounded</item><item name="cornerSize">50%</item></style>
</resources>

1、FloatPlayer

import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.PixelFormat
import android.media.MediaPlayer
import android.os.Build
import android.transition.TransitionManager
import android.util.TypedValue
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import android.view.animation.LinearInterpolator
import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.app.NotificationCompat
import com.example.floatplayer.databinding.FloatPlayerViewBindingclass FloatPlayer private constructor() {//播放器是否活动private var isPlayerActive = false//悬浮窗是否正在显示private var isShowing = falseprivate lateinit var bindingFloatPlayer: FloatPlayerViewBindingprivate val mCsApply = ConstraintSet()private val mCsReset = ConstraintSet()private var mContext: Context? = null//播放状态(默认不播放)private var isPlaying = false//控件展开状态(默认展开)private var isExpansion = trueprivate lateinit var animCoverRotation: ObjectAnimatorprivate var mediaPlayer: MediaPlayer? = null//音乐列表private val mMusicList = arrayListOf(R.raw.shanghai, R.raw.withoutyou)private var mMusicPosition = 0var mPlayControlReceiver: PlayerActionBroadCastReceiver = PlayerActionBroadCastReceiver()private lateinit var mNotificationManager: NotificationManagercompanion object {const val notificationMediaId = 10010const val notificationChannelMedia = "MediaNotification"@Volatileprivate var instance: FloatPlayer? = nullfun getInstance() = instance ?: synchronized(this) {instance ?: FloatPlayer().also { instance = it }}}init {initNotificationManager()initView()}private fun initNotificationManager() {mNotificationManager = FloatApp.appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManagerif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {mNotificationManager.createNotificationChannel(NotificationChannel(notificationChannelMedia,"播放器", NotificationManager.IMPORTANCE_DEFAULT))}}//显示播放控件fun show(context: Context) {if (!isPlayerActive) returnmContext = contextinitMediaPlayer()bindingFloatPlayer.root.visibility = View.VISIBLEval windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManagerwindowManager.addView(bindingFloatPlayer.root, createLayoutParam(context))isShowing = true}fun dismiss() {if (!isShowing || mContext == null) returnbindingFloatPlayer.root.visibility = View.INVISIBLEval windowManger = mContext!!.getSystemService(Context.WINDOW_SERVICE) as WindowManagerwindowManger.removeView(bindingFloatPlayer.root)isShowing = falsemContext = null}//开启展示播放控件fun open(context: Context) {if (!isPlayerActive) {isPlayerActive = trueshow(context)}}//是否正在播放fun playing(): Boolean {return mediaPlayer?.isPlaying == true}//播放/暂停切换fun playSwitch() {if (mediaPlayer == null) returnbindingFloatPlayer.ivPlayerControl.performClick()}//切换下一首fun playNext() {if (hasNext()) {bindingFloatPlayer.ivPlayerNext.performClick()} else {showToast("没有更多了")}}//关闭播放控件fun close() {if (!isPlayerActive || mContext == null) returndestroyMediaPlayer()dismiss()isPlayerActive = false}//创建LayoutParamprivate fun createLayoutParam(context: Context): WindowManager.LayoutParams {val layoutParam = WindowManager.LayoutParams()layoutParam.width = WindowManager.LayoutParams.WRAP_CONTENTlayoutParam.height = WindowManager.LayoutParams.WRAP_CONTENT//弹窗层级layoutParam.type = WindowManager.LayoutParams.TYPE_APPLICATIONlayoutParam.gravity = Gravity.START or Gravity.BOTTOM//背景透明layoutParam.format = PixelFormat.TRANSPARENTlayoutParam.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLElayoutParam.x =TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24f,context.resources.displayMetrics).toInt()layoutParam.y =TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 80f,context.resources.displayMetrics).toInt()return layoutParam}//初始化控件private fun initView() {bindingFloatPlayer =FloatPlayerViewBinding.inflate(LayoutInflater.from(FloatApp.appContext))mCsApply.clone(bindingFloatPlayer.root)mCsReset.clone(bindingFloatPlayer.root)bindingFloatPlayer.sivPlayerCover.setOnClickListener {playExpansionStatusSwitch(!isExpansion)bindingFloatPlayer.bgViewPlayer.doAnimation()}bindingFloatPlayer.ivPlayerControl.setOnClickListener {playControlStatusSwitch(!isPlaying)updateNotification()}bindingFloatPlayer.ivPlayerNext.setOnClickListener {mediaPlayNext()}bindingFloatPlayer.ivPlayerClose.setOnClickListener {playControlStatusSwitch(false)cancelNotificationMedia()close()}initRotationAnimator(bindingFloatPlayer.sivPlayerCover)}//初始化音频播放器private fun initMediaPlayer() {if (mediaPlayer != null) returnmediaPlayer = MediaPlayer.create(FloatApp.appContext, mMusicList[0])mediaPlayer!!.setOnCompletionListener {mediaPlayNext()}mediaPlayer!!.setOnErrorListener { _, _, _ ->mediaPlayError()true}}//销毁MediaPlayerprivate fun destroyMediaPlayer() {mediaPlayer?.stop()mediaPlayer?.release()mediaPlayer = null}//是否还有下一首private fun hasNext(): Boolean {return mMusicList.isNotEmpty() && mMusicPosition < mMusicList.size - 1}//创建音频播放器private fun mediaPlayNext() {if (!hasNext()) {showToast("没有更多了")} else {mediaPlayer?.stop()mediaPlayer?.release()mMusicPosition++mediaPlayer = MediaPlayer.create(FloatApp.appContext,mMusicList[mMusicPosition])mediaPlayer!!.start()playControlStatusSwitch(true)updateNotification()}}private fun showToast(message: String) {if (mContext == null) returnToast.makeText(mContext, message, Toast.LENGTH_SHORT).show()}//开始播放音频private fun mediaPlayStart() {if (mediaPlayer?.isPlaying == true) returnmediaPlayer?.start()}//暂停播放音频private fun mediaPlayPause() {if (mediaPlayer?.isPlaying == true) {mediaPlayer?.pause()}}//播放出错private fun mediaPlayError() {showToast("播放出错")mediaPlayer?.reset()}//播放按钮状态控制private fun playControlStatusSwitch(startPlay: Boolean) {if (startPlay) {startCoverAnim()mediaPlayStart()} else {stopCoverAnim()mediaPlayPause()}bindingFloatPlayer.ivPlayerControl.setImageResource(if (startPlay) R.drawable.ic_baseline_pause_24else R.drawable.ic_baseline_play_arrow_24)isPlaying = startPlay}//初始化旋转动画private fun initRotationAnimator(target: View) {//顺时针animCoverRotation = ObjectAnimator.ofFloat(target, "rotation", 0f, 360f)//3s一圈animCoverRotation.duration = 6000animCoverRotation.repeatMode = ValueAnimator.RESTARTanimCoverRotation.repeatCount = ValueAnimator.INFINITEanimCoverRotation.interpolator = LinearInterpolator()}//开始播放private fun startCoverAnim() {if (animCoverRotation.isPaused) {animCoverRotation.resume()} else {animCoverRotation.start()}}//取消播放private fun stopCoverAnim() {animCoverRotation.pause()}/*** 封面点击切换状态* @param expansion 展开* */private fun playExpansionStatusSwitch(expansion: Boolean) {if (expansion == isExpansion) returnif (expansion) playViewExpansion() else playViewShrink()isExpansion = expansion}//展开播放控件private fun playViewExpansion() {TransitionManager.beginDelayedTransition(bindingFloatPlayer.root)mCsReset.applyTo(bindingFloatPlayer.root)}//收缩播放控件private fun playViewShrink() {TransitionManager.beginDelayedTransition(bindingFloatPlayer.root)mCsApply.setVisibility(R.id.ivPlayerClose, View.GONE)mCsApply.setVisibility(R.id.ivPlayerNext, View.GONE)mCsApply.setVisibility(R.id.ivPlayerControl, View.GONE)mCsApply.applyTo(bindingFloatPlayer.root)}//更新播放器通知UIprivate fun updateNotification() {val notificationCompatAction = NotificationCompat.Action.Builder(if (playing()) R.drawable.ic_baseline_pause_24else R.drawable.ic_baseline_play_arrow_24,"switch",PendingIntent.getBroadcast(FloatApp.appContext,111,Intent(PlayerActionBroadCastReceiver.actionSwitch),if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENTelse PendingIntent.FLAG_UPDATE_CURRENT)).build()val nextPendingIntent = PendingIntent.getBroadcast(FloatApp.appContext, 222,Intent(PlayerActionBroadCastReceiver.actionNext),if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENTelse PendingIntent.FLAG_UPDATE_CURRENT)val notification =NotificationCompat.Builder(FloatApp.appContext, notificationChannelMedia).setVisibility(NotificationCompat.VISIBILITY_PUBLIC).setSmallIcon(R.mipmap.ic_launcher).addAction(notificationCompatAction).addAction(R.drawable.ic_baseline_skip_next_24, "next", nextPendingIntent).setStyle(androidx.media.app.NotificationCompat.MediaStyle()).setContentTitle("这是标题").setContentText("这是内容这是内容").build()mNotificationManager.notify(notificationMediaId, notification)}//取消掉通知栏播放器private fun cancelNotificationMedia() {mNotificationManager.cancel(notificationMediaId)}
}

2、BaseActivity

import androidx.appcompat.app.AppCompatActivityopen class BaseActivity: AppCompatActivity() {override fun onResume() {super.onResume()FloatPlayer.getInstance().show(this)}override fun onPause() {super.onPause()FloatPlayer.getInstance().dismiss()}
}

三、通知栏播放器实现

原理:创建 NotificationCompat.MediaStyle() 样式的通知,需要引入依赖:

implementation 'androidx.media:media:1.3.0'

并且给通知添加Action,用来和页面的播放起实现操作的联动。

如何创建通知,如下:

//创建通知private fun createNotification() {val notificationCompatAction = NotificationCompat.Action.Builder(if (FloatPlayer.getInstance().playing()) R.drawable.ic_baseline_pause_24else R.drawable.ic_baseline_play_arrow_24,"switch",PendingIntent.getBroadcast(this,111,Intent(PlayerActionBroadCastReceiver.actionSwitch),if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENTelse PendingIntent.FLAG_UPDATE_CURRENT)).build()val nextPendingIntent = PendingIntent.getBroadcast(this, 222,Intent(PlayerActionBroadCastReceiver.actionNext),if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENTelse PendingIntent.FLAG_UPDATE_CURRENT)val notification = NotificationCompat.Builder(this,FloatPlayer.notificationChannelMedia).setVisibility(NotificationCompat.VISIBILITY_PUBLIC).setSmallIcon(R.mipmap.ic_launcher).addAction(notificationCompatAction).addAction(R.drawable.ic_baseline_skip_next_24, "next", nextPendingIntent).setStyle(androidx.media.app.NotificationCompat.MediaStyle()).setContentTitle("这是标题").setContentText("这是内容这是内容").setLargeIcon(BitmapFactory.decodeResource(resources, R.mipmap.ic_player_cover)).build()val notificationManager =getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManagerif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {notificationManager.createNotificationChannel(NotificationChannel(FloatPlayer.notificationChannelMedia,"播放器", NotificationManager.IMPORTANCE_DEFAULT))}notificationManager.notify(FloatPlayer.notificationMediaId, notification)}

四、通知栏播放器和浮窗播放器联动实现

实现原理:

1、播放器的操作通过更新同一个id的通知,去更新通知播放器的UI显示。

2、定义广播接收器:PlayerActionBroadCastReceiver,接收到对应操作的广播之后FloatPlayer相应操作,通知栏播放器的操作,在上边(三)中定义的Action当中的PendingIntent发送对应操作的广播。

广播接收器 PlayerActionBroadCastReceiver 如下:

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent/*** 通知栏播放控制按钮通知播放器操作* */
class PlayerActionBroadCastReceiver : BroadcastReceiver() {companion object {const val actionSwitch = "floatPlayer.switch"const val actionNext = "floatPlayer.next"}override fun onReceive(context: Context, intent: Intent) {when (intent.action) {actionSwitch -> {FloatPlayer.getInstance().playSwitch()}actionNext -> {FloatPlayer.getInstance().playNext()}}}
}

 页面注册广播

//注册控制接受receiverprivate fun registerPlayControlReceiver() {val intentFilter = IntentFilter()intentFilter.addAction(PlayerActionBroadCastReceiver.actionSwitch)intentFilter.addAction(PlayerActionBroadCastReceiver.actionNext)registerReceiver(FloatPlayer.getInstance().mPlayControlReceiver, intentFilter)}

页面取消注册广播

//取消注册receiverprivate fun cancelPlayControlReceiver() {unregisterReceiver(FloatPlayer.getInstance().mPlayControlReceiver)}

最后

播放器动画背景View,因为 ConstraintSet 通过显示,隐藏控件展现的动画显示在真机显示有瑕疵,所以自定义背景动画View,下边是PlayerBgView代码

import android.animation.TypeEvaluator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import androidx.interpolator.view.animation.FastOutSlowInInterpolator/*** 播放器自定义的背景View:因为Constraint动画有显示瑕疵* */
class PlayerBgView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {//控件宽度private var mWidth = 0f//背景圆形半径(控件高度一半)private var mRadius = 0f//动画执行中的终点坐标private var mPositionEndX = 0fprivate var mPaint: Paint? = null//是否展开状态private var bExpend = true//背景颜色private val mColorBG = Color.parseColor("#D7D7D7")//动画时长private val mTimeAnim = 400L//展开后终点坐标x,固定private var mEndX = 0finit {initPaint()}override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)mWidth = w.toFloat()mRadius = h / 2fmPositionEndX = mWidth - mRadiusmEndX = mPositionEndXmeasurePaintStrokeWidth(h.toFloat())}private fun initPaint() {mPaint = Paint()mPaint!!.isAntiAlias = truemPaint!!.style = Paint.Style.FILLmPaint!!.color = mColorBGmPaint!!.strokeCap = Paint.Cap.ROUND}//设置画笔宽度为视图高度private fun measurePaintStrokeWidth(height: Float) {mPaint?.strokeWidth = height}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)drawBg(canvas)}private fun drawBg(canvas: Canvas) {canvas.drawCircle(mRadius, mRadius, mRadius, mPaint!!)if (mPositionEndX == 0f) returncanvas.drawLine(mRadius, mRadius, mPositionEndX, mRadius, mPaint!!)}fun doAnimation() {if (bExpend) doShrinkAnimation() else doExpandAnimation()bExpend = !bExpend}//展开动画private fun doExpandAnimation() {val animator = ValueAnimator.ofObject(PositiveEvaluator(), mRadius, mEndX)animator.addUpdateListener { valueAnimator: ValueAnimator ->mPositionEndX = valueAnimator.animatedValue as Floatinvalidate()}animator.duration = mTimeAnimanimator.interpolator = FastOutSlowInInterpolator()animator.start()}private inner class PositiveEvaluator : TypeEvaluator<Float> {override fun evaluate(v: Float, startValue: Float, endValue: Float): Float {return startValue + v * (endValue - startValue)}}//收缩动画private fun doShrinkAnimation() {val animator = ValueAnimator.ofObject(NegativeEvaluator(), mRadius, mEndX)animator.addUpdateListener { valueAnimator: ValueAnimator ->mPositionEndX = valueAnimator.animatedValue as Floatinvalidate()}animator.duration = mTimeAnimanimator.interpolator = FastOutSlowInInterpolator()animator.start()}private inner class NegativeEvaluator : TypeEvaluator<Float> {override fun evaluate(v: Float, startValue: Float, endValue: Float): Float {return endValue - v * (endValue - startValue)}}
}

更多推荐

音频播放器浮窗+通知栏播放器控制

本文发布于:2024-02-06 07:52:04,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1747621.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:播放器   通知   音频播放器

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!