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:
OxygenCobalt 2020-12-30 10:48:35 -07:00
parent 8a92108a4a
commit 95c601bf02
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
20 changed files with 159 additions and 23 deletions

View file

@ -42,6 +42,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
private val mIsShuffling = MutableLiveData(false) private val mIsShuffling = MutableLiveData(false)
private val mLoopMode = MutableLiveData(LoopMode.NONE) private val mLoopMode = MutableLiveData(LoopMode.NONE)
private val mIsInUserQueue = MutableLiveData(false)
// Other // Other
private val mIsSeeking = MutableLiveData(false) private val mIsSeeking = MutableLiveData(false)
private var mCanAnimate = false private var mCanAnimate = false
@ -60,6 +62,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
/** The current [PlaybackMode] that also determines the queue */ /** The current [PlaybackMode] that also determines the queue */
val mode: LiveData<PlaybackMode> get() = mMode val mode: LiveData<PlaybackMode> get() = mMode
val isInUserQueue: LiveData<Boolean> = mIsInUserQueue
val isPlaying: LiveData<Boolean> get() = mIsPlaying val isPlaying: LiveData<Boolean> get() = mIsPlaying
val isShuffling: LiveData<Boolean> get() = mIsShuffling val isShuffling: LiveData<Boolean> get() = mIsShuffling
/** The current repeat mode, see [LoopMode] for more information */ /** The current repeat mode, see [LoopMode] for more information */
@ -390,4 +394,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
override fun onLoopUpdate(mode: LoopMode) { override fun onLoopUpdate(mode: LoopMode) {
mLoopMode.value = mode mLoopMode.value = mode
} }
override fun onInUserQueueUpdate(isInUserQueue: Boolean) {
mIsInUserQueue.value = isInUserQueue
}
} }

View file

@ -86,6 +86,10 @@ class PlaybackStateManager private constructor() {
callbacks.forEach { it.onLoopUpdate(value) } callbacks.forEach { it.onLoopUpdate(value) }
} }
private var mIsInUserQueue = false private var mIsInUserQueue = false
set(value) {
field = value
callbacks.forEach { it.onInUserQueueUpdate(value) }
}
private var mIsRestored = false private var mIsRestored = false
private var mHasPlayed = false private var mHasPlayed = false
private var mShuffleSeed = -1L private var mShuffleSeed = -1L
@ -114,6 +118,8 @@ class PlaybackStateManager private constructor() {
val isRestored: Boolean get() = mIsRestored val isRestored: Boolean get() = mIsRestored
/** Whether this instance has started playing or not */ /** Whether this instance has started playing or not */
val hasPlayed: Boolean get() = mHasPlayed val hasPlayed: Boolean get() = mHasPlayed
/** Whether playback is in the user queue or not */
val isInUserQueue: Boolean get() = mIsInUserQueue
private val settingsManager = SettingsManager.getInstance() 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!!!*** * 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 song The song to play
* @param dontPlay (Optional, defaults to false) whether to not set [isPlaying] to true.
*/ */
private fun updatePlayback(song: Song) { private fun updatePlayback(song: Song) {
mIsInUserQueue = false
mSong = song mSong = song
mPosition = 0 mPosition = 0
if (!mIsPlaying) { if (!mIsPlaying) {
setPlayingStatus(true) setPlayingStatus(true)
} }
mIsInUserQueue = false
} }
/** /**
@ -846,6 +851,7 @@ class PlaybackStateManager private constructor() {
fun onShuffleUpdate(isShuffling: Boolean) {} fun onShuffleUpdate(isShuffling: Boolean) {}
fun onLoopUpdate(mode: LoopMode) {} fun onLoopUpdate(mode: LoopMode) {}
fun onSeekConfirm(position: Long) {} fun onSeekConfirm(position: Long) {}
fun onInUserQueueUpdate(isInUserQueue: Boolean) {}
fun onRestoreFinish() {} fun onRestoreFinish() {}
} }

View file

@ -0,0 +1,8 @@
package org.oxycblt.auxio.recycler
/**
* Interface that allows the highlighting of certain ViewHolders
*/
interface Highlightable {
fun setHighlighted(isHighlighted: Boolean)
}

View file

@ -13,11 +13,6 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Header import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.Song 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. * A table of all ViewHolder codes. Please add to these so that all viewholder codes are unique.

View file

@ -1,10 +1,15 @@
package org.oxycblt.auxio.songs package org.oxycblt.auxio.songs
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemSongBinding
import org.oxycblt.auxio.music.Song 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. * The adapter for [SongsFragment], shows basic songs without durations.
@ -16,15 +21,61 @@ class SongsAdapter(
private val data: List<Song>, private val data: List<Song>,
private val doOnClick: (data: Song) -> Unit, private val doOnClick: (data: Song) -> Unit,
private val doOnLongClick: (data: Song, view: View) -> 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 getItemCount(): Int = data.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongViewHolder { 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) { override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
holder.bind(data[position]) 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)
}
}
} }
} }

