当前位置: 首页 > news >正文

Android Compose 一个音视频APP——Magic Music Player

Magic Music APP

Magic Music APP

  • Magic Music APP
    • 概述
    • 效果预览-视频资源
    • 功能预览
    • Library
    • 歌曲播放
      • 效果预览
      • 歌曲播放
        • 依赖注入
        • 设置播放源
        • 播放进度
        • 上一首&下一首
        • UI响应
      • 歌词
        • 歌词解析
          • 解析成行
          • 逐行解析
    • 视频播放
      • AndroidView引入Exoplayer
      • 自定义Exoplayer样式
      • 横竖屏切换
    • 歌曲多任务下载
      • 下载处理
    • 通知栏前台服务
      • 媒体服务
      • 下载服务
    • Other
      • 评论
      • 搜索
      • 搜索结果
      • 登录
      • 歌手详情
      • navigation
      • End
    • Link

概述

此项目基于网易云API,使用Compose UI编写而成,项目整体采用MVVM架构,主要实现主题切换(适配深色模式
)、音视频资源播放(Media3-Exoplayer)(其中视频播放对Exoplayer进行了自定义样式、竖屏和横屏切换等处理)、前台服务(通知栏)、歌曲下载、资源评论、歌曲解析、歌词逐行匹配等功能

效果预览-视频资源

MagicPlayer

功能预览

  • 主题

    • 亮色主题
    • 深色主题
    • 手动切换主题样式
    • 系统切换主题样式
  • 登录

    • 手机号码登录
    • 邮箱登录
    • 扫码登录
  • 歌曲(Media3-Exoplayer)

    • 歌曲播放(本地资源、网络资源)
    • 歌词解析
    • 歌词匹配(逐行)
    • 评论歌曲
    • 收藏歌曲
    • 前台服务(通知栏媒体样式)
  • 视频(Media3-Exoplayer)

    • 自定义exoplayer样式
    • 视频横屏与竖屏切换
    • 收藏视频
    • 评论视频
    • 分享视频
    • 前台服务(通知栏媒体样式)
  • 下载(Aria)

    • 歌曲下载
    • 前台服务(通知栏媒体样式)
    • 清空下载内容
  • 前台服务

    • 音视频媒体通知栏样式
    • 下载进度条通知栏样式
  • 歌单

    • 歌单
    • 专辑
    • 电台
  • 搜索

    • 搜索记录
    • 清空搜索记录
    • 搜索建议
    • 热门搜索
    • 搜索结果(歌曲、歌单、专辑、歌手…)
  • 评论

    • 资源评论(歌曲、歌单、专辑…)
    • 楼层评论(回复某人的评论)
    • 发送评论
    • 点赞评论
  • 收藏

  • 最近播放

  • 播放列表

  • 用户信息

  • 推荐

    • 歌单推荐
    • 专辑推荐
    • 歌曲推荐
    • 歌手推荐
  • 榜单

Library

Library NameDescription
retrofit、okhttp用户网络请求
hilt用于依赖注入
media-exoplayer用于音视频播放
aria用于资源下载
coil用于网络图片加载
pager用户多页面切换
paging3用户分页加载
room本地资源存储

歌曲播放

效果预览

歌曲播放

播放组件使用Media3-Exoplayer,通过hilt注入Exoplayer、MediaSession以及NotificationManager等依赖,通过在中间层监听Exoplayer播放状态和通过使用ShareFlow将所监听的数据转发至需要更新UI的ViewModel层。

依赖注入

下方通过Hilt提供了AudioAttributes、ExoPlayer、MediaSession、MusicNotificationManager、MusicServiceHandler等依赖,在外部我们只需注入MusicServiceHandler依赖,便可完成数据监听,并更新UI。在中间层MusicServiceHandler我们只需注入ExoPlayer依赖,通过实现其Player.Listener接口的一系列方法,完成对播放状态以及播放数据的监听

   @Providesfun provideAudioAttributes():AudioAttributes = AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MUSIC).setUsage(C.USAGE_MEDIA).build()@OptIn(UnstableApi::class)@Singleton@Providesfun provideMusicExoPlayer(@ApplicationContext context: Context,audioAttributes: AudioAttributes):ExoPlayer = ExoPlayer.Builder(context).setAudioAttributes(audioAttributes, true).setHandleAudioBecomingNoisy(true).setTrackSelector(DefaultTrackSelector(context)).build()@Provides@Singletonfun provideMediaSession(@ApplicationContext context: Context,player: ExoPlayer,): MediaSession = MediaSession.Builder(context, player).build()@Provides@Singletonfun provideNotificationManager(@ApplicationContext context: Context,player: ExoPlayer,): MusicNotificationManager = MusicNotificationManager(context = context,exoPlayer = player)@Provides@Singletonfun provideServiceHandler(exoPlayer: ExoPlayer,musicUseCase: MusicUseCase,service: MusicApiService): MusicServiceHandler= MusicServiceHandler(exoPlayer = exoPlayer,musicUseCase = musicUseCase,service = service)
设置播放源

