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

2023-11-24 01:20

本文主要是介绍音频播放器浮窗+通知栏播放器控制,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、最终效果如图

二、音频播放器浮窗实现

原理:

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="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"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)}}
}

这篇关于音频播放器浮窗+通知栏播放器控制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/421025

相关文章

基于Python开发Windows屏幕控制工具

《基于Python开发Windows屏幕控制工具》在数字化办公时代,屏幕管理已成为提升工作效率和保护眼睛健康的重要环节,本文将分享一个基于Python和PySide6开发的Windows屏幕控制工具,... 目录概述功能亮点界面展示实现步骤详解1. 环境准备2. 亮度控制模块3. 息屏功能实现4. 息屏时间

Python远程控制MySQL的完整指南

《Python远程控制MySQL的完整指南》MySQL是最流行的关系型数据库之一,Python通过多种方式可以与MySQL进行交互,下面小编就为大家详细介绍一下Python操作MySQL的常用方法和最... 目录1. 准备工作2. 连接mysql数据库使用mysql-connector使用PyMySQL3.

如何搭建并配置HTTPD文件服务及访问权限控制

《如何搭建并配置HTTPD文件服务及访问权限控制》:本文主要介绍如何搭建并配置HTTPD文件服务及访问权限控制的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、安装HTTPD服务二、HTTPD服务目录结构三、配置修改四、服务启动五、基于用户访问权限控制六、

如何关闭Mac的Safari通知? 3招教你关闭Safari浏览器网站通知的技巧

《如何关闭Mac的Safari通知?3招教你关闭Safari浏览器网站通知的技巧》当我们在使用Mac电脑专注做一件事情的时候,总是会被一些消息推送通知所打扰,这时候,我们就希望关闭这些烦人的Mac通... Safari 浏览器的「通知」功能本意是为了方便用户及时获取最新资讯,但很容易被一些网站滥用,导致我们

Java 的 Condition 接口与等待通知机制详解

《Java的Condition接口与等待通知机制详解》在Java并发编程里,实现线程间的协作与同步是极为关键的任务,本文将深入探究Condition接口及其背后的等待通知机制,感兴趣的朋友一起看... 目录一、引言二、Condition 接口概述2.1 基本概念2.2 与 Object 类等待通知方法的区别

MySQL精准控制Binlog日志数量的三种方案

《MySQL精准控制Binlog日志数量的三种方案》作为数据库管理员,你是否经常为服务器磁盘爆满而抓狂?Binlog就像数据库的“黑匣子”,默默记录着每一次数据变动,但若放任不管,几天内这些日志文件就... 目录 一招修改配置文件:永久生效的控制术1.定位my.cnf文件2.添加核心参数不重启热更新:高手应

嵌入式Linux驱动中的异步通知机制详解

《嵌入式Linux驱动中的异步通知机制详解》:本文主要介绍嵌入式Linux驱动中的异步通知机制,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录前言一、异步通知的核心概念1. 什么是异步通知2. 异步通知的关键组件二、异步通知的实现原理三、代码示例分析1. 设备结构

SpringBoot请求参数接收控制指南分享

《SpringBoot请求参数接收控制指南分享》:本文主要介绍SpringBoot请求参数接收控制指南,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Spring Boot 请求参数接收控制指南1. 概述2. 有注解时参数接收方式对比3. 无注解时接收参数默认位置

Spring Security+JWT如何实现前后端分离权限控制

《SpringSecurity+JWT如何实现前后端分离权限控制》本篇将手把手教你用SpringSecurity+JWT搭建一套完整的登录认证与权限控制体系,具有很好的参考价值,希望对大家... 目录Spring Security+JWT实现前后端分离权限控制实战一、为什么要用 JWT?二、JWT 基本结构

Android实现两台手机屏幕共享和远程控制功能

《Android实现两台手机屏幕共享和远程控制功能》在远程协助、在线教学、技术支持等多种场景下,实时获得另一部移动设备的屏幕画面,并对其进行操作,具有极高的应用价值,本项目旨在实现两台Android手... 目录一、项目概述二、相关知识2.1 MediaProjection API2.2 Socket 网络