View file

@ -20,6 +20,8 @@ import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.logD import org.oxycblt.auxio.logD
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.PlaybackViewModel 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.settings.SettingsManager
import org.oxycblt.auxio.ui.accent import org.oxycblt.auxio.ui.accent
import org.oxycblt.auxio.ui.getLandscapeSpans import org.oxycblt.auxio.ui.getLandscapeSpans
@ -57,7 +59,9 @@ class SongsFragment : Fragment() {
val songAdapter = SongsAdapter( val songAdapter = SongsAdapter(
musicStore.songs, musicStore.songs,
doOnClick = { playbackModel.playSong(it, settingsManager.songPlaybackMode) }, doOnClick = {
playbackModel.playSong(it, settingsManager.songPlaybackMode)
},
doOnLongClick = { data, view -> doOnLongClick = { data, view ->
PopupMenu(requireContext(), view).setupSongActions( PopupMenu(requireContext(), view).setupSongActions(
requireContext(), data, playbackModel, detailModel requireContext(), data, playbackModel, detailModel
@ -65,6 +69,8 @@ class SongsFragment : Fragment() {
} }
) )
var lastHolder: Highlightable? = null
// --- UI SETUP --- // --- UI SETUP ---
binding.songToolbar.apply { 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) setupFastScroller(binding)
logD("Fragment created.") logD("Fragment created.")

View file

@ -9,8 +9,11 @@ import android.text.Spanned
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.view.MenuItem import android.view.MenuItem
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.MenuRes import androidx.annotation.MenuRes
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.text.HtmlCompat 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] * Show actions for a song item, such as the ones found in [org.oxycblt.auxio.songs.SongsFragment]
* @param context [Context] required * @param context [Context] required

View file

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <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"> xmlns:tools="http://schemas.android.com/tools"
tools:context=".detail.adapters.ArtistDetailAdapter.ArtistHeaderViewHolder">
<data> <data>

View file

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <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"> xmlns:tools="http://schemas.android.com/tools"
tools:context=".detail.adapters.GenreDetailAdapter.GenreHeaderViewHolder">
<data> <data>

View file

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <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"> xmlns:tools="http://schemas.android.com/tools"
tools:context=".settings.ui.AboutDialog">
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <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 <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -35,6 +35,8 @@
android:id="@+id/song_fast_scroll" android:id="@+id/song_fast_scroll"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginTop="@dimen/margin_small"
android:layout_marginBottom="@dimen/margin_small"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/song_toolbar" /> app:layout_constraintTop_toBottomOf="@+id/song_toolbar" />

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <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 <FrameLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -2,7 +2,7 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android" <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" xmlns:tools="http://schemas.android.com/tools"
tools:context=".detail.adapters.DetailSongAdapter.ViewHolder"> tools:context=".detail.adapters.AlbumDetailAdapter.AlbumSongViewHolder">
<data> <data>

View file

@ -2,7 +2,7 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android" <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" xmlns:tools="http://schemas.android.com/tools"
tools:context=".detail.adapters.DetailAlbumAdapter.AlbumViewHolder"> tools:context=".detail.adapters.ArtistDetailAdapter.ArtistAlbumViewHolder">
<data> <data>

View file

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <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"> xmlns:tools="http://schemas.android.com/tools"
tools:context=".detail.adapters.ArtistDetailAdapter.ArtistHeaderViewHolder">
<data> <data>

View file

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <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"> xmlns:tools="http://schemas.android.com/tools"
tools:context=".detail.adapters.GenreDetailAdapter.GenreHeaderViewHolder">
<data> <data>

View file

@ -2,7 +2,7 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android" <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" xmlns:tools="http://schemas.android.com/tools"
tools:context=".recycler.viewholders.SongViewHolder"> tools:context=".detail.adapters.GenreDetailAdapter.GenreSongViewHolder">
<data> <data>

View file

@ -2,7 +2,7 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android" <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" xmlns:tools="http://schemas.android.com/tools"
tools:context=".recycler.viewholders.SongViewHolder"> tools:context=".playback.queue.QueueAdapter.QueueSongViewHolder">
<data> <data>

View file

@ -175,6 +175,7 @@
<item name="bottomSheetStyle">@style/Theme.BottomSheetHeightFix</item> <item name="bottomSheetStyle">@style/Theme.BottomSheetHeightFix</item>
</style> </style>
<!-- Fix to make the bottomsheet go to full height instantly. -->
<style name="Theme.BottomSheetHeightFix" parent="Widget.Design.BottomSheet.Modal"> <style name="Theme.BottomSheetHeightFix" parent="Widget.Design.BottomSheet.Modal">
<item name="behavior_peekHeight">500dp</item> <item name="behavior_peekHeight">500dp</item>
</style> </style>