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

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实现局域网远程控制电脑

《Python实现局域网远程控制电脑》这篇文章主要为大家详细介绍了如何利用Python编写一个工具,可以实现远程控制局域网电脑关机,重启,注销等功能,感兴趣的小伙伴可以参考一下... 目录1.简介2. 运行效果3. 1.0版本相关源码服务端server.py客户端client.py4. 2.0版本相关源码1

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

EasyPlayer.js网页H5 Web js播放器能力合集

最近遇到一个需求,要求做一款播放器,发现能力上跟EasyPlayer.js基本一致,满足要求: 需求 功性能 分类 需求描述 功能 预览 分屏模式 单分屏(单屏/全屏) 多分屏(2*2) 多分屏(3*3) 多分屏(4*4) 播放控制 播放(单个或全部) 暂停(暂停时展示最后一帧画面) 停止(单个或全部) 声音控制(开关/音量调节) 主辅码流切换 辅助功能 屏

控制反转 的种类

之前对控制反转的定义和解释都不是很清晰。最近翻书发现在《Pro Spring 5》(免费电子版在文章最后)有一段非常不错的解释。记录一下,有道翻译贴出来方便查看。如有请直接跳过中文,看后面的原文。 控制反转的类型 控制反转的类型您可能想知道为什么有两种类型的IoC,以及为什么这些类型被进一步划分为不同的实现。这个问题似乎没有明确的答案;当然,不同的类型提供了一定程度的灵活性,但

深入解析秒杀业务中的核心问题 —— 从并发控制到事务管理

深入解析秒杀业务中的核心问题 —— 从并发控制到事务管理 秒杀系统是应对高并发、高压力下的典型业务场景,涉及到并发控制、库存管理、事务管理等多个关键技术点。本文将深入剖析秒杀商品业务中常见的几个核心问题,包括 AOP 事务管理、同步锁机制、乐观锁、CAS 操作,以及用户限购策略。通过这些技术的结合,确保秒杀系统在高并发场景下的稳定性和一致性。 1. AOP 代理对象与事务管理 在秒杀商品

PostgreSQL中的多版本并发控制(MVCC)深入解析

引言 PostgreSQL作为一款强大的开源关系数据库管理系统,以其高性能、高可靠性和丰富的功能特性而广受欢迎。在并发控制方面,PostgreSQL采用了多版本并发控制(MVCC)机制,该机制为数据库提供了高效的数据访问和更新能力,同时保证了数据的一致性和隔离性。本文将深入解析PostgreSQL中的MVCC功能,探讨其工作原理、使用场景,并通过具体SQL示例来展示其在实际应用中的表现。 一、

vue2实践:el-table实现由用户自己控制行数的动态表格

需求 项目中需要提供一个动态表单,如图: 当我点击添加时,便添加一行;点击右边的删除时,便删除这一行。 至少要有一行数据,但是没有上限。 思路 这种每一行的数据固定,但是不定行数的,很容易想到使用el-table来实现,它可以循环读取:data所绑定的数组,来生成行数据,不同的是: 1、table里面的每一个cell,需要放置一个input来支持用户编辑。 2、最后一列放置两个b

【电机控制】数字滤波算法(持续更新)

文章目录 前言1. 数字低通滤波 前言 各种数字滤波原理,离散化公式及代码。 1. 数字低通滤波 滤波器公式 一阶低通滤波器的输出 y [ n ] y[n] y[n] 可以通过以下公式计算得到: y [ n ] = α x [ n ] + ( 1 − α ) y [ n − 1 ] y[n] = \alpha x[n] + (1 - \alpha) y[n-1]

OpenStack离线Train版安装系列—3控制节点-Keystone认证服务组件

本系列文章包含从OpenStack离线源制作到完成OpenStack安装的全部过程。 在本系列教程中使用的OpenStack的安装版本为第20个版本Train(简称T版本),2020年5月13日,OpenStack社区发布了第21个版本Ussuri(简称U版本)。 OpenStack部署系列文章 OpenStack Victoria版 安装部署系列教程 OpenStack Ussuri版

OpenStack离线Train版安装系列—1控制节点-环境准备

本系列文章包含从OpenStack离线源制作到完成OpenStack安装的全部过程。 在本系列教程中使用的OpenStack的安装版本为第20个版本Train(简称T版本),2020年5月13日,OpenStack社区发布了第21个版本Ussuri(简称U版本)。 OpenStack部署系列文章 OpenStack Victoria版 安装部署系列教程 OpenStack Ussuri版