list: rework diffing abstraction
Make all adapters relying on diffing unified into a DiffAdapter superclass that can then accurately respond to the new UpdateInstructions data. UpdateInstructions is still not fully used everywhere, but will be soon.
This commit is contained in:
parent
b524beb0ac
commit
df98bb535f
32 changed files with 468 additions and 230 deletions
|
@ -9,6 +9,7 @@
|
|||
- Added ability to edit previously played or currently playing items in the queue
|
||||
- Added support for date values formatted as "YYYYMMDD"
|
||||
- Pressing the button will now clear the current selection before navigating back
|
||||
- Added support for non-standard `ARTISTS` tags
|
||||
|
||||
#### What's Fixed
|
||||
- Fixed unreliable ReplayGain adjustment application in certain situations
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<queries />
|
||||
|
||||
<application
|
||||
android:name=".AuxioApp"
|
||||
android:name=".Auxio"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_descriptor"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
|
|
|
@ -38,7 +38,7 @@ import org.oxycblt.auxio.ui.UISettings
|
|||
* Auxio: A simple, rational music player for android.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class AuxioApp : Application(), ImageLoaderFactory {
|
||||
class Auxio : Application(), ImageLoaderFactory {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// Migrate any settings that may have changed in an app update.
|
|
@ -131,7 +131,7 @@ class MainActivity : AppCompatActivity() {
|
|||
val action =
|
||||
when (intent.action) {
|
||||
Intent.ACTION_VIEW -> InternalPlayer.Action.Open(intent.data ?: return false)
|
||||
AuxioApp.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll
|
||||
Auxio.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll
|
||||
else -> return false
|
||||
}
|
||||
playbackModel.startAction(action)
|
||||
|
|
|
@ -235,8 +235,8 @@ class MainFragment :
|
|||
tryHideAllSheets()
|
||||
}
|
||||
|
||||
// Since the listener is also reliant on the bottom sheets, we must also update it
|
||||
// every frame.
|
||||
// Since the navigation listener is also reliant on the bottom sheets, we must also update
|
||||
// it every frame.
|
||||
callback.invalidateEnabled()
|
||||
|
||||
return true
|
||||
|
|
|
@ -82,7 +82,7 @@ class AlbumDetailFragment :
|
|||
// DetailViewModel handles most initialization from the navigation argument.
|
||||
detailModel.setAlbumUid(args.albumUid)
|
||||
collectImmediately(detailModel.currentAlbum, ::updateAlbum)
|
||||
collectImmediately(detailModel.albumList, detailAdapter::submitList)
|
||||
collectImmediately(detailModel.albumList, detailAdapter::diffList)
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||
|
@ -170,10 +170,10 @@ class AlbumDetailFragment :
|
|||
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) {
|
||||
detailAdapter.setPlayingItem(song, isPlaying)
|
||||
detailAdapter.setPlaying(song, isPlaying)
|
||||
} else {
|
||||
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
||||
detailAdapter.setPlayingItem(null, isPlaying)
|
||||
detailAdapter.setPlaying(null, isPlaying)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -258,7 +258,7 @@ class AlbumDetailFragment :
|
|||
}
|
||||
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
detailAdapter.setSelectedItems(selected)
|
||||
detailAdapter.setSelected(selected.toSet())
|
||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ class ArtistDetailFragment :
|
|||
// DetailViewModel handles most initialization from the navigation argument.
|
||||
detailModel.setArtistUid(args.artistUid)
|
||||
collectImmediately(detailModel.currentArtist, ::updateItem)
|
||||
collectImmediately(detailModel.artistList, detailAdapter::submitList)
|
||||
collectImmediately(detailModel.artistList, detailAdapter::diffList)
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||
|
@ -195,7 +195,7 @@ class ArtistDetailFragment :
|
|||
else -> null
|
||||
}
|
||||
|
||||
detailAdapter.setPlayingItem(playingItem, isPlaying)
|
||||
detailAdapter.setPlaying(playingItem, isPlaying)
|
||||
}
|
||||
|
||||
private fun handleNavigation(item: Music?) {
|
||||
|
@ -234,7 +234,7 @@ class ArtistDetailFragment :
|
|||
}
|
||||
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
detailAdapter.setSelectedItems(selected)
|
||||
detailAdapter.setSelected(selected.toSet())
|
||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ class GenreDetailFragment :
|
|||
// DetailViewModel handles most initialization from the navigation argument.
|
||||
detailModel.setGenreUid(args.genreUid)
|
||||
collectImmediately(detailModel.currentGenre, ::updateItem)
|
||||
collectImmediately(detailModel.genreList, detailAdapter::submitList)
|
||||
collectImmediately(detailModel.genreList, detailAdapter::diffList)
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||
|
@ -189,7 +189,7 @@ class GenreDetailFragment :
|
|||
if (parent is Genre && parent.uid == unlikelyToBeNull(detailModel.currentGenre.value).uid) {
|
||||
playingMusic = song
|
||||
}
|
||||
detailAdapter.setPlayingItem(playingMusic, isPlaying)
|
||||
detailAdapter.setPlaying(playingMusic, isPlaying)
|
||||
}
|
||||
|
||||
private fun handleNavigation(item: Music?) {
|
||||
|
@ -217,7 +217,7 @@ class GenreDetailFragment :
|
|||
}
|
||||
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
detailAdapter.setSelectedItems(selected)
|
||||
detailAdapter.setSelected(selected.toSet())
|
||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
|
|||
}
|
||||
|
||||
override fun getItemViewType(position: Int) =
|
||||
when (differ.currentList[position]) {
|
||||
when (getItem(position)) {
|
||||
// Support the Album header, sub-headers for each disc, and special album songs.
|
||||
is Album -> AlbumDetailViewHolder.VIEW_TYPE
|
||||
is DiscHeader -> DiscHeaderViewHolder.VIEW_TYPE
|
||||
|
@ -75,7 +75,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
|
|||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
super.onBindViewHolder(holder, position)
|
||||
when (val item = differ.currentList[position]) {
|
||||
when (val item = getItem(position)) {
|
||||
is Album -> (holder as AlbumDetailViewHolder).bind(item, listener)
|
||||
is DiscHeader -> (holder as DiscHeaderViewHolder).bind(item)
|
||||
is Song -> (holder as AlbumSongViewHolder).bind(item, listener)
|
||||
|
@ -83,9 +83,12 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
|
|||
}
|
||||
|
||||
override fun isItemFullWidth(position: Int): Boolean {
|
||||
if (super.isItemFullWidth(position)) {
|
||||
return true
|
||||
}
|
||||
// The album and disc headers should be full-width in all configurations.
|
||||
val item = differ.currentList[position]
|
||||
return super.isItemFullWidth(position) || item is Album || item is DiscHeader
|
||||
val item = getItem(position)
|
||||
return item is Album || item is DiscHeader
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
|
@ -46,7 +46,7 @@ import org.oxycblt.auxio.util.inflater
|
|||
class ArtistDetailAdapter(private val listener: Listener<Music>) :
|
||||
DetailAdapter(listener, DIFF_CALLBACK) {
|
||||
override fun getItemViewType(position: Int) =
|
||||
when (differ.currentList[position]) {
|
||||
when (getItem(position)) {
|
||||
// Support an artist header, and special artist albums/songs.
|
||||
is Artist -> ArtistDetailViewHolder.VIEW_TYPE
|
||||
is Album -> ArtistAlbumViewHolder.VIEW_TYPE
|
||||
|
@ -65,7 +65,7 @@ class ArtistDetailAdapter(private val listener: Listener<Music>) :
|
|||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
super.onBindViewHolder(holder, position)
|
||||
// Re-binding an item with new data and not just a changed selection/playing state.
|
||||
when (val item = differ.currentList[position]) {
|
||||
when (val item = getItem(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)
|
||||
|
@ -73,9 +73,11 @@ class ArtistDetailAdapter(private val listener: Listener<Music>) :
|
|||
}
|
||||
|
||||
override fun isItemFullWidth(position: Int): Boolean {
|
||||
if (super.isItemFullWidth(position)) {
|
||||
return true
|
||||
}
|
||||
// Artist headers should be full-width in all configurations.
|
||||
val item = differ.currentList[position]
|
||||
return super.isItemFullWidth(position) || item is Artist
|
||||
return getItem(position) is Artist
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
|
@ -20,7 +20,6 @@ package org.oxycblt.auxio.detail.recycler
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.TooltipCompat
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
|
@ -37,19 +36,19 @@ import org.oxycblt.auxio.util.inflater
|
|||
/**
|
||||
* A [RecyclerView.Adapter] that implements behavior shared across each detail view's adapters.
|
||||
* @param listener A [Listener] to bind interactions to.
|
||||
* @param itemCallback A [DiffUtil.ItemCallback] to use with [AsyncListDiffer] when updating the
|
||||
* @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the
|
||||
* internal list.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
abstract class DetailAdapter(
|
||||
private val listener: Listener<*>,
|
||||
itemCallback: DiffUtil.ItemCallback<Item>
|
||||
) : SelectionIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
|
||||
// Safe to leak this since the listener will not fire during initialization
|
||||
@Suppress("LeakingThis") protected val differ = AsyncListDiffer(this, itemCallback)
|
||||
diffCallback: DiffUtil.ItemCallback<Item>
|
||||
) :
|
||||
SelectionIndicatorAdapter<Item, RecyclerView.ViewHolder>(ListDiffer.Async(diffCallback)),
|
||||
AuxioRecyclerView.SpanSizeLookup {
|
||||
|
||||
override fun getItemViewType(position: Int) =
|
||||
when (differ.currentList[position]) {
|
||||
when (getItem(position)) {
|
||||
// Implement support for headers and sort headers
|
||||
is Header -> HeaderViewHolder.VIEW_TYPE
|
||||
is SortHeader -> SortHeaderViewHolder.VIEW_TYPE
|
||||
|
@ -64,7 +63,7 @@ abstract class DetailAdapter(
|
|||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (val item = differ.currentList[position]) {
|
||||
when (val item = getItem(position)) {
|
||||
is Header -> (holder as HeaderViewHolder).bind(item)
|
||||
is SortHeader -> (holder as SortHeaderViewHolder).bind(item, listener)
|
||||
}
|
||||
|
@ -72,22 +71,10 @@ abstract class DetailAdapter(
|
|||
|
||||
override fun isItemFullWidth(position: Int): Boolean {
|
||||
// Headers should be full-width in all configurations.
|
||||
val item = differ.currentList[position]
|
||||
val item = getItem(position)
|
||||
return item is Header || item is SortHeader
|
||||
}
|
||||
|
||||
override val currentList: List<Item>
|
||||
get() = differ.currentList
|
||||
|
||||
/**
|
||||
* Asynchronously update the list with new items. Assumes that the list only contains data
|
||||
* supported by the concrete [DetailAdapter] implementation.
|
||||
* @param newList The new [Item]s for the adapter to display.
|
||||
*/
|
||||
fun submitList(newList: List<Item>) {
|
||||
differ.submitList(newList)
|
||||
}
|
||||
|
||||
/** An extended [SelectableListListener] for [DetailAdapter] implementations. */
|
||||
interface Listener<in T : Music> : SelectableListListener<T> {
|
||||
// TODO: Split off into sub-listeners if a collapsing toolbar is implemented.
|
||||
|
|
|
@ -44,7 +44,7 @@ import org.oxycblt.auxio.util.inflater
|
|||
class GenreDetailAdapter(private val listener: Listener<Music>) :
|
||||
DetailAdapter(listener, DIFF_CALLBACK) {
|
||||
override fun getItemViewType(position: Int) =
|
||||
when (differ.currentList[position]) {
|
||||
when (getItem(position)) {
|
||||
// Support the Genre header and generic Artist/Song items. There's nothing about
|
||||
// a genre that will make the artists/songs homogeneous, so it doesn't matter what we
|
||||
// use for their ViewHolders.
|
||||
|
@ -64,7 +64,7 @@ class GenreDetailAdapter(private val listener: Listener<Music>) :
|
|||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
super.onBindViewHolder(holder, position)
|
||||
when (val item = differ.currentList[position]) {
|
||||
when (val item = getItem(position)) {
|
||||
is Genre -> (holder as GenreDetailViewHolder).bind(item, listener)
|
||||
is Artist -> (holder as ArtistViewHolder).bind(item, listener)
|
||||
is Song -> (holder as SongViewHolder).bind(item, listener)
|
||||
|
@ -72,9 +72,11 @@ class GenreDetailAdapter(private val listener: Listener<Music>) :
|
|||
}
|
||||
|
||||
override fun isItemFullWidth(position: Int): Boolean {
|
||||
if (super.isItemFullWidth(position)) {
|
||||
return true
|
||||
}
|
||||
// Genre headers should be full-width in all configurations
|
||||
val item = differ.currentList[position]
|
||||
return super.isItemFullWidth(position) || item is Genre
|
||||
return getItem(position) is Genre
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
|
@ -31,8 +31,8 @@ import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
|||
import org.oxycblt.auxio.list.*
|
||||
import org.oxycblt.auxio.list.ListFragment
|
||||
import org.oxycblt.auxio.list.recycler.AlbumViewHolder
|
||||
import org.oxycblt.auxio.list.recycler.ListDiffer
|
||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.music.library.Sort
|
||||
import org.oxycblt.auxio.playback.formatDurationMs
|
||||
|
@ -67,7 +67,7 @@ class AlbumListFragment :
|
|||
}
|
||||
|
||||
collectImmediately(homeModel.albumsList, albumAdapter::replaceList)
|
||||
collectImmediately(selectionModel.selected, albumAdapter::setSelectedItems)
|
||||
collectImmediately(selectionModel.selected, ::updateSelection)
|
||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
}
|
||||
|
||||
|
@ -130,9 +130,13 @@ class AlbumListFragment :
|
|||
openMusicMenu(anchor, R.menu.menu_album_actions, item)
|
||||
}
|
||||
|
||||
private fun updateSelection(selection: List<Music>) {
|
||||
albumAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
|
||||
}
|
||||
|
||||
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||
// If an album is playing, highlight it within this adapter.
|
||||
albumAdapter.setPlayingItem(parent as? Album, isPlaying)
|
||||
albumAdapter.setPlaying(parent as? Album, isPlaying)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -140,25 +144,14 @@ class AlbumListFragment :
|
|||
* @param listener An [SelectableListListener] to bind interactions to.
|
||||
*/
|
||||
private class AlbumAdapter(private val listener: SelectableListListener<Album>) :
|
||||
SelectionIndicatorAdapter<AlbumViewHolder>() {
|
||||
private val differ = SyncListDiffer(this, AlbumViewHolder.DIFF_CALLBACK)
|
||||
|
||||
override val currentList: List<Item>
|
||||
get() = differ.currentList
|
||||
SelectionIndicatorAdapter<Album, AlbumViewHolder>(
|
||||
ListDiffer.Async(AlbumViewHolder.DIFF_CALLBACK)) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
AlbumViewHolder.from(parent)
|
||||
|
||||
override fun onBindViewHolder(holder: AlbumViewHolder, position: Int) {
|
||||
holder.bind(differ.currentList[position], listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously update the list with new [Album]s.
|
||||
* @param newList The new [Album]s for the adapter to display.
|
||||
*/
|
||||
fun replaceList(newList: List<Album>) {
|
||||
differ.replaceList(newList)
|
||||
holder.bind(getItem(position), listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,9 +29,10 @@ import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
|||
import org.oxycblt.auxio.list.*
|
||||
import org.oxycblt.auxio.list.ListFragment
|
||||
import org.oxycblt.auxio.list.recycler.ArtistViewHolder
|
||||
import org.oxycblt.auxio.list.recycler.ListDiffer
|
||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicMode
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.library.Sort
|
||||
|
@ -48,7 +49,7 @@ class ArtistListFragment :
|
|||
FastScrollRecyclerView.PopupProvider,
|
||||
FastScrollRecyclerView.Listener {
|
||||
private val homeModel: HomeViewModel by activityViewModels()
|
||||
private val homeAdapter = ArtistAdapter(this)
|
||||
private val artistAdapter = ArtistAdapter(this)
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||
FragmentHomeListBinding.inflate(inflater)
|
||||
|
@ -58,13 +59,13 @@ class ArtistListFragment :
|
|||
|
||||
binding.homeRecycler.apply {
|
||||
id = R.id.home_artist_recycler
|
||||
adapter = homeAdapter
|
||||
adapter = artistAdapter
|
||||
popupProvider = this@ArtistListFragment
|
||||
listener = this@ArtistListFragment
|
||||
}
|
||||
|
||||
collectImmediately(homeModel.artistsList, homeAdapter::replaceList)
|
||||
collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
|
||||
collectImmediately(homeModel.artistsList, artistAdapter::replaceList)
|
||||
collectImmediately(selectionModel.selected, ::updateSelection)
|
||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
}
|
||||
|
||||
|
@ -107,9 +108,13 @@ class ArtistListFragment :
|
|||
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
||||
}
|
||||
|
||||
private fun updateSelection(selection: List<Music>) {
|
||||
artistAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
|
||||
}
|
||||
|
||||
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||
// If an artist is playing, highlight it within this adapter.
|
||||
homeAdapter.setPlayingItem(parent as? Artist, isPlaying)
|
||||
artistAdapter.setPlaying(parent as? Artist, isPlaying)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -117,25 +122,14 @@ class ArtistListFragment :
|
|||
* @param listener An [SelectableListListener] to bind interactions to.
|
||||
*/
|
||||
private class ArtistAdapter(private val listener: SelectableListListener<Artist>) :
|
||||
SelectionIndicatorAdapter<ArtistViewHolder>() {
|
||||
private val differ = SyncListDiffer(this, ArtistViewHolder.DIFF_CALLBACK)
|
||||
|
||||
override val currentList: List<Item>
|
||||
get() = differ.currentList
|
||||
SelectionIndicatorAdapter<Artist, ArtistViewHolder>(
|
||||
ListDiffer.Async(ArtistViewHolder.DIFF_CALLBACK)) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
ArtistViewHolder.from(parent)
|
||||
|
||||
override fun onBindViewHolder(holder: ArtistViewHolder, position: Int) {
|
||||
holder.bind(differ.currentList[position], listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously update the list with new [Artist]s.
|
||||
* @param newList The new [Artist]s for the adapter to display.
|
||||
*/
|
||||
fun replaceList(newList: List<Artist>) {
|
||||
differ.replaceList(newList)
|
||||
holder.bind(getItem(position), listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,9 +29,10 @@ import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
|||
import org.oxycblt.auxio.list.*
|
||||
import org.oxycblt.auxio.list.ListFragment
|
||||
import org.oxycblt.auxio.list.recycler.GenreViewHolder
|
||||
import org.oxycblt.auxio.list.recycler.ListDiffer
|
||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicMode
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.library.Sort
|
||||
|
@ -47,7 +48,7 @@ class GenreListFragment :
|
|||
FastScrollRecyclerView.PopupProvider,
|
||||
FastScrollRecyclerView.Listener {
|
||||
private val homeModel: HomeViewModel by activityViewModels()
|
||||
private val homeAdapter = GenreAdapter(this)
|
||||
private val genreAdapter = GenreAdapter(this)
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||
FragmentHomeListBinding.inflate(inflater)
|
||||
|
@ -57,13 +58,13 @@ class GenreListFragment :
|
|||
|
||||
binding.homeRecycler.apply {
|
||||
id = R.id.home_genre_recycler
|
||||
adapter = homeAdapter
|
||||
adapter = genreAdapter
|
||||
popupProvider = this@GenreListFragment
|
||||
listener = this@GenreListFragment
|
||||
}
|
||||
|
||||
collectImmediately(homeModel.genresList, homeAdapter::replaceList)
|
||||
collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
|
||||
collectImmediately(homeModel.genresList, genreAdapter::replaceList)
|
||||
collectImmediately(selectionModel.selected, ::updateSelection)
|
||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
}
|
||||
|
||||
|
@ -106,9 +107,13 @@ class GenreListFragment :
|
|||
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
||||
}
|
||||
|
||||
private fun updateSelection(selection: List<Music>) {
|
||||
genreAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
|
||||
}
|
||||
|
||||
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||
// If a genre is playing, highlight it within this adapter.
|
||||
homeAdapter.setPlayingItem(parent as? Genre, isPlaying)
|
||||
genreAdapter.setPlaying(parent as? Genre, isPlaying)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -116,25 +121,13 @@ class GenreListFragment :
|
|||
* @param listener An [SelectableListListener] to bind interactions to.
|
||||
*/
|
||||
private class GenreAdapter(private val listener: SelectableListListener<Genre>) :
|
||||
SelectionIndicatorAdapter<GenreViewHolder>() {
|
||||
private val differ = SyncListDiffer(this, GenreViewHolder.DIFF_CALLBACK)
|
||||
|
||||
override val currentList: List<Item>
|
||||
get() = differ.currentList
|
||||
|
||||
SelectionIndicatorAdapter<Genre, GenreViewHolder>(
|
||||
ListDiffer.Async(GenreViewHolder.DIFF_CALLBACK)) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
GenreViewHolder.from(parent)
|
||||
|
||||
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
|
||||
holder.bind(differ.currentList[position], listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously update the list with new [Genre]s.
|
||||
* @param newList The new [Genre]s for the adapter to display.
|
||||
*/
|
||||
fun replaceList(newList: List<Genre>) {
|
||||
differ.replaceList(newList)
|
||||
holder.bind(getItem(position), listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,9 +30,10 @@ import org.oxycblt.auxio.home.HomeViewModel
|
|||
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
||||
import org.oxycblt.auxio.list.*
|
||||
import org.oxycblt.auxio.list.ListFragment
|
||||
import org.oxycblt.auxio.list.recycler.ListDiffer
|
||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SongViewHolder
|
||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicMode
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
|
@ -50,7 +51,7 @@ class SongListFragment :
|
|||
FastScrollRecyclerView.PopupProvider,
|
||||
FastScrollRecyclerView.Listener {
|
||||
private val homeModel: HomeViewModel by activityViewModels()
|
||||
private val homeAdapter = SongAdapter(this)
|
||||
private val songAdapter = SongAdapter(this)
|
||||
// Save memory by re-using the same formatter and string builder when creating popup text
|
||||
private val formatterSb = StringBuilder(64)
|
||||
private val formatter = Formatter(formatterSb)
|
||||
|
@ -63,13 +64,13 @@ class SongListFragment :
|
|||
|
||||
binding.homeRecycler.apply {
|
||||
id = R.id.home_song_recycler
|
||||
adapter = homeAdapter
|
||||
adapter = songAdapter
|
||||
popupProvider = this@SongListFragment
|
||||
listener = this@SongListFragment
|
||||
}
|
||||
|
||||
collectImmediately(homeModel.songLists, homeAdapter::replaceList)
|
||||
collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
|
||||
collectImmediately(homeModel.songLists, songAdapter::replaceList)
|
||||
collectImmediately(selectionModel.selected, ::updateSelection)
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
}
|
||||
|
@ -136,12 +137,16 @@ class SongListFragment :
|
|||
openMusicMenu(anchor, R.menu.menu_song_actions, item)
|
||||
}
|
||||
|
||||
private fun updateSelection(selection: List<Music>) {
|
||||
songAdapter.setSelected(selection.filterIsInstanceTo(mutableSetOf()))
|
||||
}
|
||||
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
if (parent == null) {
|
||||
homeAdapter.setPlayingItem(song, isPlaying)
|
||||
songAdapter.setPlaying(song, isPlaying)
|
||||
} else {
|
||||
// Ignore playback that is not from all songs
|
||||
homeAdapter.setPlayingItem(null, isPlaying)
|
||||
songAdapter.setPlaying(null, isPlaying)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -150,25 +155,14 @@ class SongListFragment :
|
|||
* @param listener An [SelectableListListener] to bind interactions to.
|
||||
*/
|
||||
private class SongAdapter(private val listener: SelectableListListener<Song>) :
|
||||
SelectionIndicatorAdapter<SongViewHolder>() {
|
||||
private val differ = SyncListDiffer(this, SongViewHolder.DIFF_CALLBACK)
|
||||
|
||||
override val currentList: List<Item>
|
||||
get() = differ.currentList
|
||||
SelectionIndicatorAdapter<Song, SongViewHolder>(
|
||||
ListDiffer.Async(SongViewHolder.DIFF_CALLBACK)) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
SongViewHolder.from(parent)
|
||||
|
||||
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
|
||||
holder.bind(differ.currentList[position], listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously update the list with new [Song]s.
|
||||
* @param newList The new [Song]s for the adapter to display.
|
||||
*/
|
||||
fun replaceList(newList: List<Song>) {
|
||||
differ.replaceList(newList)
|
||||
holder.bind(getItem(position), listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,20 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.list
|
||||
|
||||
/**
|
||||
|
@ -12,8 +29,8 @@ enum class UpdateInstructions {
|
|||
DIFF,
|
||||
|
||||
/**
|
||||
* Synchronously remove the current list and replace it with a new one. This should be used
|
||||
* for large diffs with that would cause erratic scroll behavior or in-efficiency.
|
||||
* Synchronously remove the current list and replace it with a new one. This should be used for
|
||||
* large diffs with that would cause erratic scroll behavior or in-efficiency.
|
||||
*/
|
||||
REPLACE
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.list.recycler
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.list.UpdateInstructions
|
||||
|
||||
/**
|
||||
* A [RecyclerView.Adapter] with [ListDiffer] integration.
|
||||
* @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use.
|
||||
*/
|
||||
abstract class DiffAdapter<T, VH : RecyclerView.ViewHolder>(differFactory: ListDiffer.Factory<T>) :
|
||||
RecyclerView.Adapter<VH>() {
|
||||
private val differ = differFactory.new(@Suppress("LeakingThis") this)
|
||||
|
||||
final override fun getItemCount() = differ.currentList.size
|
||||
|
||||
/** The current list of [T] items. */
|
||||
val currentList: List<T>
|
||||
get() = differ.currentList
|
||||
|
||||
/**
|
||||
* Get a [T] item at the given position.
|
||||
* @param at The position to get the item at.
|
||||
* @throws IndexOutOfBoundsException If the index is not in the list bounds/
|
||||
*/
|
||||
fun getItem(at: Int) = differ.currentList[at]
|
||||
|
||||
/**
|
||||
* Dynamically determine how to update the list based on the given [UpdateInstructions].
|
||||
* @param newList The new list of [T] items to show.
|
||||
* @param instructions The [UpdateInstructions] specifying how to update the list.
|
||||
*/
|
||||
fun submitList(newList: List<T>, instructions: UpdateInstructions) {
|
||||
when (instructions) {
|
||||
UpdateInstructions.DIFF -> diffList(newList)
|
||||
UpdateInstructions.REPLACE -> replaceList(newList)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update this list using [DiffUtil]. This can simplify the work of updating the list, but can
|
||||
* also cause erratic behavior.
|
||||
* @param newList The new list of [T] items to show.
|
||||
* @param onDone Callback that will be invoked when the update is completed, allowing means to
|
||||
* reset the state.
|
||||
*/
|
||||
fun diffList(newList: List<T>, onDone: () -> Unit = {}) = differ.diffList(newList, onDone)
|
||||
|
||||
/**
|
||||
* Visually replace the previous list with a new list. This is useful for large diffs that are
|
||||
* too erratic for [diffList].
|
||||
* @param newList The new list of [T] items to show.
|
||||
*/
|
||||
fun replaceList(newList: List<T>) = differ.replaceList(newList)
|
||||
}
|
224
app/src/main/java/org/oxycblt/auxio/list/recycler/ListDiffer.kt
Normal file
224
app/src/main/java/org/oxycblt/auxio/list/recycler/ListDiffer.kt
Normal file
|
@ -0,0 +1,224 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.list.recycler
|
||||
|
||||
import androidx.recyclerview.widget.AdapterListUpdateCallback
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import java.lang.reflect.Field
|
||||
import org.oxycblt.auxio.util.lazyReflectedField
|
||||
import org.oxycblt.auxio.util.requireIs
|
||||
|
||||
/**
|
||||
* List differ wrapper that provides more flexibility regarding the way lists are updated.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
interface ListDiffer<T> {
|
||||
/** The current list of [T] items. */
|
||||
val currentList: List<T>
|
||||
|
||||
/**
|
||||
* Update this list using [DiffUtil]. This can simplify the work of updating the list, but can
|
||||
* also cause erratic behavior.
|
||||
* @param newList The new list of [T] items to show.
|
||||
* @param onDone Callback that will be invoked when the update is completed, allowing means to
|
||||
* reset the state.
|
||||
*/
|
||||
fun diffList(newList: List<T>, onDone: () -> Unit = {})
|
||||
|
||||
/**
|
||||
* Visually replace the previous list with a new list. This is useful for large diffs that are
|
||||
* too erratic for [diffList].
|
||||
* @param newList The new list of [T] items to show.
|
||||
*/
|
||||
fun replaceList(newList: List<T>)
|
||||
|
||||
/**
|
||||
* Defines the creation of new [ListDiffer] instances. Allows such [ListDiffer]s to be passed as
|
||||
* arguments without reliance on a `this` [RecyclerView.Adapter].
|
||||
*/
|
||||
abstract class Factory<T> {
|
||||
/**
|
||||
* Create a new [ListDiffer] bound to the given [RecyclerView.Adapter].
|
||||
* @param adapter The [RecyclerView.Adapter] to bind to.
|
||||
*/
|
||||
abstract fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* Update lists on another thread. This is useful when large diffs are likely to occur in this
|
||||
* list that would be exceedingly slow with [Blocking].
|
||||
* @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the
|
||||
* internal list.
|
||||
*/
|
||||
class Async<T>(private val diffCallback: DiffUtil.ItemCallback<T>) : Factory<T>() {
|
||||
override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T> =
|
||||
RealAsyncListDiffer(AdapterListUpdateCallback(adapter), diffCallback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update lists on the main thread. This is useful when many small, discrete list diffs are
|
||||
* likely to occur that would cause [Async] to get race conditions.
|
||||
* @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the
|
||||
* internal list.
|
||||
*/
|
||||
class Blocking<T>(private val diffCallback: DiffUtil.ItemCallback<T>) : Factory<T>() {
|
||||
override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T> =
|
||||
RealBlockingListDiffer(AdapterListUpdateCallback(adapter), diffCallback)
|
||||
}
|
||||
}
|
||||
|
||||
private class RealAsyncListDiffer<T>(
|
||||
private val updateCallback: ListUpdateCallback,
|
||||
diffCallback: DiffUtil.ItemCallback<T>
|
||||
) : ListDiffer<T> {
|
||||
private val inner =
|
||||
AsyncListDiffer(updateCallback, AsyncDifferConfig.Builder(diffCallback).build())
|
||||
|
||||
override val currentList: List<T>
|
||||
get() = inner.currentList
|
||||
|
||||
override fun diffList(newList: List<T>, onDone: () -> Unit) {
|
||||
inner.submitList(newList, onDone)
|
||||
}
|
||||
|
||||
override fun replaceList(newList: List<T>) {
|
||||
if (inner.currentList == newList) {
|
||||
// Nothing to do.
|
||||
return
|
||||
}
|
||||
// Do possibly the most idiotic thing possible and mutate the internal differ state
|
||||
// so we don't have to deal with any disjoint list garbage. This should cancel any prior
|
||||
// updates and correctly set up the list values while still allowing for the same
|
||||
// visual animation as the blocking replaceList.
|
||||
val oldListSize = inner.currentList.size
|
||||
ASD_MAX_GENERATION_FIELD.set(inner, requireIs<Int>(ASD_MAX_GENERATION_FIELD.get(inner)) + 1)
|
||||
ASD_MUTABLE_LIST_FIELD.set(inner, newList.ifEmpty { null })
|
||||
ASD_READ_ONLY_LIST_FIELD.set(inner, newList)
|
||||
updateCallback.onRemoved(0, oldListSize)
|
||||
updateCallback.onInserted(0, newList.size)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val ASD_MAX_GENERATION_FIELD: Field by
|
||||
lazyReflectedField(AsyncListDiffer::class, "mMaxScheduledGeneration")
|
||||
val ASD_MUTABLE_LIST_FIELD: Field by lazyReflectedField(AsyncListDiffer::class, "mList")
|
||||
val ASD_READ_ONLY_LIST_FIELD: Field by
|
||||
lazyReflectedField(AsyncListDiffer::class, "mReadOnlyList")
|
||||
}
|
||||
}
|
||||
|
||||
private class RealBlockingListDiffer<T>(
|
||||
private val updateCallback: ListUpdateCallback,
|
||||
private val diffCallback: DiffUtil.ItemCallback<T>
|
||||
) : ListDiffer<T> {
|
||||
override var currentList = listOf<T>()
|
||||
|
||||
override fun diffList(newList: List<T>, onDone: () -> Unit) {
|
||||
if (newList === currentList || newList.isEmpty() && currentList.isEmpty()) {
|
||||
onDone()
|
||||
return
|
||||
}
|
||||
|
||||
if (newList.isEmpty()) {
|
||||
val oldListSize = currentList.size
|
||||
currentList = listOf()
|
||||
updateCallback.onRemoved(0, oldListSize)
|
||||
onDone()
|
||||
return
|
||||
}
|
||||
|
||||
if (currentList.isEmpty()) {
|
||||
currentList = newList
|
||||
updateCallback.onInserted(0, newList.size)
|
||||
onDone()
|
||||
return
|
||||
}
|
||||
|
||||
val oldList = currentList
|
||||
val result =
|
||||
DiffUtil.calculateDiff(
|
||||
object : DiffUtil.Callback() {
|
||||
override fun getOldListSize(): Int {
|
||||
return oldList.size
|
||||
}
|
||||
|
||||
override fun getNewListSize(): Int {
|
||||
return newList.size
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(
|
||||
oldItemPosition: Int,
|
||||
newItemPosition: Int
|
||||
): Boolean {
|
||||
val oldItem: T? = oldList[oldItemPosition]
|
||||
val newItem: T? = newList[newItemPosition]
|
||||
return if (oldItem != null && newItem != null) {
|
||||
diffCallback.areItemsTheSame(oldItem, newItem)
|
||||
} else {
|
||||
oldItem == null && newItem == null
|
||||
}
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItemPosition: Int,
|
||||
newItemPosition: Int
|
||||
): Boolean {
|
||||
val oldItem: T? = oldList[oldItemPosition]
|
||||
val newItem: T? = newList[newItemPosition]
|
||||
return if (oldItem != null && newItem != null) {
|
||||
diffCallback.areContentsTheSame(oldItem, newItem)
|
||||
} else if (oldItem == null && newItem == null) {
|
||||
true
|
||||
} else {
|
||||
throw AssertionError()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getChangePayload(
|
||||
oldItemPosition: Int,
|
||||
newItemPosition: Int
|
||||
): Any? {
|
||||
val oldItem: T? = oldList[oldItemPosition]
|
||||
val newItem: T? = newList[newItemPosition]
|
||||
return if (oldItem != null && newItem != null) {
|
||||
diffCallback.getChangePayload(oldItem, newItem)
|
||||
} else {
|
||||
throw AssertionError()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
currentList = newList
|
||||
result.dispatchUpdatesTo(updateCallback)
|
||||
onDone()
|
||||
}
|
||||
|
||||
override fun replaceList(newList: List<T>) {
|
||||
if (currentList == newList) {
|
||||
// Nothing to do.
|
||||
return
|
||||
}
|
||||
|
||||
diffList(listOf())
|
||||
diffList(newList)
|
||||
}
|
||||
}
|
|
@ -19,33 +19,27 @@ package org.oxycblt.auxio.list.recycler
|
|||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* A [RecyclerView.Adapter] that supports indicating the playback status of a particular item.
|
||||
* @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
|
||||
abstract class PlayingIndicatorAdapter<T, VH : RecyclerView.ViewHolder>(
|
||||
differFactory: ListDiffer.Factory<T>
|
||||
) : DiffAdapter<T, VH>(differFactory) {
|
||||
// There are actually two states for this adapter:
|
||||
// - The currently playing item, which is usually marked as "selected" and becomes accented.
|
||||
// - Whether playback is ongoing, which corresponds to whether the item's ImageGroup is
|
||||
// marked as "playing" or not.
|
||||
private var currentMusic: Music? = null
|
||||
private var currentItem: T? = null
|
||||
private var isPlaying = false
|
||||
|
||||
/**
|
||||
* The current list of the adapter. This is used to update items if the indicator state changes.
|
||||
*/
|
||||
abstract val currentList: List<Item>
|
||||
|
||||
override fun getItemCount() = currentList.size
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int, payloads: List<Any>) {
|
||||
// Only try to update the playing indicator if the ViewHolder supports it
|
||||
if (holder is ViewHolder) {
|
||||
holder.updatePlayingIndicator(currentList[position] == currentMusic, isPlaying)
|
||||
holder.updatePlayingIndicator(currentList[position] == currentItem, isPlaying)
|
||||
}
|
||||
|
||||
if (payloads.isEmpty()) {
|
||||
|
@ -56,14 +50,14 @@ abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerV
|
|||
}
|
||||
/**
|
||||
* Update the currently playing item in the list.
|
||||
* @param music The [Music] currently being played, or null if it is not being played.
|
||||
* @param item The [T] currently being played, or null if it is not being played.
|
||||
* @param isPlaying Whether playback is ongoing or paused.
|
||||
*/
|
||||
fun setPlayingItem(music: Music?, isPlaying: Boolean) {
|
||||
fun setPlaying(item: T?, isPlaying: Boolean) {
|
||||
var updatedItem = false
|
||||
if (currentMusic != music) {
|
||||
val oldItem = currentMusic
|
||||
currentMusic = music
|
||||
if (currentItem != item) {
|
||||
val oldItem = currentItem
|
||||
currentItem = item
|
||||
|
||||
// Remove the playing indicator from the old item
|
||||
if (oldItem != null) {
|
||||
|
@ -76,8 +70,8 @@ abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerV
|
|||
}
|
||||
|
||||
// Enable the playing indicator on the new item
|
||||
if (music != null) {
|
||||
val pos = currentList.indexOfFirst { it == music }
|
||||
if (item != null) {
|
||||
val pos = currentList.indexOfFirst { it == item }
|
||||
if (pos > -1) {
|
||||
notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED)
|
||||
} else {
|
||||
|
@ -94,8 +88,8 @@ abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerV
|
|||
// We may have already called notifyItemChanged before when checking
|
||||
// if the item was being played, so in that case we don't need to
|
||||
// update again here.
|
||||
if (!updatedItem && music != null) {
|
||||
val pos = currentList.indexOfFirst { it == music }
|
||||
if (!updatedItem && item != null) {
|
||||
val pos = currentList.indexOfFirst { it == item }
|
||||
if (pos > -1) {
|
||||
notifyItemChanged(pos, PAYLOAD_PLAYING_INDICATOR_CHANGED)
|
||||
} else {
|
||||
|
|
|
@ -24,11 +24,13 @@ import org.oxycblt.auxio.music.Music
|
|||
/**
|
||||
* A [PlayingIndicatorAdapter] that also supports indicating the selection status of a group of
|
||||
* items.
|
||||
* @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> :
|
||||
PlayingIndicatorAdapter<VH>() {
|
||||
private var selectedItems = setOf<Music>()
|
||||
abstract class SelectionIndicatorAdapter<T, VH : RecyclerView.ViewHolder>(
|
||||
differFactory: ListDiffer.Factory<T>
|
||||
) : PlayingIndicatorAdapter<T, VH>(differFactory) {
|
||||
private var selectedItems = setOf<T>()
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int, payloads: List<Any>) {
|
||||
super.onBindViewHolder(holder, position, payloads)
|
||||
|
@ -39,9 +41,9 @@ abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> :
|
|||
|
||||
/**
|
||||
* Update the list of selected items.
|
||||
* @param items A list of selected [Music].
|
||||
* @param items A set of selected [T] items.
|
||||
*/
|
||||
fun setSelectedItems(items: List<Music>) {
|
||||
fun setSelected(items: Set<T>) {
|
||||
val oldSelectedItems = selectedItems
|
||||
val newSelectedItems = items.toSet()
|
||||
if (newSelectedItems == oldSelectedItems) {
|
||||
|
|
|
@ -64,8 +64,7 @@ class MetadataExtractor(
|
|||
/**
|
||||
* Returns a flow that parses all [Song.Raw] instances queued by the sub-extractors. This will
|
||||
* first delegate to the sub-extractors before parsing the metadata itself.
|
||||
* @param emit A listener that will be invoked with every new [Song.Raw] instance when they are
|
||||
* successfully loaded.
|
||||
* @return A flow of [Song.Raw] instances.
|
||||
*/
|
||||
fun extract() = flow {
|
||||
while (true) {
|
||||
|
@ -310,8 +309,9 @@ class Task(context: Context, private val raw: Song.Raw) {
|
|||
// Album artist
|
||||
comments["musicbrainz_albumartistid"]?.let { raw.albumArtistMusicBrainzIds = it }
|
||||
(comments["albumartists"] ?: comments["albumartist"])?.let { raw.albumArtistNames = it }
|
||||
(comments["albumartists_sort"] ?: comments["albumartistsort"])
|
||||
?.let { raw.albumArtistSortNames = it }
|
||||
(comments["albumartists_sort"] ?: comments["albumartistsort"])?.let {
|
||||
raw.albumArtistSortNames = it
|
||||
}
|
||||
|
||||
// Genre
|
||||
comments["genre"]?.let { raw.genreNames = it }
|
||||
|
|
|
@ -199,7 +199,7 @@ interface PlaybackSettings : Settings<PlaybackSettings.Listener> {
|
|||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private companion object {
|
||||
const val OLD_KEY_ALT_NOTIF_ACTION = "KEY_ALT_NOTIF_ACTION"
|
||||
const val OLD_KEY_LIB_PLAYBACK_MODE = "KEY_SONG_PLAY_MODE2"
|
||||
const val OLD_KEY_DETAIL_PLAYBACK_MODE = "auxio_detail_song_play_mode"
|
||||
|
|
|
@ -27,9 +27,10 @@ import com.google.android.material.shape.MaterialShapeDrawable
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.ItemQueueSongBinding
|
||||
import org.oxycblt.auxio.list.EditableListListener
|
||||
import org.oxycblt.auxio.list.recycler.DiffAdapter
|
||||
import org.oxycblt.auxio.list.recycler.ListDiffer
|
||||
import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SongViewHolder
|
||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.util.*
|
||||
|
||||
|
@ -39,16 +40,13 @@ import org.oxycblt.auxio.util.*
|
|||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class QueueAdapter(private val listener: EditableListListener<Song>) :
|
||||
RecyclerView.Adapter<QueueSongViewHolder>() {
|
||||
private var differ = SyncListDiffer(this, QueueSongViewHolder.DIFF_CALLBACK)
|
||||
DiffAdapter<Song, QueueSongViewHolder>(ListDiffer.Blocking(QueueSongViewHolder.DIFF_CALLBACK)) {
|
||||
// Since PlayingIndicator adapter relies on an item value, we cannot use it for this
|
||||
// adapter, as one item can appear at several points in the UI. Use a similar implementation
|
||||
// with an index value instead.
|
||||
private var currentIndex = 0
|
||||
private var isPlaying = false
|
||||
|
||||
override fun getItemCount() = differ.currentList.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
QueueSongViewHolder.from(parent)
|
||||
|
||||
|
@ -61,31 +59,13 @@ class QueueAdapter(private val listener: EditableListListener<Song>) :
|
|||
payload: List<Any>
|
||||
) {
|
||||
if (payload.isEmpty()) {
|
||||
viewHolder.bind(differ.currentList[position], listener)
|
||||
viewHolder.bind(getItem(position), listener)
|
||||
}
|
||||
|
||||
viewHolder.isFuture = position > currentIndex
|
||||
viewHolder.updatePlayingIndicator(position == currentIndex, isPlaying)
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously update the list with new items. This is exceedingly slow for large diffs, so
|
||||
* only use it for trivial updates.
|
||||
* @param newList The new [Song]s for the adapter to display.
|
||||
*/
|
||||
fun submitList(newList: List<Song>) {
|
||||
differ.submitList(newList)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the list with a new list. This is exceedingly slow for large diffs, so only use it
|
||||
* for trivial updates.
|
||||
* @param newList The new [Song]s for the adapter to display.
|
||||
*/
|
||||
fun replaceList(newList: List<Song>) {
|
||||
differ.replaceList(newList)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the position of the currently playing item in the queue. This will mark the item as
|
||||
* playing and any previous items as played.
|
||||
|
|
|
@ -33,7 +33,6 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
|
|||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* A [ViewBindingFragment] that displays an editable queue.
|
||||
|
@ -102,13 +101,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), EditableListL
|
|||
|
||||
// Replace or diff the queue depending on the type of change it is.
|
||||
val instructions = queueModel.instructions
|
||||
if (instructions?.update == UpdateInstructions.REPLACE) {
|
||||
logD("Replacing queue")
|
||||
queueAdapter.replaceList(queue)
|
||||
} else {
|
||||
logD("Diffing queue")
|
||||
queueAdapter.submitList(queue)
|
||||
}
|
||||
queueAdapter.submitList(queue, instructions?.update ?: UpdateInstructions.DIFF)
|
||||
// Update position in list (and thus past/future items)
|
||||
queueAdapter.setPosition(index, isPlaying)
|
||||
|
||||
|
|
|
@ -165,7 +165,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
|||
fun write(state: SavedState?) {
|
||||
requireBackgroundThread()
|
||||
// Only bother saving a state if a song is actively playing from one.
|
||||
// This is not the case with a null state or a state with an out-of-bounds index.
|
||||
// This is not the case with a null state.
|
||||
if (state != null) {
|
||||
// Transform saved state into raw state, which can then be written to the database.
|
||||
val rawPlaybackState =
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
package org.oxycblt.auxio.search
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.list.*
|
||||
import org.oxycblt.auxio.list.recycler.*
|
||||
|
@ -30,14 +29,11 @@ import org.oxycblt.auxio.music.*
|
|||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class SearchAdapter(private val listener: SelectableListListener<Music>) :
|
||||
SelectionIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
|
||||
private val differ = AsyncListDiffer(this, DIFF_CALLBACK)
|
||||
|
||||
override val currentList: List<Item>
|
||||
get() = differ.currentList
|
||||
SelectionIndicatorAdapter<Item, RecyclerView.ViewHolder>(ListDiffer.Async(DIFF_CALLBACK)),
|
||||
AuxioRecyclerView.SpanSizeLookup {
|
||||
|
||||
override fun getItemViewType(position: Int) =
|
||||
when (differ.currentList[position]) {
|
||||
when (getItem(position)) {
|
||||
is Song -> SongViewHolder.VIEW_TYPE
|
||||
is Album -> AlbumViewHolder.VIEW_TYPE
|
||||
is Artist -> ArtistViewHolder.VIEW_TYPE
|
||||
|
@ -57,7 +53,7 @@ class SearchAdapter(private val listener: SelectableListListener<Music>) :
|
|||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (val item = differ.currentList[position]) {
|
||||
when (val item = getItem(position)) {
|
||||
is Song -> (holder as SongViewHolder).bind(item, listener)
|
||||
is Album -> (holder as AlbumViewHolder).bind(item, listener)
|
||||
is Artist -> (holder as ArtistViewHolder).bind(item, listener)
|
||||
|
@ -66,17 +62,7 @@ class SearchAdapter(private val listener: SelectableListListener<Music>) :
|
|||
}
|
||||
}
|
||||
|
||||
override fun isItemFullWidth(position: Int) = differ.currentList[position] is Header
|
||||
|
||||
/**
|
||||
* Asynchronously update the list with new items. Assumes that the list only contains supported
|
||||
* data..
|
||||
* @param newList The new [Item]s for the adapter to display.
|
||||
* @param callback A block called when the asynchronous update is completed.
|
||||
*/
|
||||
fun submitList(newList: List<Item>, callback: () -> Unit) {
|
||||
differ.submitList(newList, callback)
|
||||
}
|
||||
override fun isItemFullWidth(position: Int) = getItem(position) is Header
|
||||
|
||||
private companion object {
|
||||
/** A comparator that can be used with DiffUtil. */
|
||||
|
|
|
@ -153,7 +153,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
|||
// Don't show the RecyclerView (and it's stray overscroll effects) when there
|
||||
// are no results.
|
||||
binding.searchRecycler.isInvisible = results.isEmpty()
|
||||
searchAdapter.submitList(results.toMutableList()) {
|
||||
searchAdapter.diffList(results.toMutableList()) {
|
||||
// I would make it so that the position is only scrolled back to the top when
|
||||
// the query actually changes instead of once every re-creation event, but sadly
|
||||
// that doesn't seem possible.
|
||||
|
@ -162,7 +162,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
|||
}
|
||||
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
searchAdapter.setPlayingItem(parent ?: song, isPlaying)
|
||||
searchAdapter.setPlaying(parent ?: song, isPlaying)
|
||||
}
|
||||
|
||||
private fun handleNavigation(item: Music?) {
|
||||
|
@ -180,7 +180,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
|||
}
|
||||
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
searchAdapter.setSelectedItems(selected)
|
||||
searchAdapter.setSelected(selected.toSet())
|
||||
if (requireBinding().searchSelectionToolbar.updateSelectionAmount(selected.size) &&
|
||||
selected.isNotEmpty()) {
|
||||
// Make selection of obscured items easier by hiding the keyboard.
|
||||
|
|
|
@ -123,14 +123,15 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
|
|||
if (pkgName == "android") {
|
||||
// No default browser [Must open app chooser, may not be supported]
|
||||
openAppChooser(browserIntent)
|
||||
} else try {
|
||||
browserIntent.setPackage(pkgName)
|
||||
startActivity(browserIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
// Not a browser but an app chooser
|
||||
browserIntent.setPackage(null)
|
||||
openAppChooser(browserIntent)
|
||||
}
|
||||
} else
|
||||
try {
|
||||
browserIntent.setPackage(pkgName)
|
||||
startActivity(browserIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
// Not a browser but an app chooser
|
||||
browserIntent.setPackage(null)
|
||||
openAppChooser(browserIntent)
|
||||
}
|
||||
} else {
|
||||
// No app installed to open the link
|
||||
context.showToast(R.string.err_no_app)
|
||||
|
|
|
@ -75,10 +75,8 @@ interface UISettings : Settings<UISettings.Listener> {
|
|||
var accent = sharedPreferences.getInt(OLD_KEY_ACCENT3, 5)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// Accents were previously frozen as soon as the OS was updated to android
|
||||
// twelve,
|
||||
// as dynamic colors were enabled by default. This is no longer the case, so we
|
||||
// need
|
||||
// to re-update the setting to dynamic colors here.
|
||||
// twelve, as dynamic colors were enabled by default. This is no longer the
|
||||
// case, so we need to re-update the setting to dynamic colors here.
|
||||
accent = 16
|
||||
}
|
||||
|
||||
|
@ -96,7 +94,7 @@ interface UISettings : Settings<UISettings.Listener> {
|
|||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private companion object {
|
||||
const val OLD_KEY_ACCENT3 = "auxio_accent"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,8 @@ fun <T> unlikelyToBeNull(value: T?) =
|
|||
* @return A data casted to [T].
|
||||
* @throws IllegalStateException If the data cannot be casted to [T].
|
||||
*/
|
||||
inline fun <reified T> requireIs(data: Any): T {
|
||||
inline fun <reified T> requireIs(data: Any?): T {
|
||||
requireNotNull(data) { "Unexpected datatype: null" }
|
||||
check(data is T) { "Unexpected datatype: ${data::class.simpleName}" }
|
||||
return data
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
android:background="?attr/colorSurface"
|
||||
android:transitionGroup="true">
|
||||
|
||||
<!-- TODO: Try to align the queue bar with the playback bar. -->
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/explore_nav_host"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
|
|
Loading…
Reference in a new issue