Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ object EqualizerHelper {

fun isEnabled(): Boolean = enabled

/** 均衡器是否已成功初始化(音频会话有效) */
fun isInitialized(): Boolean = equalizer != null

/** 应用预设 */
fun applyPreset(preset: Preset) {
val eq = equalizer ?: return
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/java/com/puremusicplayer/player/PlayerManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
/** 可视化数据槽:波形原始数据(供波形样式使用) */
Expand Down Expand Up @@ -73,6 +81,8 @@ object PlayerManager {
duration.value = 0
lyrics.value = emptyList()
hasLyrics.value = false
pendingResumePosition = 0
resumeSongKey = null
}

/** 保存当前播放状态到持久化存储(播放列表、当前索引、进度) */
Expand All @@ -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
}
Expand Down
22 changes: 19 additions & 3 deletions app/src/main/java/com/puremusicplayer/player/PlayerService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down
43 changes: 38 additions & 5 deletions app/src/main/java/com/puremusicplayer/ui/LibraryFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ class LibraryFragment : Fragment() {
private var lastLoadedTreeUri: String? = "<<init>>"
/** 防止重复扫描 */
private var isScanning = false
/** 当前展示的歌曲/收藏列表缓存,供「滚动到正在播放」定位使用 */
private var shownSongsCache = emptyList<Song>()
private var shownFavsCache = emptyList<Song>()

private val requestPermission = registerForActivityResult(
ActivityResultContracts.RequestPermission()
Expand Down Expand Up @@ -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())
}
Expand Down Expand Up @@ -153,10 +164,10 @@ class LibraryFragment : Fragment() {
private fun applySongs(songs: List<Song>) {
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 }
Expand Down Expand Up @@ -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 }
Expand All @@ -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) ||
Expand Down