Add playing indicators to SongsFragment
Add an indicator for the currently playing song to SongsFragment, as long as the current playback mode is ALL_SONGS.
This commit is contained in:
parent
8a92108a4a
commit
95c601bf02
20 changed files with 159 additions and 23 deletions
|
@ -42,6 +42,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
private val mIsShuffling = MutableLiveData(false)
|
||||
private val mLoopMode = MutableLiveData(LoopMode.NONE)
|
||||
|
||||
private val mIsInUserQueue = MutableLiveData(false)
|
||||
|
||||
// Other
|
||||
private val mIsSeeking = MutableLiveData(false)
|
||||
private var mCanAnimate = false
|
||||
|
@ -60,6 +62,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
/** The current [PlaybackMode] that also determines the queue */
|
||||
val mode: LiveData<PlaybackMode> get() = mMode
|
||||
|
||||
val isInUserQueue: LiveData<Boolean> = mIsInUserQueue
|
||||
|
||||
val isPlaying: LiveData<Boolean> get() = mIsPlaying
|
||||
val isShuffling: LiveData<Boolean> get() = mIsShuffling
|
||||
/** The current repeat mode, see [LoopMode] for more information */
|
||||
|
@ -390,4 +394,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
override fun onLoopUpdate(mode: LoopMode) {
|
||||
mLoopMode.value = mode
|
||||
}
|
||||
|
||||
override fun onInUserQueueUpdate(isInUserQueue: Boolean) {
|
||||
mIsInUserQueue.value = isInUserQueue
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,6 +86,10 @@ class PlaybackStateManager private constructor() {
|
|||
callbacks.forEach { it.onLoopUpdate(value) }
|
||||
}
|
||||
private var mIsInUserQueue = false
|
||||
set(value) {
|
||||
field = value
|
||||
callbacks.forEach { it.onInUserQueueUpdate(value) }
|
||||
}
|
||||
private var mIsRestored = false
|
||||
private var mHasPlayed = false
|
||||
private var mShuffleSeed = -1L
|
||||
|
@ -114,6 +118,8 @@ class PlaybackStateManager private constructor() {
|
|||
val isRestored: Boolean get() = mIsRestored
|
||||
/** Whether this instance has started playing or not */
|
||||
val hasPlayed: Boolean get() = mHasPlayed
|
||||
/** Whether playback is in the user queue or not */
|
||||
val isInUserQueue: Boolean get() = mIsInUserQueue
|
||||
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
|
@ -239,17 +245,16 @@ class PlaybackStateManager private constructor() {
|
|||
/**
|
||||
* Shortcut function for updating what song is being played. ***USE THIS INSTEAD OF WRITING OUT ALL THE CODE YOURSELF!!!***
|
||||
* @param song The song to play
|
||||
* @param dontPlay (Optional, defaults to false) whether to not set [isPlaying] to true.
|
||||
*/
|
||||
private fun updatePlayback(song: Song) {
|
||||
mIsInUserQueue = false
|
||||
|
||||
mSong = song
|
||||
mPosition = 0
|
||||
|
||||
if (!mIsPlaying) {
|
||||
setPlayingStatus(true)
|
||||
}
|
||||
|
||||
mIsInUserQueue = false
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -846,6 +851,7 @@ class PlaybackStateManager private constructor() {
|
|||
fun onShuffleUpdate(isShuffling: Boolean) {}
|
||||
fun onLoopUpdate(mode: LoopMode) {}
|
||||
fun onSeekConfirm(position: Long) {}
|
||||
fun onInUserQueueUpdate(isInUserQueue: Boolean) {}
|
||||
fun onRestoreFinish() {}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package org.oxycblt.auxio.recycler
|
||||
|
||||
/**
|
||||
* Interface that allows the highlighting of certain ViewHolders
|
||||
*/
|
||||
interface Highlightable {
|
||||
fun setHighlighted(isHighlighted: Boolean)
|
||||
}
|
|
@ -13,11 +13,6 @@ import org.oxycblt.auxio.music.Artist
|
|||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.recycler.viewholders.AlbumViewHolder.Companion.from
|
||||
import org.oxycblt.auxio.recycler.viewholders.ArtistViewHolder.Companion.from
|
||||
import org.oxycblt.auxio.recycler.viewholders.GenreViewHolder.Companion.from
|
||||
import org.oxycblt.auxio.recycler.viewholders.HeaderViewHolder.Companion.from
|
||||
import org.oxycblt.auxio.recycler.viewholders.SongViewHolder.Companion.from
|
||||
|
||||
/*
|
||||
* A table of all ViewHolder codes. Please add to these so that all viewholder codes are unique.
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
package org.oxycblt.auxio.songs
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.recycler.viewholders.SongViewHolder
|
||||
import org.oxycblt.auxio.recycler.Highlightable
|
||||
import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder
|
||||
import org.oxycblt.auxio.ui.accent
|
||||
import org.oxycblt.auxio.ui.setTextColorResource
|
||||
|
||||
/**
|
||||
* The adapter for [SongsFragment], shows basic songs without durations.
|
||||
|
@ -16,15 +21,61 @@ class SongsAdapter(
|
|||
private val data: List<Song>,
|
||||
private val doOnClick: (data: Song) -> Unit,
|
||||
private val doOnLongClick: (data: Song, view: View) -> Unit
|
||||
) : RecyclerView.Adapter<SongViewHolder>() {
|
||||
) : RecyclerView.Adapter<SongsAdapter.SongViewHolder>() {
|
||||
|
||||
private var currentSong: Song? = null
|
||||
private var lastHolder: Highlightable? = null
|
||||
|
||||
override fun getItemCount(): Int = data.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongViewHolder {
|
||||
return SongViewHolder.from(parent.context, doOnClick, doOnLongClick)
|
||||
return SongViewHolder(
|
||||
ItemSongBinding.inflate(LayoutInflater.from(parent.context))
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
|
||||
holder.bind(data[position])
|
||||
|
||||
if (currentSong != null) {
|
||||
if (data[position].id == currentSong?.id) {
|
||||
// Reset the last ViewHolder before assigning the new, correct one to be highlighted
|
||||
lastHolder?.setHighlighted(false)
|
||||
lastHolder = holder
|
||||
holder.setHighlighted(true)
|
||||
} else {
|
||||
holder.setHighlighted(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setCurrentSong(song: Song?) {
|
||||
// Clear out the last ViewHolder as a song update usually signifies that this current
|
||||
// ViewHolder is likely invalid.
|
||||
lastHolder?.setHighlighted(false)
|
||||
lastHolder = null
|
||||
|
||||
currentSong = song
|
||||
}
|
||||
|
||||
inner class SongViewHolder(
|
||||
private val binding: ItemSongBinding
|
||||
) : BaseViewHolder<Song>(binding, doOnClick, doOnLongClick), Highlightable {
|
||||
private val normalTextColor = binding.songName.currentTextColor
|
||||
|
||||
override fun onBind(data: Song) {
|
||||
binding.song = data
|
||||
|
||||
binding.songName.requestLayout()
|
||||
binding.songInfo.requestLayout()
|
||||
}
|
||||
|
||||
override fun setHighlighted(isHighlighted: Boolean) {
|
||||
if (isHighlighted) {
|
||||
binding.songName.setTextColorResource(accent.first)
|
||||
} else {
|
||||
binding.songName.setTextColor(normalTextColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ import org.oxycblt.auxio.detail.DetailViewModel
|
|||
import org.oxycblt.auxio.logD
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
||||
import org.oxycblt.auxio.recycler.Highlightable
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import org.oxycblt.auxio.ui.accent
|
||||
import org.oxycblt.auxio.ui.getLandscapeSpans
|
||||
|
@ -57,7 +59,9 @@ class SongsFragment : Fragment() {
|
|||
|
||||
val songAdapter = SongsAdapter(
|
||||
musicStore.songs,
|
||||
doOnClick = { playbackModel.playSong(it, settingsManager.songPlaybackMode) },
|
||||
doOnClick = {
|
||||
playbackModel.playSong(it, settingsManager.songPlaybackMode)
|
||||
},
|
||||
doOnLongClick = { data, view ->
|
||||
PopupMenu(requireContext(), view).setupSongActions(
|
||||
requireContext(), data, playbackModel, detailModel
|
||||
|
@ -65,6 +69,8 @@ class SongsFragment : Fragment() {
|
|||
}
|
||||
)
|
||||
|
||||
var lastHolder: Highlightable? = null
|
||||
|
||||
// --- UI SETUP ---
|
||||
|
||||
binding.songToolbar.apply {
|
||||
|
@ -97,6 +103,40 @@ class SongsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
playbackModel.song.observe(viewLifecycleOwner) { song ->
|
||||
if (playbackModel.mode.value == PlaybackMode.ALL_SONGS) {
|
||||
logD(playbackModel.isInUserQueue.toString())
|
||||
songAdapter.setCurrentSong(song)
|
||||
|
||||
lastHolder?.setHighlighted(false)
|
||||
lastHolder = null
|
||||
|
||||
if (song != null) {
|
||||
val pos = musicStore.songs.indexOfFirst { it.id == song.id }
|
||||
|
||||
// Check if the ViewHolder for this song is visible, if it is then highlight it.
|
||||
// If it isn't, SongsAdapter will take care of it when it is visible.
|
||||
binding.songRecycler.layoutManager?.findViewByPosition(pos)?.let { child ->
|
||||
binding.songRecycler.getChildViewHolder(child)?.let {
|
||||
lastHolder = it as Highlightable
|
||||
|
||||
lastHolder?.setHighlighted(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
playbackModel.isInUserQueue.observe(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
songAdapter.setCurrentSong(null)
|
||||
lastHolder?.setHighlighted(false)
|
||||
lastHolder = null
|
||||
}
|
||||
}
|
||||
|
||||
setupFastScroller(binding)
|
||||
|
||||
logD("Fragment created.")
|
||||
|
|
|
@ -9,8 +9,11 @@ import android.text.Spanned
|
|||
import android.text.style.ForegroundColorSpan
|
||||
import android.view.MenuItem
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.MenuRes
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.text.HtmlCompat
|
||||
|
@ -81,6 +84,20 @@ fun Spanned.render(): Spanned {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a [TextView] text color, without having to resolve the resource.
|
||||
*/
|
||||
fun TextView.setTextColorResource(@ColorRes color: Int) {
|
||||
setTextColor(color.toColor(context))
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a [TextView] text color, using an attr resource
|
||||
*/
|
||||
fun TextView.setTextColorAttr(@AttrRes attr: Int) {
|
||||
setTextColor(resolveAttr(context, attr))
|
||||
}
|
||||
|
||||
/**
|
||||
* Show actions for a song item, such as the ones found in [org.oxycblt.auxio.songs.SongsFragment]
|
||||
* @param context [Context] required
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".detail.adapters.ArtistDetailAdapter.ArtistHeaderViewHolder">
|
||||
|
||||
<data>
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".detail.adapters.GenreDetailAdapter.GenreHeaderViewHolder">
|
||||
|
||||
<data>
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".settings.ui.AboutDialog">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".settings.SettingsFragment">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -35,6 +35,8 @@
|
|||
android:id="@+id/song_fast_scroll"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:layout_marginBottom="@dimen/margin_small"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/song_toolbar" />
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".settings.ui.AccentAdapter.ViewHolder">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".detail.adapters.DetailSongAdapter.ViewHolder">
|
||||
tools:context=".detail.adapters.AlbumDetailAdapter.AlbumSongViewHolder">
|
||||
|
||||
<data>
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".detail.adapters.DetailAlbumAdapter.AlbumViewHolder">
|
||||
tools:context=".detail.adapters.ArtistDetailAdapter.ArtistAlbumViewHolder">
|
||||
|
||||
<data>
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".detail.adapters.ArtistDetailAdapter.ArtistHeaderViewHolder">
|
||||
|
||||
<data>
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".detail.adapters.GenreDetailAdapter.GenreHeaderViewHolder">
|
||||
|
||||
<data>
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".recycler.viewholders.SongViewHolder">
|
||||
tools:context=".detail.adapters.GenreDetailAdapter.GenreSongViewHolder">
|
||||
|
||||
<data>
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".recycler.viewholders.SongViewHolder">
|
||||
tools:context=".playback.queue.QueueAdapter.QueueSongViewHolder">
|
||||
|
||||
<data>
|
||||
|
||||
|
|
|
@ -175,6 +175,7 @@
|
|||
<item name="bottomSheetStyle">@style/Theme.BottomSheetHeightFix</item>
|
||||
</style>
|
||||
|
||||
<!-- Fix to make the bottomsheet go to full height instantly. -->
|
||||
<style name="Theme.BottomSheetHeightFix" parent="Widget.Design.BottomSheet.Modal">
|
||||
<item name="behavior_peekHeight">500dp</item>
|
||||
</style>
|
||||
|
|
Loading…
Reference in a new issue