home: indicate playback on items [#218]

Indicate playback in the home view as well.

This is mostly a QoL change. Might also add this to the search view.
This commit is contained in:
Alexander Capehart 2022-09-01 19:27:36 -06:00
parent 5f6cdad507
commit 87ca4c8ab1
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
13 changed files with 227 additions and 93 deletions

View file

@ -257,10 +257,10 @@ class AlbumDetailFragment :
}
if (parent is Album && parent.id == unlikelyToBeNull(detailModel.currentAlbum.value).id) {
detailAdapter.highlightSong(song)
detailAdapter.activateSong(song)
} else {
// Clear the ViewHolders if the mode isn't ALL_SONGS
detailAdapter.highlightSong(null)
detailAdapter.activateSong(null)
}
}

View file

@ -202,18 +202,18 @@ class ArtistDetailFragment :
private fun updatePlayback(song: Song?, parent: MusicParent?) {
if (parent is Artist && parent.id == unlikelyToBeNull(detailModel.currentArtist.value).id) {
detailAdapter.highlightSong(song)
detailAdapter.activateSong(song)
} else {
// Clear the ViewHolders if the given song is not part of this artist.
detailAdapter.highlightSong(null)
// Ignore song playback not from the artist
detailAdapter.activateSong(null)
}
if (parent is Album &&
parent.artist.id == unlikelyToBeNull(detailModel.currentArtist.value).id) {
detailAdapter.highlightAlbum(parent)
detailAdapter.activateAlbum(parent)
} else {
// Clear out the album viewholder if the parent is not an album.
detailAdapter.highlightAlbum(null)
// Ignore album playback not from the artist
detailAdapter.activateAlbum(null)
}
}
}

View file

@ -83,7 +83,7 @@ class GenreDetailFragment :
binding.detailRecycler.apply {
adapter = detailAdapter
setSpanSizeLookup { pos ->
val item = detailModel.albumData.value[pos]
val item = detailModel.genreData.value[pos]
item is Genre || item is Header || item is SortHeader
}
}
@ -195,9 +195,10 @@ class GenreDetailFragment :
private fun updatePlayback(song: Song?, parent: MusicParent?) {
if (parent is Genre && parent.id == unlikelyToBeNull(detailModel.currentGenre.value).id) {
detailAdapter.highlightSong(song)
detailAdapter.activateSong(song)
} else {
detailAdapter.highlightSong(null)
// Ignore song playback not from the genre
detailAdapter.activateSong(null)
}
}
}

View file