为了避免重复无效网络请求,对歌曲URL进行本地缓存,已经拥有URL的歌曲便不再重复获取URL,直接将其设置为当前播放项,通过MediaMetadata设置媒体相关信息,便于之后在开启前台通知栏服务时,获取相关信息

    private suspend fun replaceMediaItem(index: Int){if (playlist.isEmpty())returncurrentPlayIndex = indexif (!playlist[currentPlayIndex].isLoading) {//未加载getMusicUrl(playlist[currentPlayIndex].songID){ url,duration,size->playlist[currentPlayIndex].url = urlplaylist[currentPlayIndex].duration = durationplaylist[currentPlayIndex].isLoading = trueplaylist[currentPlayIndex].size = CommonUtil.formatFileSize(size.toDouble())setMediaItem(playlist[currentPlayIndex])}}else{setMediaItem(playlist[currentPlayIndex])}}private suspend fun setMediaItem(bean: SongMediaBean){exoPlayer.setMediaItem(MediaItem.Builder().setUri(bean.url) //播放链接.setMediaMetadata(MediaMetadata.Builder().setArtist(bean.artist) //歌手.setTitle(bean.songName) //歌曲名称.setSubtitle(bean.artist) // 歌手.setArtworkUri(bean.cover.toUri()) //封面.setDescription("${bean.songID}").build()).build())exoPlayer.prepare()exoPlayer.playWhenReady = truestartProgress()_eventFlow.emit(AudioPlayState.CurrentPlayItem(playlist[currentPlayIndex]))_eventFlow.emit(AudioPlayState.Playing(true))}
播放进度

通过JOB开启一个协程,并每隔0.5s获取一次当前播放进度,并通过ShareFlow传递到下游

    /*** 为歌曲播放时,每隔0.5s查询一次当前播放progress,并通知UI进行更新*/private suspend fun startProgress() = job.run {while(true){delay(500L)_eventFlow.emit(AudioPlayState.Progress(exoPlayer.currentPosition,exoPlayer.duration))}}/*** 当歌曲暂停时,停止更新progress*/private suspend fun stopProgress(){job?.cancel()_eventFlow.emit(AudioPlayState.Playing(false))}
上一首&下一首

每次APP首次加载时,将缓存到本地的播放列表项取出存储到进程中,之后的每次数据更新都在进程中的播放列表进行变化,并变更到数据库

     fun getNextIndex():Int = (currentPlayIndex + 1) % playlist.sizefun getPriorIndex(): Int =if (currentPlayIndex <= 0)playlist.size - 1else(currentPlayIndex - 1) % playlist.size/*** 切换播放列表下一首*/private suspend fun next(){if (playlist.isNotEmpty()){val next =  getNextIndex()replaceMediaItem(next)}else{currentPlayIndex = -1}}/*** 切换播放列表上一首*/private suspend fun prior(){if (playlist.isNotEmpty()){val prior = getPriorIndex()replaceMediaItem(prior)}else{currentPlayIndex = -1}}
UI响应

在需要响应数据的ViewModel层,只需注入MusicServiceHandler依赖即可,并对其传递的事件进行监听,并根据事件状态,做出不同的处理,在ViewModel从对各数据值通过mutableStateOf封装在一个data class中,并绑定至Composable函数中,当ViewModel值的状态发生改变时,UI界面及时响应变更并更新UI

    private fun playerStatus(){viewModelScope.launch(Dispatchers.IO) {musicServiceHandler.eventFlow.collect {when(it){is AudioPlayState.Ready->{_uiStatus.value = uiStatus.value.copy(totalDuration = transformTime(it.duration))}is AudioPlayState.Buffering->{calculateProgress(it.progress,it.duration)}is AudioPlayState.Playing->{_uiStatus.value = uiStatus.value.copy(isPlaying = it.isPlaying)}is AudioPlayState.Progress->{calculateProgress(it.progress,it.duration)val line = matchLyric(it.progress)_uiStatus.value = _uiStatus.value.copy(currentLine = line)}is AudioPlayState.CurrentPlayItem->{if (it.bean != null){_uiStatus.value = uiStatus.value.copy(artist = it.bean.artist,name = it.bean.songName,cover = it.bean.cover,musicID = it.bean.songID,totalDuration = transformTime(it.bean.duration))//同步更新数据库musicUseCase.updateUrl(it.bean.songID,it.bean.url)musicUseCase.updateLoading(it.bean.songID, true)musicUseCase.updateDuration(it.bean.songID, it.bean.duration)musicUseCase.updateSize(it.bean.songID, it.bean.size)}}is AudioPlayState.Reenter->{if (it.bean != null){_uiStatus.value = uiStatus.value.copy(artist = it.bean.artist,name = it.bean.songName,cover = it.bean.cover,musicID = it.bean.songID,totalDuration = transformTime(it.bean.duration))}}is AudioPlayState.NetworkFailed->{_eventFlow.emit(MusicPlayerStatus.NetworkFailed(it.msg))}}}}}

歌词

歌词解析

此项目采用的是歌词逐行解析,首先了解一下lrc歌词格式

[00:18.466]今天我 寒夜里看雪飘过
分别代表[分:秒:毫秒]内容

逐行歌词解析主要采用两个正则表达式:一个将所有歌词拆分成行的形式,一个解析每一行的内容

其中“(.+)”是匹配任意长度字符,"\\d"是匹配0-9任一数字,“\\d{2,3}”是匹配2位或者3位数字

private val PATTERN_LINE = Pattern.compile("((\\[\\d\\d:\\d\\d\\.\\d{2,3}\\])+)(.+)")
private val PATTERN_TIME = Pattern.compile("\\[(\\d\\d):(\\d\\d)\\.(\\d{2,3})\\]")
解析成行

通过\\n将歌词解析成数行,此处\\为转义字符,实为\,故\\n\n,意味换行符。然后对每一行歌词进行解析

   fun parseLyric(lrcText: String): List<LyricBean>? {if (lrcText.isEmpty()) {return null}val entityList: MutableList<LyricBean> = ArrayList<LyricBean>()// 以换行符为分割点val array = lrcText.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()for (line in array) {// 循环遍历按行解析val list: List<LyricBean>? = parseLine(line)list?.let {entityList.addAll(it)}}// 以时间为基准,从小到大排列entityList.sortBy {it.time}return entityList}
逐行解析

由于此处部分歌曲的歌词URL并未严格遵守lrc格式,部分歌曲歌词首部作者信息等使用JSON字符进行返回,所有在对每一行进行解析时,对此情况进行JSON处理,然后解析添加到歌词列表中。余下,便是常规lrc正则表达式判定,并读取其中的数据

   /*** 解析每一句歌词* 其中头部和尾部存在歌手、编曲等JSON信息* 中间为标准LRC歌词格式* @param line*/private fun parseLine(line: String): List<LyricBean>? {var newLine = lineval entryList: MutableList<LyricBean> = ArrayList<LyricBean>()if (newLine.isEmpty()) {return null}// 去除空格newLine = line.trim { it <= ' ' }/*** 作者等信息:* [{"t":0,"c":[{"tx":"作词: "},{"tx":"黄家驹","li":"http://p1.music.126.net/2rERC5bz1BD0GZrU06saTw==/109951166629360845.jpg","or":"orpheus://nm/artist/home?id=189688&type=artist"}]},*  {"t":1000,"c":[{"tx":"作曲: "},{"tx":"黄家驹","li":"http://p1.music.126.net/2rERC5bz1BD0GZrU06saTw==/109951166629360845.jpg","or":"orpheus://nm/artist/home?id=189688&type=artist"}]},*  {"t":2000,"c":[{"tx":"编曲: "},{"tx":"Beyond"},{"tx":"/"},{"tx":"梁邦彦"}]},*  {"t":3000,"c":[{"tx":"制作人: "},{"tx":"Beyond"},{"tx":"/"},{"tx":"梁邦彦"}]},*  {"t":271852,"c":[{"tx":"录音: "},{"tx":"Shunichi Yokoi"}]}]* *//**** 歌词和时间:[00:18.466]今天我 寒夜里看雪飘过* */val lineMatcher: Matcher = PATTERN_LINE.matcher(newLine)// 正则表达式,判断line中是否包含“[00:00.00]xxx”格式的内容"// 如果没有,则为JSON字符串try {if (!lineMatcher.matches()) {if (!PATTERN_TIME.matcher(newLine).matches()){//解析作者等信息val infoBean = GsonFormat.fromJson(newLine,LyricAuthorBean::class.java)var content = ""infoBean.c.forEach {//将所有信息组成一行content += it.tx}entryList.add(LyricBean(infoBean.t,content))}else{//某一行歌词只包含“[00:00.00]”内容,不包含文字,则不进行处理return null}}}catch (e:Exception){println(e.message)return null}// 获取文本内容val text: String? = lineMatcher.group(3)// 获取时间标签val times: String? = lineMatcher.group(1)val timeMatcher: Matcher? = times?.let { PATTERN_TIME.matcher(it) }if (timeMatcher != null) {//将时间转为毫秒级while (timeMatcher.find()) {val min: Long = timeMatcher.group(1)?.toLong() ?:0L // 分val sec: Long = timeMatcher.group(2)?.toLong() ?:0L // 秒val mil: Long = timeMatcher.group(3)?.toLong() ?:0L // 毫秒val time: Long = min * DateUtils.MINUTE_IN_MILLIS + sec * DateUtils.SECOND_IN_MILLIS + mil * 10entryList.add(LyricBean(text = text ?: "", time = time))}}return entryList}

视频播放

视频播放依旧使用的是Media3-Exoplayer组件,相对于音频资源播放,需要稍加封装。此项目对Exoplayer进行了自定义样式处理、竖屏和横屏切换处理、通知栏媒体样式前台服务处理等。视频播放分为MV和MLOG两种类型,所衍生出两个不同UI的界面,其中播放逻辑基本一致,此处便以其中一处作为讲解示例

AndroidView引入Exoplayer

在Compose中还并未有PlayerView对应的组件,所有需要通过AndroidView进行引入,其中factory为初始化组件参数,update为当状态发生变化,导致发生重组时,更新相对应的数据。其中useController = false意味不使用其自带的控件,例如播放、暂停、进度条等

    AndroidView(factory = { context->PlayerView(context).apply {viewModel.mediaController.valueuseController = falselayoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT)}},update = {if (it.player == null)it.player = viewModel.mediaController.valuewhen(lifecycle.value){Lifecycle.Event.ON_STOP-> {it.onPause()it.player?.stop()}Lifecycle.Event.ON_PAUSE-> {it.onPause()it.player?.pause()}Lifecycle.Event.ON_RESUME-> it.onResume()else-> Unit}},modifier = Modifier.fillMaxWidth().aspectRatio(16 / 9f).clickable { viewModel.onPlayEvent(MvPlayerEvent.ShowControlPanel) }.background(MagicMusicTheme.colors.black).constrainAs(playerRes){start.linkTo(parent.start)end.linkTo(parent.end)top.linkTo(parent.top)})

自定义Exoplayer样式

通过useController = false不使用自带的控件后,将播放控件分为竖屏和横屏两种状态,并通过AnimatedVisibility进行显示与隐藏,具体的代码便不在贴出,可以点击文末项目链接进行浏览。总体思路便是不使用自带的控件,然后将自己需要的控件样式与AndroidView引入的Exoplayer进行组合

  ConstraintLayout(modifier = Modifier.fillMaxSize().background(MagicMusicTheme.colors.background).statusBarsPadding().navigationBarsPadding()){val (playerRes,controlRes,similarRes) = createRefs()AndroidView(factory = { context->PlayerView(context).apply { //省略不必要代码... }},update = { //省略不必要代码... })//竖屏播放控件PlayerControls(isPlaying = value.isPlaying,isVisible = value.isVisibility && !value.isFullScreen,progress = value.progress,currentPosition = value.currentPosition,bean = value.mvInfo,onBack = onBack,onChangeProgress = { viewModel.onPlayEvent(MvPlayerEvent.ChangeProgress(it)) },onPlayOrPause = { viewModel.onPlayEvent(MvPlayerEvent.PlayOrPause) },onFullScreen = { viewModel.onPlayEvent(MvPlayerEvent.FullScreen) },modifier = Modifier.fillMaxWidth().constrainAs(controlRes){start.linkTo(parent.start)end.linkTo(parent.end)top.linkTo(playerRes.top)bottom.linkTo(playerRes.bottom)})AnimatedVisibility(visible = !value.isFullScreen,enter = EnterTransition.None,exit = ExitTransition.None,modifier = Modifier.constrainAs(similarRes){start.linkTo(parent.start)end.linkTo(parent.end)top.linkTo(playerRes.bottom)}){LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp),contentPadding = PaddingValues(start = 20.dp, end = 20.dp, bottom = 10.dp, top = 5.dp),modifier = Modifier.fillMaxWidth().background(MagicMusicTheme.colors.background)){//省略不必要代码...}}//全屏时的播放控件AnimatedVisibility(visible = value.isFullScreen && value.isVisibility,enter = EnterTransition.None,exit = ExitTransition.None,) {if (value.mvInfo != null){FullScreenControl(progress = value.progress,currentPosition = value.currentPosition,title = value.mvInfo.name,duration = value.mvInfo.duration.toLong(),isPlaying = value.isPlaying,onExitFullScreen = { viewModel.onPlayEvent(MvPlayerEvent.FullScreen) },onPlayOrPause = { viewModel.onPlayEvent(MvPlayerEvent.PlayOrPause) },onChangeProgress = { viewModel.onPlayEvent(MvPlayerEvent.ChangeProgress(it))  },onShowControl = { viewModel.onPlayEvent(MvPlayerEvent.ShowControlPanel) })}}}}

横竖屏切换

首先在manifest的Activity中添加如下属性,包括对键盘、屏幕方向、屏幕大小的一些配置

 android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"

因为我使用的是单Activity模式,故我讲屏幕旋转逻辑放在MainActivity中,暴露外部一个方法进行调用即可。由于此方法需要一个Context上下参数,故设置了一个懒加载的MainActivity上下文,然后在onCreate中初始化parentThis = this。其中activity.requestedOrientation = orientation语句为完成屏幕旋转的关键,剩下的便是对系统状态栏和导航栏的隐藏和显示逻辑处理

companion object{lateinit var parentThis:MainActivityfun Context.setScreenOrientation(orientation: Int) {val activity = this.findActivity() ?: returnactivity.requestedOrientation = orientationif (orientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {hideSystemUi()} else {showSystemUi()}}private fun Context.hideSystemUi() {val activity = this.findActivity() ?: returnval window = activity.window ?: returnWindowCompat.setDecorFitsSystemWindows(window, false)WindowInsetsControllerCompat(window, window.decorView).let { controller ->controller.hide(WindowInsetsCompat.Type.systemBars())controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE}}private fun Context.showSystemUi() {val activity = this.findActivity() ?: returnval window = activity.window ?: returnWindowCompat.setDecorFitsSystemWindows(window, true)WindowInsetsControllerCompat(window,window.decorView).show(WindowInsetsCompat.Type.systemBars())}private fun Context.findActivity(): Activity? = when (this) {is Activity -> thisis ContextWrapper -> baseContext.findActivity()else -> null}}

在ViewModel中响应的横竖屏按钮切换事件处理,便可以直接引用上述暴露的方法,并在最后变更当前屏幕状态,让UI界面进行重组

             with(MainActivity.parentThis){if (_uiState.value.isFullScreen){//纵向setScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)}else{//横向setScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)}}_uiState.value = uiState.value.copy(isFullScreen = !_uiState.value.isFullScreen)

歌曲多任务下载

歌曲下载采用Aria Library实现多任务下载,并实现前台服务下载,在通知栏显示下载进度。在外部开启下载服务,通过startService方式启动,并通过将下载回调通过接口进行返回,然后在中间层DownloadHandler通过bindService绑定服务,并通过其中的binder获取当前service,然后实现接返回的接口,并通过ShareFlow传递至下游的ViewModel。

### Service 在`DownloadService`中完成Aria注册,初始化等配置,并实现`DownloadTaskListener`接口,继承其一系列方法,并注册接口回调,供中间层接收下载进度
 fun setDownloadListener(listener: DownloadListener){this.listener = listener}private fun onDownloadListener(task: DownloadTask,msg:String){if (this::listener.isInitialized){listener.onDownloadState(task,msg)}}

下列为实现DownloadTaskListener的一系列接口,对不同的下载状态进行处理,然后将处理结果通过onDownloadListener进行回调至中间层

/*** 任务预加载*/override fun onPre(task: DownloadTask?) {if (task != null){onDownloadListener(task,"")}}/*** 任务预加载完成*/override fun onTaskPre(task: DownloadTask?) {if (task != null){onDownloadListener(task,"")}}/*** 等待中*/override fun onWait(task: DownloadTask?) {if (task != null){onDownloadListener(task,"")}}/*** 开始下载*/override fun onTaskStart(task:DownloadTask?){if (task != null){onDownloadListener(task,"")}}/*** 下载暂停*/override fun onTaskStop(task:DownloadTask?){if (task != null){onDownloadListener(task,"")}}/*** 下载恢复*/override fun onTaskResume(task:DownloadTask?){if (task != null){onDownloadListener(task,"")}}/*** 下载中*/@RequiresApi(Build.VERSION_CODES.O)override fun onTaskRunning(task:DownloadTask?){if (task != null){task.convertFileSizeval progress = (task.currentProgress * 100 / task.fileSize).toInt()notification.setProgress(progress)onDownloadListener(task,"")}}/*** 任务不支持断点*/override fun onNoSupportBreakPoint(task: DownloadTask?) {if (task != null){onDownloadListener(task,"")}}/*** 下载完成*/override fun onTaskComplete(task:DownloadTask?){if (task != null){val completeList = Aria.download(this).allCompleteTaskval unCompleteList = Aria.download(this).allNotCompleteTaskif (completeList != null && unCompleteList != null && completeList.isNotEmpty() && unCompleteList.isEmpty()){if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){stopForeground(Service.STOP_FOREGROUND_DETACH)isForegroundSuc = false}//下载任务全部完成,结束servicestopSelf()}onDownloadListener(task,"")}}/*** 下载失败*/override fun onTaskFail(task: DownloadTask?, e: Exception?){if (task != null){onDownloadListener(task,e?.message.toString())}}/*** 取消下载*/override fun onTaskCancel(task:DownloadTask?){if (task != null){onDownloadListener(task,"")}}

下载处理

在中间层DownloadHandler需要创建下载文件夹,对需求下载的内容进行查重,判断其是否已经被下载,如若已经下载,便不在重复下载、下载状态处理、以及读写权限处理等。下列是对Service中的接口进行监听,并通过将监听的数据处理后,通过ShareFlow分发至下游

 @OptIn(DelicateCoroutinesApi::class)private fun downloadListener(downloadService: DownloadService) {downloadService.setDownloadListener(object : DownloadListener {override fun onDownloadState(task: DownloadTask,msg:String) {val index = searchIndex(task.key)if (index == -1) returnGlobalScope.launch(Dispatchers.Main) {when (task.state) {IEntity.STATE_PRE -> {downloadList[index].taskID = task.entity.iddownloadUseCase.updateTaskID(musicID = downloadList[index].musicID,taskID = task.entity.id)_eventFlow.emit(DownloadStateFlow.Prepare(task,index))}IEntity.STATE_WAIT -> {_eventFlow.emit(DownloadStateFlow.Prepare(task,index))}IEntity.STATE_RUNNING -> {_eventFlow.emit(DownloadStateFlow.Running(task,index))}IEntity.STATE_STOP -> {_eventFlow.emit(DownloadStateFlow.Stop(task,index))}IEntity.STATE_CANCEL -> {downloadList.removeAt(index)_eventFlow.emit(DownloadStateFlow.Cancel(task,index))}IEntity.STATE_COMPLETE -> {downloadList[index].download = truedownloadUseCase.updateDownloadState(musicID = downloadList[index].musicID,download = true)Aria.download(this).load(task.entity.id).removeRecord()_eventFlow.emit(DownloadStateFlow.Complete(task,index))}IEntity.STATE_FAIL -> {_eventFlow.emit(DownloadStateFlow.Fail(task,index,msg))}}}}})}

通知栏前台服务

在此项目中前台服务通知栏分为媒体资源和下载两种样式,其中媒体资源的音频和视频服务启动方式不一样,音频采用startService启动,视频则采用MediaControl,其内部自带服务启动,只需对其进行相对应初始化即可;下载则是采用startServicebindService混合启动模式,即两种都使用

媒体服务

由于音频服务和视频服务都继承MediaSessionService,不同之处在于启动方式和依赖注入,故此处以音频服务为例。

文章顶部已经介绍了hilt依赖注入,此处便不在重复,直接通过@Inject注入所需依赖,然后外部通过startService启动服务后,在onStartCommand中构建通知栏

@AndroidEntryPoint
class MusicService:MediaSessionService() {@Injectlateinit var mediaSession: MediaSession@Injectlateinit var notificationManager: MusicNotificationManageroverride fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {notificationManager.startNotificationService(mediaSession = mediaSession,mediaSessionService = this)}return super.onStartCommand(intent, flags, startId)}override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession = mediaSessionoverride fun onDestroy() {super.onDestroy()mediaSession.apply {release()if (player.playbackState != Player.STATE_IDLE) {player.seekTo(0)player.playWhenReady = falseplayer.stop()}}}
}

在Android 8.0之后开启的通知栏需要建立Channel,其中setMediaDescriptionAdapter为设置通知栏显示的相关信息,此部分来源于当前播放项,也就是文章之前提过的MediaItem中获取

class MusicNotificationManager @Inject constructor(@ApplicationContext private val context: Context,private val exoPlayer: ExoPlayer
) {private val NOTIFICATION_ID = 1private val NOTIFICATION_CHANNEL_NAME = "Music Notification channel"private val NOTIFICATION_CHANNEL_ID = "Music Notification channel id"private var  notificationManager = NotificationManagerCompat.from(context)init {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {createNotificationChannel()}}@RequiresApi(Build.VERSION_CODES.O)fun startNotificationService(mediaSessionService: MediaSessionService,mediaSession: MediaSession,){buildNotification(mediaSession)startForegroundNotificationService(mediaSessionService)}@RequiresApi(Build.VERSION_CODES.O)private fun startForegroundNotificationService(mediaSessionService: MediaSessionService){val notification = Notification.Builder(context, NOTIFICATION_CHANNEL_ID).setCategory(Notification.CATEGORY_SERVICE).build()mediaSessionService.startForeground(NOTIFICATION_ID, notification)}@OptIn(UnstableApi::class)private fun buildNotification(mediaSession: MediaSession){PlayerNotificationManager.Builder(context,NOTIFICATION_ID,NOTIFICATION_CHANNEL_ID).setMediaDescriptionAdapter(MusicNotificationAdapter(context = context,pendingIntent = mediaSession.sessionActivity)).setSmallIconResourceId(R.drawable.magicmusic_logo) //通知栏的小图标.build().apply {setMediaSessionToken(mediaSession.sessionCompatToken)setUseFastForwardActionInCompactView(true)setUseRewindActionInCompactView(true)setUseNextActionInCompactView(true)setPriority(NotificationCompat.PRIORITY_DEFAULT)setPlayer(exoPlayer)}}@RequiresApi(Build.VERSION_CODES.O)private fun createNotificationChannel(){val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID,NOTIFICATION_CHANNEL_NAME,NotificationManager.IMPORTANCE_DEFAULT,)notificationManager.createNotificationChannel(channel)}
}

由于音乐cover为URL,需要在通知栏显示,需要将其转化为bitmap,下列getBitmap方法启动一个协程并使用coil将url转为bitmap并通过函数返回,然后在getCurrentLargeIcon方法中设置bitmap即可,其他的title、subTitle等信息便可以直接设置

@UnstableApi
class MusicNotificationAdapter(private val context: Context,private val pendingIntent: PendingIntent?,
):PlayerNotificationManager.MediaDescriptionAdapter {/*** 通知栏中歌曲的封面、名称、作者等信息*/override fun getCurrentContentTitle(player: Player): CharSequence {return player.mediaMetadata.title ?: "Unknown"}override fun createCurrentContentIntent(player: Player): PendingIntent? = pendingIntentoverride fun getCurrentContentText(player: Player): CharSequence {return player.mediaMetadata.subtitle ?: "Unknown"}override fun getCurrentLargeIcon(player: Player,callback: PlayerNotificationManager.BitmapCallback): Bitmap? {getBitmap(url = player.mediaMetadata.artworkUri, //此字段内容为约定而使onSuccess = {callback.onBitmap(it)},onError = {})return null}@OptIn(DelicateCoroutinesApi::class)private fun getBitmap(url:Uri?,onSuccess:(Bitmap)->Unit,onError:(String)->Unit){var bitmap:Bitmap? = nullval scope = GlobalScope.launch(Dispatchers.Main){val request = ImageRequest.Builder(context = context).data(url).allowHardware(false).build()val result = context.imageLoader.execute(request)if (result is SuccessResult){bitmap =  (result.drawable as BitmapDrawable).bitmap}else{cancel("Error Request")}}scope.invokeOnCompletion {bitmap?.let { bitmap->onSuccess(bitmap)}?:it?.let {onError(it.message.toString())}?: onError("Unknown Exception")}}
}

还需在manifest中声明此服务

<serviceandroid:name=".route.musicplayer.service.MusicService"android:exported="true"android:foregroundServiceType="mediaPlayback"><intent-filter><action android:name="androidx.media3.session.MediaSessionService" /></intent-filter></service>

下载服务

开启下载是通过startService方式启动,其中通过Binder返回当前Service对象,开启下载服务后,在onStartCommand中解析下载信息,然后开启前台服务。值得注意的是,如果明确服务为前台服务,在 Android 8.0 以后可以通过调用 startForegroundService启动前台服务,
它和 startService 的区别在于是它包含一个隐含承诺,即必须在服务启动后尽快调用 startForeground,否则 5s 后服务将停止,且会触发 ANR。所有下来对启动服务进行了处理,让后台计时4.5S,若4.5S之后仍未启动服务,则手动关闭服务,防止发生异常

class DownloadService:Service(),DownloadTaskListener {private lateinit var notification:DownloadNotificationprivate var isForegroundSuc = falseprivate var timerFlag = falseprivate val FOREGROUND_NOTIFY_ID = 1private lateinit var listener:DownloadListenerprivate var notificationID = 100private var map:Map<String,Int> = emptyMap()override fun onBind(p0: Intent?): IBinder = DownloadBinder()inner class DownloadBinder:Binder(){val service:DownloadServiceget() = this@DownloadService}override fun onCreate() {super.onCreate()initAria()initNotification()}@RequiresApi(Build.VERSION_CODES.O)override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {if (intent != null){val url = intent.getStringExtra(Constants.DownloadURL) ?: ""val path = intent.getStringExtra(Constants.DownloadPath) ?: ""val cover = intent.getStringExtra(Constants.DownloadCover) ?: ""val name = intent.getStringExtra(Constants.DownloadName) ?: "Unknown"val taskID = Aria.download(this).load(url).setFilePath(path).create()if (taskID > 0L){notificationID++map += url to notificationIDstartForeground(name,cover)}/*** 如果明确服务一定是前台服务,在 Android 8.0 以后可以调用 startForegroundService,* 它和 startService 的区别是它隐含了一个承诺,必须在服务中尽快调用 startForeground,否则 5s 后服务将停止,且会触发 ANR。*/if (!timerFlag){timerFlag = trueobject :CountDownTimer(4500L,4500L){override fun onTick(p0: Long) {}override fun onFinish() {if (!isForegroundSuc){/*** 如果4.5s后没有执行相关操作,则停止服务*/stopForeground(STOP_FOREGROUND_DETACH)stopSelf()}}}.start()}}return super.onStartCommand(intent, flags, startId)}@RequiresApi(Build.VERSION_CODES.O)private fun startForeground(name: String, cover: String) {if (!isForegroundSuc) {getBitmap(url = cover,onSuccess = {startForeground(FOREGROUND_NOTIFY_ID, notification.createNotification(notificationID,name,it))isForegroundSuc = true},onError = {val bitmap = BitmapFactory.decodeResource(APP.context.resources, R.drawable.magicmusic_logo)startForeground(FOREGROUND_NOTIFY_ID, notification.createNotification(notificationID,name,bitmap))isForegroundSuc = true})}}@kotlin.OptIn(DelicateCoroutinesApi::class)private fun getBitmap(url: String?,onSuccess:(Bitmap)->Unit,onError:(String)->Unit){var bitmap: Bitmap? = nullval scope = GlobalScope.launch(Dispatchers.Main){val request = ImageRequest.Builder(context = APP.context).data(url).allowHardware(false).build()val result = APP.context.imageLoader.execute(request)if (result is SuccessResult){bitmap =  (result.drawable as BitmapDrawable).bitmap}else{cancel("Error Request")}}scope.invokeOnCompletion {bitmap?.let { bitmap->onSuccess(bitmap)}?:it?.let {onError(it.message.toString())}?: onError("Unknown Exception")}}private fun initAria(){Aria.download(this).register()Aria.get(this).downloadConfig.setMaxTaskNum(3).setUseBlock(true).setConvertSpeed(true).setUpdateInterval(3000L)}private fun initNotification(){if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){notification = DownloadNotification(APP.context)}}override fun onDestroy() {super.onDestroy()Aria.download(this).unRegister()isForegroundSuc = falsetimerFlag = falsestopForeground(STOP_FOREGROUND_DETACH)stopSelf()}//省略...
}

在通知栏处,在创建通知栏时,只需设置.setProgress(maxProgress,0,false)即可出现进度条,然后只需暴露创建通知和刷新下载进度Progress两个方法即可,在服务中通过计算当前下载进度然后调用DownloadNotificationsetProgress,便可完成通知栏下载进度动态显示

@RequiresApi(Build.VERSION_CODES.O)
class DownloadNotification(private val context:Context
) {private val NOTIFICATION_CHANNEL_NAME = "Download Notification channel"private val NOTIFICATION_CHANNEL_ID = "Download Notification channel id"private lateinit var notificationBuilder:NotificationCompat.Builderprivate lateinit var notificationManager: NotificationManagerCompatprivate val maxProgress = 100fun createNotification(id:Int,name: String,bitmap: Bitmap):Notification?{if (context != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){notificationManager = NotificationManagerCompat.from(context)notificationBuilder = NotificationCompat.Builder(context,NOTIFICATION_CHANNEL_ID.plus(id))createNotificationChannel(id)return startNotification(id,name, bitmap)}return null}@OptIn(UnstableApi::class)private fun startNotification(id: Int,name: String,bitmap: Bitmap):Notification?{notificationBuilder.setPriority(NotificationCompat.PRIORITY_DEFAULT).setSmallIcon(R.drawable.magicmusic_logo).setAutoCancel(false).setProgress(maxProgress,0,false).setContentText(name).setLargeIcon(bitmap)if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {return null}notificationManager.notify(id,notificationBuilder.build())return notificationBuilder.build()}fun setProgress(id:Int,progress:Int){if (this::notificationBuilder.isInitialized){if (progress in 0 until maxProgress){notificationBuilder.setContentText("${progress}% downloaded")notificationBuilder.setProgress(maxProgress,progress,false)}else if (progress == maxProgress){notificationBuilder.setContentText("downloaded successful!")notificationBuilder.setAutoCancel(true)}else{notificationBuilder.setContentText("downloaded failed!")}if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {return}notificationManager.notify(id,notificationBuilder.build())}}@RequiresApi(Build.VERSION_CODES.O)private fun createNotificationChannel(id:Int){val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID.plus(id),NOTIFICATION_CHANNEL_NAME.plus(id),NotificationManager.IMPORTANCE_DEFAULT,)notificationManager.createNotificationChannel(channel)}
}

Other

每一个页面都适配了亮色主题和深色主题,由于篇幅有限,还有些许页面没有做过多解释,下载只对部分功能效果图进行贴出

评论

评论分为歌单评论、专辑评论、歌曲评论、MV评论、MLOG评论等,而每一个功能的评论又分为:资源评论、楼层评论(回复他人的评论)、发送评论、点赞评论几部分

搜索

搜索结果

登录

歌手详情

navigation

End

由于篇幅有限,便只贴示部分图片,如若有意,可以点击下方项目链接进行浏览

Link

Github

https://github.com/FranzLiszt-1847/MagicPlayer

Gitee

https://gitee.com/FranzLiszt1847/MagicPlayer

相关文章:

Android Compose 一个音视频APP——Magic Music Player

Magic Music APP Magic Music APP Magic Music APP概述效果预览-视频资源功能预览Library歌曲播放效果预览歌曲播放依赖注入设置播放源播放进度上一首&下一首UI响应 歌词歌词解析解析成行逐行解析 视频播放AndroidView引入Exoplayer自定义Exoplayer样式横竖屏切换 歌曲多任…...

Nginx实战:安装搭建

目录 前言 一、yum安装 二、编译安装 1.下载安装包 2.解压 3.生成makefile文件 4.编译 5.安装执行 6.执行命令软连接 7.Nginx命令 前言 nginx的安装有两种方式&#xff1a; 1、yum安装&#xff1a;安装快速&#xff0c;但是无法在安装的时候带上想要的第三方包 2、…...

Qt之条件变量QWaitCondition详解(从使用到原理分析全)

QWaitCondition内部实现结构图&#xff1a; 相关系列文章 C之Pimpl惯用法 目录 1.简介 2.示例 2.1.全局配置 2.2.生产者Producer 2.3.消费者Consumer 2.4.测试例子 3.原理分析 3.1.源码介绍 3.2.辅助函数CreateEvent 3.3.辅助函数WaitForSingleObject 3.4.QWaitCo…...

OpenSource - 一站式自动化运维及自动化部署平台

文章目录 orion-ops 是什么重构特性快速开始技术栈功能预览添砖加瓦License orion-ops 是什么 orion-ops 一站式自动化运维及自动化部署平台, 使用多环境的概念, 提供了机器管理、机器监控报警、Web终端、WebSftp、机器批量执行、机器批量上传、在线查看日志、定时调度任务、应…...

【后端高频面试题--设计模式下篇】

&#x1f680; 作者 &#xff1a;“码上有前” &#x1f680; 文章简介 &#xff1a;后端高频面试题 &#x1f680; 欢迎小伙伴们 点赞&#x1f44d;、收藏⭐、留言&#x1f4ac; 后端高频面试题--设计模式下篇 往期精彩内容设计模式总览模板方法模式怎么理解模板方法模式模板方…...

这才是大学生该做的副业,别再痴迷于游戏了!

感谢大家一直以来的支持和关注&#xff0c;尤其是在我的上一个公众号被关闭后&#xff0c;仍然选择跟随我的老粉丝们&#xff0c;你们的支持是我继续前行的动力。为了回馈大家长期以来的陪伴&#xff0c;我决定分享一些实用的干货&#xff0c;这些都是我亲身实践并且取得成功的…...

Ubuntu20.04 安装jekyll

首先使根据官方文档安装&#xff1a;Jekyll on Ubuntu | Jekyll • Simple, blog-aware, static sites 如果没有报错&#xff0c;就不用再继续看下去了。 我这边在执行gem install jekyll bundler时报错&#xff0c;所以安装了rvm&#xff0c;安装rvm可以参考这篇文章Ubuntu …...

AWK语言

一. awk awk&#xff1a;报告生成器&#xff0c;格式化输出。 在 Linux/UNIX 系统中&#xff0c;awk 是一个功能强大的编辑工具&#xff0c;逐行读取输入文本&#xff0c;默认以空格或tab键作为分隔符作为分隔&#xff0c;并按模式或者条件执行编辑命令。而awk比较倾向于将一行…...

精通Nmap:网络扫描与安全的终极武器

一、引言 Nmap&#xff0c;即NetworkMapper&#xff0c;是一款开源的网络探测和安全审计工具。它能帮助您发现网络中的设备&#xff0c;并识别潜在的安全风险。在这个教程中&#xff0c;我们将一步步引导您如何有效地使用Nmap&#xff0c;让您的网络更加安全。 因为Nmap还有图…...

Java 学习和实践笔记(11)

三大神器&#xff1a; 官方网址: http://www.jetbrains.com/idea/ 官方网址: https://code.visualstudio.com/ 官方网址: http://www.eclipse.org 装好了idea社区版&#xff0c;并试运行以下代码&#xff0c;OK&#xff01; //TIP To <b>Run</b> code, press &l…...

开发实体类

开发实体类之间先在pom文件中加入该依赖 <!-- 开发实体类--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>provided</scope></dependency>我们在实体类中声明各个属…...

人工智能学习与实训笔记(十五):Scikit-learn库的基础与使用

人工智能专栏文章汇总&#xff1a;人工智能学习专栏文章汇总-CSDN博客 本篇目录 一、介绍 1. 1 Scikit-learn的发展历程及定义 1.2 理解算法包、算法库及算法框架之间的区别和联系 二、Scikit-learn官网结构 三、安装与设置 3.1 Python环境的安装与配置 3.2 Scikit-lea…...

插值与拟合算法介绍

在数据处理和科学计算领域,插值与拟合是两种极为重要的数据分析方法。它们被广泛应用于信号处理、图像处理、机器学习、金融分析等多个领域,对于理解和预测数据趋势具有至关重要的作用。本文将深入浅出地介绍这两种算法的基本原理,并结合C语言编程环境探讨如何在CSDN开发者社…...

下一代Windows系统曝光:基于GPT-4V,Agent跨应用调度,代号UFO

下一代Windows操作系统提前曝光了&#xff1f;&#xff1f; 微软首个为Windows而设的智能体&#xff08;Agent&#xff09; 亮相&#xff1a; 基于GPT-4V&#xff0c;一句话就可以在多个应用中无缝切换&#xff0c;完成复杂任务。整个过程无需人为干预&#xff0c;其执行成功…...

二.自定义头文件

一.Worker.h 1.1概述 - 类名&#xff1a;Worker - 继承关系&#xff1a;所有其他类&#xff08;Employee、Manager、Boss&#xff09;都继承自该抽象类 - 头文件保护&#xff1a;使用 pragma once 防止头文件重复包含 - 引入标准库&#xff1a;包含 <iostream> 和 <st…...

【AIGC】Stable Diffusion之模型微调工具

推荐一款好用的模型微调工具&#xff0c;cybertron furnace 是一个lora训练整合包&#xff0c;提供训练 lora 模型的工具集或环境。集成环境包括必要的依赖项和配置文件、预训练脚本&#xff0c;支持人物、二次元、画风、自定义lora的训练&#xff0c;以简化用户训练 lora 模型…...

探索未来科技前沿:深度学习的进展与应用

深度学习的进展 摘要&#xff1a;深度学习作为人工智能领域的重要分支&#xff0c;近年来取得了巨大的进展&#xff0c;并在各个领域展现出惊人的应用潜力。本文将介绍深度学习的发展历程、技术原理以及在图像识别、自然语言处理等领域的应用&#xff0c;展望深度学习在未来的…...

PTA | Wifi密码

下面是微博上流传的一张照片&#xff1a;“各位亲爱的同学们&#xff0c;鉴于大家有时需要使用 wifi&#xff0c;又怕耽误亲们的学习&#xff0c;现将 wifi 密码设置为下列数学题答案&#xff1a;A-1&#xff1b;B-2&#xff1b;C-3&#xff1b;D-4&#xff1b;请同学们自己作答…...

Linux中gdb使用说明书

首先我们要使用gdb&#xff0c;必须明白gdb使用范围&#xff1a; 要使用gdb调试&#xff0c;必须在源代码生成二进制程序的时候, 加上 -g 选项&#xff08;gcc/g) 其次&#xff0c;我们就要来学习gdb使用的一些命令了&#xff1a; list&#xff0f;l 行号&#xff1a;显…...

LInux——开发工具的使用

目录 Linux软件包管理器 yum rzsz Linux编辑器——vim vim的使用 vim的基本操作 命令模式的常见命令 底行模式的常见命令 vim是需要配置的 Linux编译器——gcc/g 预处理 编译 汇编 链接 函数库 Linux项目自动化构建工具 make/makefile make原理 项目清理 Linux调试器g…...

沁恒CH32V30X学习笔记03--64位systick

systick CH32F2x 系列产品Cortex-M3 内核自带了一个 24 位自减型计数器(SysTick timer)。支持 HCLK 或 HCLK/8 作为时基,具有较高优先级别(6)。一般可用于操作系统的时基。 CH32V3x 系列产品内核自带了一个 64 位加减计数器(SysTick),支持 HCLK 或者 HCLK/8 作为时基,…...

【JavaEE】IP协议

作者主页&#xff1a;paper jie_博客 本文作者&#xff1a;大家好&#xff0c;我是paper jie&#xff0c;感谢你阅读本文&#xff0c;欢迎一建三连哦。 本文于《JavaEE》专栏&#xff0c;本专栏是针对于大学生&#xff0c;编程小白精心打造的。笔者用重金(时间和精力)打造&…...

计算机网络-数据通信基础

目录 前言 一、数据通信基本概念 二、数据通信相关知识1 总结 前言 正在学习计算机网络体系&#xff0c;把每日所学的知识梳理出来&#xff0c;既能够当作读书笔记&#xff0c;又能分享出来和大家一同学习讨论。 一、数据通信基本概念 基本概念&#xff1a;信源、信道、信宿&…...

【lesson53】线程控制

文章目录 线程控制 线程控制 线程创建 代码&#xff1a; 运行代码&#xff1a; 强调一点&#xff0c;线程和进程不一样&#xff0c;进程有父进程的概念&#xff0c;但在线程组里面&#xff0c;所有的线程都是对等关系。 错误检查: 传统的一些函数是&#xff0c;成功返回0&…...

TypeScript(一):TypeScript基本理解

TypeScript基本理解 为什么使用TS JavaScript发展至今&#xff0c;没有进行数据类型的验证而我们知道&#xff0c;在编程阶段&#xff0c;错误发现的越早越好而TS就解决了JS的这个问题 认识TypeScript TypeScript是拥有类型的JavaScript超级&#xff0c;它可以编译成普通、…...

C语言—指针

碎碎念:做指针题的时候我仿佛回到了原点&#xff0c;总觉得目的是为了把框架搭建起来&#xff0c;我胡说的哈31 1.利用指针变量将一个数组中的数据反向输出。 /*1.利用指针变量将一个数组中的数据反向输出。*/#include <stdio.h> #include <time.h> #include <…...

c++作业

Shell中的函数&#xff08;先调用后使用的原则&#xff09;&#xff08;没有申明&#xff09; &#xff08;Function&#xff09; 函数名&#xff08;有没有参数根据调用格式&#xff09;&#xff08;不能写任何内容&#xff09; { 函数体 Return 返回值 } 函数名 ----》…...

什么是tomcat?tomcat是干什么用的?

前言 Tomcat是一个开源的、轻量级的应用服务器&#xff0c;是Apache软件基金会的一个项目。它实现了Java Servlet、JavaServer Pages&#xff08;JSP&#xff09;和Java Expression Language&#xff08;EL&#xff09;等Java技术&#xff0c;用于支持在Java平台上运行的动态W…...

中科院一区论文复现,改进蜣螂算法,Fuch映射+反向学习+自适应步长+随机差分变异,MATLAB代码...

本期文章复现一篇发表于2024年来自中科院一区TOP顶刊《Energy》的改进蜣螂算法。 论文引用如下&#xff1a; Li Y, Sun K, Yao Q, et al. A dual-optimization wind speed forecasting model based on deep learning and improved dung beetle optimization algorithm[J]. Ener…...

C# 如何实现一个事件总线

EventBus&#xff08;事件总线&#xff09;是一种用于在应用程序内部或跨应用程序组件之间进行事件通信的机制。 它允许不同的组件通过发布和订阅事件来进行解耦和通信。在给定的代码片段中&#xff0c;我们可以看到一个使用C#实现的Event Bus。它定义了一些接口和类来实现事件…...

Python学习路线图

防止忘记&#xff0c;温故知新 进阶路线...

作业2.14

chgrp: 只能修改文件的所属组 chgrp 新的组 文件名 要求&#xff1a;修改的目标组已经存在 chown: chown 新的用户名 文件名 sudo chown root &#xff1a;1 将文件1的所属组用户和所属组用户都改为root sudo chown root&#xff1a;ubuntu 1 将文件1的所属用户…...

基于python+django+mysql的小区物业管理系统

该系统是基于pythondjango开发的小区物业管理系统。适用场景&#xff1a;大学生、课程作业、毕业设计。学习过程中&#xff0c;如遇问题可以在github给作者留言。主要功能有&#xff1a;业主管理、报修管理、停车管理、资产管理、小区管理、用户管理、日志管理、系统信息。 演示…...

控制与状态机算法

控制与状态机算法是计算机科学、电子工程和自动化领域中常用的一种设计工具,它用来描述一个系统的行为,该系统在不同时间点可以处于不同的状态,并且其行为取决于当前状态以及输入的信号或事件。状态机算法的核心概念包括: 状态(State):系统的任何可能配置。每个状态代表…...

sql常用语句小结

创建表&#xff1a; create table 表名&#xff08; 字段1 字段类型 【约束】【comment 字段1注释】&#xff0c; //【】里面的东西可以不用加上去 字段2 字段类型 【约束】【comment 字段2注释】 &#xff09;【comment 表注释】 约束&#xff1a;作用于表中字段上的规则…...

云计算基础-虚拟机迁移原理

什么是虚拟机迁移 虚拟机迁移是指将正在运行的虚拟机实例从一个物理服务器&#xff08;或主机&#xff09;迁移到另一个物理服务器&#xff08;或主机&#xff09;的过程&#xff0c;而不会中断虚拟机的运行。 虚拟机拟机迁移分类虚 热迁移&#xff1a;开机状态下迁移 冷迁…...

云计算基础-云计算概念

云计算定义 云计算是一种基于互联网的计算方式&#xff0c;通过这种计算方式&#xff0c;共享的软硬件资源和信息可以按需提供给计算机和其他设备。云计算依赖资源共享以达成规模经济&#xff0c;类似基础设置(如电力网)。 云计算最基本的概念就是云加端&#xff0c;我们有一个…...

如何将阿里云服务器迁移

&#x1f4d1;前言 本文主要是如何将阿里云服务器迁移实现数据转移的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️** &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是青衿&#x1f947; ☁️博客首页&#xff1a;CSDN主页放风讲故事 &#x1f304;每日…...

如何将本地的python项目部署到linux服务器中

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂 。 前言 本地写好的python项目&#xff0c;如何部署在服务器上运行呢&#xff1f;今天&#xff0c;我们就来抽一点点时间来看看。&#xff08;网上找的资料&#xff0c;大部分都囫囵吞枣的…...

每日五道java面试题之java基础篇(五)

目录&#xff1a; 第一题. final、finally、finalize 的区别&#xff1f;第二题. 和 equals 的区别&#xff1f;第三题.hashCode 与 equals?第四题. Java 是值传递&#xff0c;还是引⽤传递&#xff1f;第五题 深拷贝和浅拷贝&#xff1f; 第一题. final、finally、finalize 的…...

HiveSQL——用户行为路径分析

注&#xff1a;参考文档&#xff1a; SQL之用户行为路径分析--HQL面试题46【拼多多面试题】_路径分析 sql-CSDN博客文章浏览阅读2k次&#xff0c;点赞6次&#xff0c;收藏19次。目录0 问题描述1 数据分析2 小结0 问题描述已知用户行为表 tracking_log&#xff0c; 大概字段有&…...

专利的申请

申请发明或者实用新型专利的&#xff0c;应当提交请求书、说明书及其摘要和权利要求书等文件。 请求书应当写明发明或者实用新型的名称&#xff0c;发明人或者设计人的姓名&#xff0c;申请人姓名或者名称、地址&#xff0c;以及其他事项。 说明书应当对发明或者实用新型作出清…...

嵌入式学习 C++ Day5、6

嵌入式学习 C Day5、6 一、思维导图 二、作业 1.以下是一个简单的比喻&#xff0c;将多态概念与生活中的实际情况相联系&#xff1a; 比喻&#xff1a;动物园的讲解员和动物表演 想象一下你去了一家动物园&#xff0c;看到了许多不同种类的动物&#xff0c;如狮子、大象、猴…...

阿里云香港服务器cn2速度测试和租用价格表

阿里云香港服务器中国香港数据中心网络线路类型BGP多线精品&#xff0c;中国电信CN2高速网络高质量、大规格BGP带宽&#xff0c;运营商精品公网直连中国内地&#xff0c;时延更低&#xff0c;优化海外回中国内地流量的公网线路&#xff0c;可以提高国际业务访问质量。阿里云服务…...

《学成在线》微服务实战项目实操笔记系列(P92~P120)【下】

史上最详细《学成在线》项目实操笔记系列【下】&#xff0c;跟视频的每一P对应&#xff0c;全系列18万字&#xff0c;涵盖详细步骤与问题的解决方案。如果你操作到某一步卡壳&#xff0c;参考这篇&#xff0c;相信会带给你极大启发。 四、课程发布模块 4.1 (课程发布)模块需求…...

php数据类型以及运算符、判断条件

php数据类型以及运算符 1. php数据类型2. 使用举例3. 运算符4. 判断条件if else elseif 1. php数据类型 包括 String(字符串)、Integer(整型)、Float(浮点型)、Boolean(布尔型)、Array(数组)、Object(对象)、NULL(空值) 2. 使用举例 1.字符串 2.整型 3.浮点型 4.布尔型 5.数组…...

大数据01-导论

零、文章目录 大数据01-导论 1、数据与数据分析 **数据&#xff1a;是事实或观察的结果&#xff0c;是对客观事物的逻辑归纳&#xff0c;是用于表示客观事物的未经加工的原始素材。**数据可以是连续的值&#xff0c;比如声音、图像&#xff0c;称为模拟数据&#xff1b;也可…...

智能网卡(SmartNIC):增强网络性能

在当今的数字时代&#xff0c;网络性能和数据安全是各行各业面临的关键挑战。智能网卡是一项颠覆性的技术创新&#xff0c;对增强网络性能和加强数据安全性具有关键推动作用。本文旨在探讨智能网卡的工作原理及其在不同应用场景中的重要作用。 什么是智能网卡&#xff1f; 智…...

算法刷题day14

目录 引言一、平均二、三国游戏三、松散子序列 引言 今天做了三道新题&#xff0c;类型是贪心、枚举、DP&#xff0c;不是特别难&#xff0c;但是努力一下刚好能够够得上&#xff0c;还是不错的&#xff0c;只要能够一直坚持下去&#xff0c;不断刷题不断总结&#xff0c;就是…...

个性签名大全

只许一生浮世清欢愿我以孤独作为铠甲&#xff0c;自此不再受伤愿我是阳光&#xff0c;明媚而不忧伤我不敢太勇敢太执着太骄傲&#xff0c;我怕失去开始你是我的天使&#xff0c;最后你是我的唯一姐的霸气&#xff0c;无人能比&#xff0c;哥的傲气&#xff0c;无人能朋唯有万事…...