今天來更新呈現的 NowPlaying 上的小條 UI,呈現播放資訊和進度,先介紹呈現播放資訊,裡面的實作還是參考 uamp。在 NowPlayingViewModel 傳入之前所實作的 MusicServiceConnection,裡面有播放歌曲的相關資訊,透過觀者的方式來取得資訊:
private val musicServiceConnection = musicServiceConnection.also {
it.playbackState.observeForever(playbackStateObserver)
it.nowPlaying.observeForever(mediaMetadataObserver)
checkPlaybackPosition()
}
private val playbackStateObserver = Observer<PlaybackStateCompat> {
playbackState = it ?: EMPTY_PLAYBACK_STATE
val metadata = musicServiceConnection.nowPlaying.value ?: NOTHING_PLAYING
updateState(playbackState, metadata)
}
private val mediaMetadataObserver = Observer<MediaMetadataCompat> {
updateState(playbackState, it)
mediaDuration = it.duration
}
分別觀察 MusicServiceConnection 內的 MediaMetadataCompat(歌曲資訊) 和 PlaybackStateCompat (播放狀態),再有變化時,就會通知 updateState,來更新 NowPlayingMetadata,這個自訂的 NowPlayingMetadata 結構會被 NowPlayingFragment 觀察,真的是各種觀察,一層一層觀察上來 XD
val mediaMetadata = MutableLiveData<NowPlayingMetadata>()
......
private fun updateState(
playbackState: PlaybackStateCompat,
mediaMetadata: MediaMetadataCompat
) {
// Only update media item once we have duration available
if (mediaMetadata.duration != 0L && mediaMetadata.id != null) {
val nowPlayingMetadata = NowPlayingMetadata(
mediaMetadata.id!!,
mediaMetadata.displayIconUri,
mediaMetadata.title?.trim(),
mediaMetadata.displaySubtitle?.trim(),
NowPlayingMetadata.timestampToMSS(context, mediaMetadata.duration)
)
this.mediaMetadata.postValue(nowPlayingMetadata)
}
}
有了上述的資訊,在 NowPlayingFragment 再觀察 mediaMetadata,將結果更新在 UI 上
nowPlayingViewModel.mediaMetadata.observe(viewLifecycleOwner,
Observer { mediaItem -> updateUI(view, mediaItem) })
更新的 function 就很一般,將要顯示的內容填入
private fun updateUI(view: View, metadata: NowPlayingViewModel.NowPlayingMetadata) {
if (metadata.albumArtUri == Uri.EMPTY) {
smallCover.setImageResource(R.drawable.ic_default_cover_icon)
smallCover.setBackgroundResource(R.drawable.ic_default_cover_background)
} else {
Glide.with(view)
.load(metadata.albumArtUri)
.into(smallCover)
}
smallTitle.text = metadata.title
smallSubTitle.text = metadata.subtitle
}
另外一個要更新的部分為播放進度條,這邊用 SeekBar 元件來顯示,SeekBar 元件在顯示上有預設 padding,因此要撐滿左右要把 padding 設定為 0。
android:paddingStart="0dp"
android:paddingEnd="0dp"
然後因為在小條 UI 上,拖拉不太方便,因此把拖拉關掉,這邊好像沒有一個 API 可以直接使用,參考網路上的做法,自己處理 Touch 事件,回傳 true,消耗掉事件。
private fun disableSeekInSmallSeekBar() {
smallSeekBar.setOnTouchListener { _, _ -> true }
}
在上述的 musicServiceConnection 內,會呼叫 checkPlaybackPosition function,就是更新播放進度使用,因為播放進度在播放中會一直有變化,因此會使用 Handler 每間隔一段時間去抓取(0.1 秒),去抓取現在的播放進度,這邊會有兩個觀察者變數,一個是 mediaPlayProgress,目前播放進度的百分比,回傳 0~100,供 SeekBar 顯示使用。另外一個是 mediaPosition,回傳目前的播放時間,之後在大的顯示頁面會使用到。
/**
* Internal function that recursively calls itself every [POSITION_UPDATE_INTERVAL_MILLIS] ms
* to check the current playback position and updates the corresponding LiveData object when it
* has changed.
*/
private fun checkPlaybackPosition(): Boolean = handler.postDelayed({
val currPosition = playbackState.currentPlayBackPosition
if (mediaPosition.value != currPosition) {
mediaPosition.postValue(currPosition)
if (mediaDuration > 0) {
val progress = ((currPosition * 100 / mediaDuration)).toInt()
mediaPlayProgress.postValue(progress.toInt())
}
}
if (updatePosition)
checkPlaybackPosition()
}, POSITION_UPDATE_INTERVAL_MILLIS)
覺得拿現在播放進度的時間在 uamp 專案內有點神奇,是需要運算出來的,不是直接拿某個欄位,裡面就會有播放時間了,這邊大家如果知道原因的話,或是有其他方式可以使用,歡迎在下面留言討論!
成果圖:
程式碼在這,分支名稱(day16_nowplaying_small_ui): Fancy/day16_nowplaying_small_ui