@ -64,25 +64,28 @@ class AlbumDetailAdapter(private val listener: Listener) :
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int,
payload: List<Any>
payloads: List<Any>
) {
if (payload.isEmpty()) {
super.onBindViewHolder(holder, position, payloads)
if (payloads.isEmpty()) {
when (val item = differ.currentList[position]) {
is Album -> (holder as AlbumDetailViewHolder).bind(item, listener)
is DiscHeader -> (holder as DiscHeaderViewHolder).bind(item)
is Song -> (holder as AlbumSongViewHolder).bind(item, listener)
}
}
super.onBindViewHolder(holder, position, payload)
}
override fun shouldHighlightViewHolder(item: Item) = item is Song && item.id == currentSong?.id
override fun shouldActivateViewHolder(position: Int): Boolean {
val item = differ.currentList[position]
return item is Song && item.id == currentSong?.id
}
/** Update the [song] that this adapter should highlight */
fun highlightSong(song: Song?) {
/** Update the [song] that this adapter should indicate playback */
fun activateSong(song: Song?) {
if (song == currentSong) return
highlightImpl(currentSong, song)
activateImpl(differ.currentList, currentSong, song)
currentSong = song
}

View file

@ -66,34 +66,36 @@ class ArtistDetailAdapter(private val listener: Listener) :
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int,
payload: List<Any>
payloads: List<Any>
) {
if (payload.isEmpty()) {
super.onBindViewHolder(holder, position, payloads)
if (payloads.isEmpty()) {
when (val item = differ.currentList[position]) {
is Artist -> (holder as ArtistDetailViewHolder).bind(item, listener)
is Album -> (holder as ArtistAlbumViewHolder).bind(item, listener)
is Song -> (holder as ArtistSongViewHolder).bind(item, listener)
}
}
super.onBindViewHolder(holder, position, payload)
}
override fun shouldHighlightViewHolder(item: Item) =
(item is Album && item.id == currentAlbum?.id) ||
override fun shouldActivateViewHolder(position: Int): Boolean {
val item = differ.currentList[position]
return (item is Album && item.id == currentAlbum?.id) ||
(item is Song && item.id == currentSong?.id)
}
/** Update the current [album] that this adapter should highlight */
fun highlightAlbum(album: Album?) {
/** Update the [album] that this adapter should indicate playback */
fun activateAlbum(album: Album?) {
if (album == currentAlbum) return
highlightImpl(currentAlbum, album)
activateImpl(differ.currentList, currentAlbum, album)
currentAlbum = album
}
/** Update the [song] that this adapter should highlight */
fun highlightSong(song: Song?) {
/** Update the [song] that this adapter should indicate playback */
fun activateSong(song: Song?) {
if (song == currentSong) return
highlightImpl(currentSong, song)
activateImpl(differ.currentList, currentSong, song)
currentSong = song
}

View file

@ -26,6 +26,7 @@ import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.databinding.ItemSortHeaderBinding
import org.oxycblt.auxio.detail.SortHeader
import org.oxycblt.auxio.ui.recycler.ActivationAdapter
import org.oxycblt.auxio.ui.recycler.Header
import org.oxycblt.auxio.ui.recycler.HeaderViewHolder
import org.oxycblt.auxio.ui.recycler.Item
@ -33,12 +34,11 @@ import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logW
abstract class DetailAdapter<L : DetailAdapter.Listener>(
private val listener: L,
diffCallback: DiffUtil.ItemCallback<Item>
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
) : ActivationAdapter<RecyclerView.ViewHolder>() {
@Suppress("LeakingThis") override fun getItemCount() = differ.currentList.size
override fun getItemViewType(position: Int) =
@ -61,58 +61,27 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int,
payload: List<Any>
payloads: List<Any>
) {
val item = differ.currentList[position]
if (payload.isEmpty()) {
if (payloads.isEmpty()) {
when (item) {
is Header -> (holder as HeaderViewHolder).bind(item)
is SortHeader -> (holder as SortHeaderViewHolder).bind(item, listener)
}
}
holder.itemView.isActivated = shouldHighlightViewHolder(item)
super.onBindViewHolder(holder, position, payloads)
}
protected val differ = AsyncListDiffer(this, diffCallback)
protected abstract fun shouldHighlightViewHolder(item: Item): Boolean
protected inline fun <reified T : Item> highlightImpl(oldItem: T?, newItem: T?) {
if (oldItem != null) {
val pos = differ.currentList.indexOfFirst { item -> item.id == oldItem.id && item is T }
if (pos > -1) {
notifyItemChanged(pos, PAYLOAD_HIGHLIGHT_CHANGED)
} else {
logW("oldItem was not in adapter data")
}
}
if (newItem != null) {
val pos = differ.currentList.indexOfFirst { item -> item is T && item.id == newItem.id }
if (pos > -1) {
notifyItemChanged(pos, PAYLOAD_HIGHLIGHT_CHANGED)
} else {
logW("newItem was not in adapter data")
}
}
}
fun submitList(list: List<Item>) {
differ.submitList(list)
}
companion object {
// This payload value serves two purposes:
// 1. It disables animations from notifyItemChanged, which looks bad when highlighting
// ViewHolders.
// 2. It instructs adapters to avoid re-binding information, and instead simply
// change the highlight state.
val PAYLOAD_HIGHLIGHT_CHANGED = Any()
val DIFFER =
object : SimpleItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {

View file

@ -58,24 +58,27 @@ class GenreDetailAdapter(private val listener: Listener) :
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int,
payload: List<Any>
payloads: List<Any>
) {
if (payload.isEmpty()) {
super.onBindViewHolder(holder, position, payloads)
if (payloads.isEmpty()) {
when (val item = differ.currentList[position]) {
is Genre -> (holder as GenreDetailViewHolder).bind(item, listener)
is Song -> (holder as SongViewHolder).bind(item, listener)
}
}
super.onBindViewHolder(holder, position, payload)
}
override fun shouldHighlightViewHolder(item: Item) = item is Song && item.id == currentSong?.id
override fun shouldActivateViewHolder(position: Int): Boolean {
val item = differ.currentList[position]
return item is Song && item.id == currentSong?.id
}
/** Update the [song] that this adapter should highlight */
fun highlightSong(song: Song?) {
/** Update the [song] that this adapter should indicate playback */
fun activateSong(song: Song?) {
if (song == currentSong) return
highlightImpl(currentSong, song)
activateImpl(differ.currentList, currentSong, song)
currentSong = song
}

View file

@ -21,14 +21,15 @@ import android.os.Bundle
import android.text.format.DateUtils
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import java.util.*
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.recycler.ActivationAdapter
import org.oxycblt.auxio.ui.recycler.AlbumViewHolder
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
@ -55,6 +56,7 @@ class AlbumListFragment : HomeListFragment<Album>() {
}
collectImmediately(homeModel.albums, homeAdapter::replaceList)
collectImmediately(playbackModel.parent, ::handleParent)
}
override fun getPopup(pos: Int): String? {
@ -107,21 +109,47 @@ class AlbumListFragment : HomeListFragment<Album>() {
}
}
private fun handleParent(parent: MusicParent?) {
if (parent is Album) {
homeAdapter.activateAlbum(parent)
} else {
// Ignore playback not from albums
homeAdapter.activateAlbum(null)
}
}
private class AlbumAdapter(private val listener: MenuItemListener) :
RecyclerView.Adapter<AlbumViewHolder>() {
ActivationAdapter<AlbumViewHolder>() {
private val differ = SyncListDiffer(this, AlbumViewHolder.DIFFER)
private var currentAlbum: Album? = null
override fun getItemCount() = differ.currentList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
AlbumViewHolder.new(parent)
override fun onBindViewHolder(holder: AlbumViewHolder, position: Int) {
holder.bind(differ.currentList[position], listener)
override fun onBindViewHolder(holder: AlbumViewHolder, position: Int, payloads: List<Any>) {
super.onBindViewHolder(holder, position, payloads)
if (payloads.isEmpty()) {
holder.bind(differ.currentList[position], listener)
}
}
override fun shouldActivateViewHolder(position: Int): Boolean {
val item = differ.currentList[position]
return item.id == currentAlbum?.id
}
fun replaceList(newList: List<Album>) {
differ.replaceList(newList)
}
/** Update the [album] that this adapter should indicate playback */
fun activateAlbum(album: Album?) {
if (album == currentAlbum) return
activateImpl(differ.currentList, currentAlbum, album)
currentAlbum = album
}
}
}

View file

@ -20,13 +20,14 @@ package org.oxycblt.auxio.home.list
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.recycler.ActivationAdapter
import org.oxycblt.auxio.ui.recycler.ArtistViewHolder
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
@ -50,6 +51,7 @@ class ArtistListFragment : HomeListFragment<Artist>() {
}
collectImmediately(homeModel.artists, homeAdapter::replaceList)
collectImmediately(playbackModel.parent, ::handleParent)
}
override fun getPopup(pos: Int): String? {
@ -83,21 +85,51 @@ class ArtistListFragment : HomeListFragment<Artist>() {
}
}
private fun handleParent(parent: MusicParent?) {
if (parent is Artist) {
homeAdapter.activateArtist(parent)
} else {
// Ignore playback not from artists
homeAdapter.activateArtist(null)
}
}
private class ArtistAdapter(private val listener: MenuItemListener) :
RecyclerView.Adapter<ArtistViewHolder>() {
ActivationAdapter<ArtistViewHolder>() {
private val differ = SyncListDiffer(this, ArtistViewHolder.DIFFER)
private var currentArtist: Artist? = null
override fun getItemCount() = differ.currentList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ArtistViewHolder.new(parent)
override fun onBindViewHolder(holder: ArtistViewHolder, position: Int) {
holder.bind(differ.currentList[position], listener)
override fun onBindViewHolder(
holder: ArtistViewHolder,
position: Int,
payloads: List<Any>
) {
super.onBindViewHolder(holder, position, payloads)
if (payloads.isEmpty()) {
holder.bind(differ.currentList[position], listener)
}
}
override fun shouldActivateViewHolder(position: Int): Boolean {
val item = differ.currentList[position]
return item.id == currentArtist?.id
}
fun replaceList(newList: List<Artist>) {
differ.replaceList(newList)
}
/** Update the [artist] that this adapter should indicate playback */
fun activateArtist(artist: Artist?) {
if (artist == currentArtist) return
activateImpl(differ.currentList, currentArtist, artist)
currentArtist = artist
}
}
}

View file

@ -20,13 +20,14 @@ package org.oxycblt.auxio.home.list
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.recycler.ActivationAdapter
import org.oxycblt.auxio.ui.recycler.GenreViewHolder
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
@ -50,6 +51,7 @@ class GenreListFragment : HomeListFragment<Genre>() {
}
collectImmediately(homeModel.genres, homeAdapter::replaceList)
collectImmediately(playbackModel.parent, ::handlePlayback)
}
override fun getPopup(pos: Int): String? {
@ -83,21 +85,47 @@ class GenreListFragment : HomeListFragment<Genre>() {
}
}
private fun handlePlayback(parent: MusicParent?) {
if (parent is Genre) {
homeAdapter.activateGenre(parent)
} else {
// Ignore playback not from genres
homeAdapter.activateGenre(null)
}
}
private class GenreAdapter(private val listener: MenuItemListener) :
RecyclerView.Adapter<GenreViewHolder>() {
ActivationAdapter<GenreViewHolder>() {
private val differ = SyncListDiffer(this, GenreViewHolder.DIFFER)
private var currentGenre: Genre? = null
override fun getItemCount() = differ.currentList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
GenreViewHolder.new(parent)
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
holder.bind(differ.currentList[position], listener)
override fun onBindViewHolder(holder: GenreViewHolder, position: Int, payloads: List<Any>) {
super.onBindViewHolder(holder, position, payloads)
if (payloads.isEmpty()) {
holder.bind(differ.currentList[position], listener)
}
}
override fun shouldActivateViewHolder(position: Int): Boolean {
val item = differ.currentList[position]
return item.id == currentGenre?.id
}
fun replaceList(newList: List<Genre>) {
differ.replaceList(newList)
}
/** Update the [genre] that this adapter should indicate playback */
fun activateGenre(genre: Genre?) {
if (genre == currentGenre) return
activateImpl(differ.currentList, currentGenre, genre)
currentGenre = genre
}
}
}

View file

@ -21,14 +21,15 @@ import android.os.Bundle
import android.text.format.DateUtils
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import java.util.Formatter
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.recycler.ActivationAdapter
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.SongViewHolder
@ -57,6 +58,7 @@ class SongListFragment : HomeListFragment<Song>() {
}
collectImmediately(homeModel.songs, homeAdapter::replaceList)
collectImmediately(playbackModel.song, playbackModel.parent, ::handlePlayback)
}
override fun getPopup(pos: Int): String? {
@ -111,21 +113,47 @@ class SongListFragment : HomeListFragment<Song>() {
}
}
private fun handlePlayback(song: Song?, parent: MusicParent?) {
if (parent == null) {
homeAdapter.activateSong(song)
} else {
// Ignore playback that is not from all songs
homeAdapter.activateSong(null)
}
}
private class SongAdapter(private val listener: MenuItemListener) :
RecyclerView.Adapter<SongViewHolder>() {
ActivationAdapter<SongViewHolder>() {
private val differ = SyncListDiffer(this, SongViewHolder.DIFFER)
private var currentSong: Song? = null
override fun getItemCount() = differ.currentList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
SongViewHolder.new(parent)
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
holder.bind(differ.currentList[position], listener)
override fun onBindViewHolder(holder: SongViewHolder, position: Int, payloads: List<Any>) {
super.onBindViewHolder(holder, position, payloads)
if (payloads.isEmpty()) {
holder.bind(differ.currentList[position], listener)
}
}
override fun shouldActivateViewHolder(position: Int): Boolean {
val item = differ.currentList[position]
return item.id == currentSong?.id
}
fun replaceList(newList: List<Song>) {
differ.replaceList(newList)
}
/** Update the [song] that this adapter should indicate playback */
fun activateSong(song: Song?) {
if (song == currentSong) return
activateImpl(differ.currentList, currentSong, song)
currentSong = song
}
}
}

View file

@ -23,6 +23,7 @@ import androidx.recyclerview.widget.AdapterListUpdateCallback
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.util.logW
/**
* The base for all items in Auxio. Any datatype can derive this type and gain some behavior not
@ -171,3 +172,43 @@ abstract class SimpleItemCallback<T : Item> : DiffUtil.ItemCallback<T>() {
return oldItem.id == newItem.id
}
}
abstract class ActivationAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
override fun onBindViewHolder(holder: VH, position: Int) = throw UnsupportedOperationException()
override fun onBindViewHolder(holder: VH, position: Int, payloads: List<Any>) {
holder.itemView.isActivated = shouldActivateViewHolder(position)
}
protected abstract fun shouldActivateViewHolder(position: Int): Boolean
protected inline fun <reified T : Item> activateImpl(
currentList: List<Item>,
oldItem: T?,
newItem: T?
) {
if (oldItem != null) {
val pos = currentList.indexOfFirst { item -> item.id == oldItem.id && item is T }
if (pos > -1) {
notifyItemChanged(pos, PAYLOAD_ACTIVATION_CHANGED)
} else {
logW("oldItem was not in adapter data")
}
}
if (newItem != null) {
val pos = currentList.indexOfFirst { item -> item is T && item.id == newItem.id }
if (pos > -1) {
notifyItemChanged(pos, PAYLOAD_ACTIVATION_CHANGED)
} else {
logW("newItem was not in adapter data")
}
}
}
companion object {
val PAYLOAD_ACTIVATION_CHANGED = Any()
}
}

View file

@ -213,8 +213,7 @@ list similar to this:
`Item being displayed | Header Item | Child Item | Child Item | Child Item...`
Note that the actual dataset used is more complex once sorting and disc numbers is taken into
account. Item highlighting and certain shared ViewHolders are already managed by the `DetailAdapter`
super-class, which should be implemented by all adapters in the detail UI.
account.
#### `.home`
This package contains the components for the "home" UI in Auxio, or the UI that the user first sees