From 08924ee516e8d8ed2b015a3aea3b27fc158b2c63 Mon Sep 17 00:00:00 2001 From: WorkBuddy Date: Sat, 11 Jul 2026 01:14:10 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=A4=B1=E8=B4=A5=20C?= =?UTF-8?q?L=E2=80=94=E2=80=94=E9=98=9F=E5=88=97=E6=8C=81=E4=B9=85?= =?UTF-8?q?=E5=8C=96=E8=A2=AB=E8=A6=86=E7=9B=96=E3=80=81=E7=BB=AD=E6=92=AD?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=E5=A4=B1=E6=95=88=E3=80=81=E5=9D=87=E8=A1=A1?= =?UTF-8?q?=E5=99=A8=E4=BC=9A=E8=AF=9D=E5=85=9C=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原失败 CL(8a8a3b9) 仅被修掉编译错误后重引入(787df56),但仍有三处运行时缺陷使 「队列持久化 / 续播」功能不可用: 1. 曲库加载会覆盖播放队列 - LibraryFragment.applySongs 原每次都 playlist.clear()+填入 allSongs+saveQueue, 把 restoreQueue 恢复的自定义队列/续播位置悄悄覆盖,导致续播落错歌曲。 - 改为:播放队列仅由「用户点歌」与「续播恢复」维护,曲库展示列表(allSongs) 与播放队列(PlayerManager.playlist)彻底解耦。 2. 续播位置被清零 - PlayerService.playAt 在 prepare 前把 progress 置 0,onPrepared 再读它, 导致「续播到上次位置」永远不触发。 - 改为:restoreQueue 写入 pendingResumePosition/resumeSongKey,仅当重播的歌曲 与恢复出的同一首一致时才 seek 到上次位置,随后清零,避免误续播到其它歌曲。 3. 均衡器会话兜底 - 若服务创建时音频会话尚未就绪(sessionId==0)导致 Equalizer 未初始化, 在 onPrepared(会话此刻一定有效)补初始化,保证均衡器功能可用。 附带借鉴 Auxio 的轻量改进:曲库「歌曲/收藏」列表高亮正在播放的歌曲, 其不可见时自动滚动到该曲。保持零额外依赖、安装包 <10MB。 --- .../puremusicplayer/player/EqualizerHelper.kt | 3 ++ .../puremusicplayer/player/PlayerManager.kt | 13 ++++++ .../puremusicplayer/player/PlayerService.kt | 22 ++++++++-- .../com/puremusicplayer/ui/LibraryFragment.kt | 43 ++++++++++++++++--- 4 files changed, 73 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/puremusicplayer/player/EqualizerHelper.kt b/app/src/main/java/com/puremusicplayer/player/EqualizerHelper.kt index ffc5e8d..15678f3 100644 --- a/app/src/main/java/com/puremusicplayer/player/EqualizerHelper.kt +++ b/app/src/main/java/com/puremusicplayer/player/EqualizerHelper.kt @@ -49,6 +49,9 @@ object EqualizerHelper { fun isEnabled(): Boolean = enabled + /** 均衡器是否已成功初始化(音频会话有效) */ + fun isInitialized(): Boolean = equalizer != null + /** 应用预设 */ fun applyPreset(preset: Preset) { val eq = equalizer ?: return diff --git a/app/src/main/java/com/puremusicplayer/player/PlayerManager.kt b/app/src/main/java/com/puremusicplayer/player/PlayerManager.kt index e152ee8..a2e6ca3 100644 --- a/app/src/main/java/com/puremusicplayer/player/PlayerManager.kt +++ b/app/src/main/java/com/puremusicplayer/player/PlayerManager.kt @@ -38,6 +38,14 @@ object PlayerManager { */ val sleepRemaining = MutableLiveData(0) + /** + * 续播位置(毫秒):由 restoreQueue 写入,仅对恢复出的同一首歌生效一次。 + * playAt 在准备完成时据此 seek,随后清零,避免误续播到其它歌曲。 + */ + var pendingResumePosition: Int = 0 + /** 续播对应的歌曲主键(Song.favKey);为空表示无需续播 */ + var resumeSongKey: String? = null + /** 可视化数据槽:由 PlayerService 推送 FFT,VisualizerView 注册接收 */ var fftSink: ((ByteArray) -> Unit)? = null /** 可视化数据槽:波形原始数据(供波形样式使用) */ @@ -73,6 +81,8 @@ object PlayerManager { duration.value = 0 lyrics.value = emptyList() hasLyrics.value = false + pendingResumePosition = 0 + resumeSongKey = null } /** 保存当前播放状态到持久化存储(播放列表、当前索引、进度) */ @@ -93,6 +103,9 @@ object PlayerManager { progress.value = state.currentPosition if (currentIndex in playlist.indices) { currentSong.value = playlist[currentIndex] + // 记录续播信息:仅当恢复出的这首歌被重新播放时才跳到上次位置 + resumeSongKey = playlist[currentIndex].favKey() + pendingResumePosition = state.currentPosition } return true } diff --git a/app/src/main/java/com/puremusicplayer/player/PlayerService.kt b/app/src/main/java/com/puremusicplayer/player/PlayerService.kt index 06a1a50..ac929c9 100644 --- a/app/src/main/java/com/puremusicplayer/player/PlayerService.kt +++ b/app/src/main/java/com/puremusicplayer/player/PlayerService.kt @@ -46,6 +46,8 @@ class PlayerService : android.app.Service() { private var notificationManager: NotificationManager? = null private val handler = Handler(Looper.getMainLooper()) private var progressTick = 0 + /** 本次播放需要续播到的位置(毫秒);0 表示从头播放 */ + private var resumePosition = 0 private var foregroundStarted = false @@ -74,16 +76,23 @@ class PlayerService : android.app.Service() { mediaPlayer = MediaPlayer().apply { setWakeMode(applicationContext, PowerManager.PARTIAL_WAKE_LOCK) setOnPreparedListener { mp -> - // 续播:回到保存的播放位置 - val savedPos = PlayerManager.progress.value ?: 0 + // 续播:回到上次保存的播放位置(由 restoreQueue 经 pendingResumePosition 传入) + val savedPos = resumePosition + resumePosition = 0 if (savedPos > 0 && savedPos < mp.duration) { mp.seekTo(savedPos) PlayerManager.progress.value = savedPos } mp.start() PlayerManager.isPlaying.value = true - PlayerManager.isPlaying.value = true PlayerManager.duration.value = mp.duration + // 若服务创建时音频会话尚未就绪导致均衡器未初始化,此时会话已有效,补初始化 + if (Prefs.equalizerEnabled && !EqualizerHelper.isInitialized()) { + EqualizerHelper.init(mp.audioSessionId) + EqualizerHelper.setEnabled(true) + val presets = EqualizerHelper.Preset.values() + EqualizerHelper.applyPreset(presets[Prefs.equalizerPreset.coerceIn(0, presets.size - 1)]) + } applyPlaybackSpeed(mp) updatePlaybackState() updateNotification() @@ -108,6 +117,13 @@ class PlayerService : android.app.Service() { if (PlayerManager.currentIndex < 0 && PlayerManager.playlist.isNotEmpty()) { PlayerManager.currentIndex = 0 } + // 仅当本次播放的歌曲与「恢复出的歌曲」一致时,才续播到上次位置 + val song = PlayerManager.playlist.getOrNull(PlayerManager.currentIndex) + resumePosition = if (song != null && song.favKey() == PlayerManager.resumeSongKey) { + PlayerManager.pendingResumePosition + } else 0 + PlayerManager.resumeSongKey = null + PlayerManager.pendingResumePosition = 0 playAt(PlayerManager.currentIndex, true) } PlayerControls.ACTION_PAUSE -> pause() diff --git a/app/src/main/java/com/puremusicplayer/ui/LibraryFragment.kt b/app/src/main/java/com/puremusicplayer/ui/LibraryFragment.kt index a085e22..fd021d7 100644 --- a/app/src/main/java/com/puremusicplayer/ui/LibraryFragment.kt +++ b/app/src/main/java/com/puremusicplayer/ui/LibraryFragment.kt @@ -45,6 +45,9 @@ class LibraryFragment : Fragment() { private var lastLoadedTreeUri: String? = "<>" /** 防止重复扫描 */ private var isScanning = false + /** 当前展示的歌曲/收藏列表缓存,供「滚动到正在播放」定位使用 */ + private var shownSongsCache = emptyList() + private var shownFavsCache = emptyList() private val requestPermission = registerForActivityResult( ActivityResultContracts.RequestPermission() @@ -89,6 +92,14 @@ class LibraryFragment : Fragment() { if (currentTab == 3) applyView() } + // 借鉴 Auxio:当前播放歌曲变化时,刷新「正在播放」高亮并滚动到该曲 + PlayerManager.currentSong.observe(viewLifecycleOwner) { + if (currentTab == 0 || currentTab == 3) { + applyView() + scrollToCurrent() + } + } + if (Permissions.hasAudioPermission(requireContext())) loadLibrary() else requestPermission.launch(Permissions.audioPermissionName()) } @@ -153,10 +164,10 @@ class LibraryFragment : Fragment() { private fun applySongs(songs: List) { allSongs = songs favoritesList = allSongs.filter { PlayerManager.favorites.value?.contains(it.favKey()) == true } - PlayerManager.playlist.clear() - PlayerManager.playlist.addAll(allSongs) + // 注意:此处不再覆盖 PlayerManager.playlist。 + // 播放队列由「用户点歌」(playFrom/playGroup) 与「续播恢复」(restoreQueue) 独立维护, + // 否则会把恢复出来的自定义队列 / 续播位置悄悄覆盖掉,导致续播落错歌曲。 PlayerManager.playMode = PlayMode.values().getOrElse(Prefs.playModeOrdinal) { PlayMode.REPEAT_ALL } - PlayerManager.saveQueue(requireContext()) showLoading(false) albums = allSongs.groupBy { it.album } @@ -195,16 +206,24 @@ class LibraryFragment : Fragment() { val shownSongs = sortSongs(filteredSongs) val shownFavs = sortSongs(filteredFavs) + // 缓存当前展示列表,供「滚动到正在播放」使用 + shownSongsCache = shownSongs + shownFavsCache = shownFavs + + // 借鉴 Auxio:在歌曲 / 收藏列表中高亮「正在播放」的那一首 + val curKey = PlayerManager.current()?.favKey() + val curInSongs = shownSongs.indexOfFirst { it.favKey() == curKey } + val curInFavs = shownFavs.indexOfFirst { it.favKey() == curKey } val (adapter, listEmpty) = when (currentTab) { - 0 -> SongAdapter(shownSongs, + 0 -> SongAdapter(shownSongs, curInSongs, onFavClick = { pos -> shownSongs.getOrNull(pos)?.let { PlayerManager.toggleFav(it.favKey()) } binding.recyclerView.adapter?.notifyItemChanged(pos) }) { playFrom(it, shownSongs) } to shownSongs.isEmpty() 1 -> GroupAdapter(filteredAlbums) { playGroup(it) } to filteredAlbums.isEmpty() 2 -> GroupAdapter(filteredArtists) { playGroup(it) } to filteredArtists.isEmpty() - 3 -> SongAdapter(shownFavs, + 3 -> SongAdapter(shownFavs, curInFavs, onFavClick = { pos -> shownFavs.getOrNull(pos)?.let { PlayerManager.toggleFav(it.favKey()) } favoritesList = allSongs.filter { PlayerManager.favorites.value?.contains(it.favKey()) == true } @@ -225,6 +244,20 @@ class LibraryFragment : Fragment() { } } + /** 当前播放歌曲变化时:刷新高亮,并在其不可见时自动滚动到(借鉴 Auxio 的「正在播放」指示) */ + private fun scrollToCurrent() { + if (currentTab != 0 && currentTab != 3) return + val curKey = PlayerManager.current()?.favKey() ?: return + val list = if (currentTab == 0) shownSongsCache else shownFavsCache + val pos = list.indexOfFirst { it.favKey() == curKey } + if (pos < 0) return + val lm = binding.recyclerView.layoutManager as? LinearLayoutManager + ?: return + val first = lm.findFirstVisibleItemPosition() + val last = lm.findLastVisibleItemPosition() + if (pos !in first..last) binding.recyclerView.smoothScrollToPosition(pos) + } + private fun matchesSong(song: Song, q: String): Boolean = song.title.lowercase().contains(q) || song.artist.lowercase().contains(q) ||