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 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
}
}

View file

@ -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() {}
}

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.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.

View file

@ -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)
}
}
}
}

View file

@ -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.")

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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"

View file

@ -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"

View file

@ -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" />

View file

@ -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"

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>