From 0c69a35e8007061fcab4029d319d864bef621e8b Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Wed, 18 Jan 2023 16:45:13 -0700 Subject: [PATCH] detail: diff instead of replace when resorting Completely rework the detail list implementations so that resorting the song list causes a replace operation instead of a diff operation. This finally makes the list experience consistent across the app. --- CHANGELOG.md | 5 +- .../BackportBottomSheetBehavior.java | 2 +- ...BackportMaterialDividerItemDecoration.java | 3 + .../auxio/detail/AlbumDetailFragment.kt | 9 +- .../auxio/detail/ArtistDetailFragment.kt | 5 +- .../java/org/oxycblt/auxio/detail/Detail.kt | 18 ++++ .../oxycblt/auxio/detail/DetailViewModel.kt | 94 +++++++++++++++---- .../auxio/detail/GenreDetailFragment.kt | 5 +- .../detail/recycler/AlbumDetailAdapter.kt | 12 +-- .../detail/recycler/ArtistDetailAdapter.kt | 12 +-- .../auxio/detail/recycler/DetailAdapter.kt | 51 +++++++++- .../detail/recycler/GenreDetailAdapter.kt | 10 +- .../org/oxycblt/auxio/home/HomeViewModel.kt | 41 ++++---- .../auxio/home/list/AlbumListFragment.kt | 11 ++- .../auxio/home/list/ArtistListFragment.kt | 10 +- .../auxio/home/list/GenreListFragment.kt | 11 ++- .../auxio/home/list/SongListFragment.kt | 10 +- .../main/java/org/oxycblt/auxio/list/Data.kt | 1 - .../oxycblt/auxio/list/adapter/AdapterUtil.kt | 42 +++++++++ .../list/{recycler => adapter}/DiffAdapter.kt | 2 +- .../list/{recycler => adapter}/ListDiffer.kt | 52 ++++------ .../PlayingIndicatorAdapter.kt | 2 +- .../SelectionIndicatorAdapter.kt | 2 +- .../SimpleDiffCallback.kt} | 4 +- .../list/recycler/HeaderItemDecoration.kt | 1 + .../auxio/list/recycler/ViewHolders.kt | 12 ++- .../auxio/playback/queue/QueueAdapter.kt | 10 +- .../auxio/playback/queue/QueueFragment.kt | 4 +- .../auxio/playback/queue/QueueViewModel.kt | 10 +- .../org/oxycblt/auxio/search/SearchAdapter.kt | 8 +- .../oxycblt/auxio/search/SearchFragment.kt | 4 +- 31 files changed, 315 insertions(+), 148 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/list/adapter/AdapterUtil.kt rename app/src/main/java/org/oxycblt/auxio/list/{recycler => adapter}/DiffAdapter.kt (98%) rename app/src/main/java/org/oxycblt/auxio/list/{recycler => adapter}/ListDiffer.kt (80%) rename app/src/main/java/org/oxycblt/auxio/list/{recycler => adapter}/PlayingIndicatorAdapter.kt (99%) rename app/src/main/java/org/oxycblt/auxio/list/{recycler => adapter}/SelectionIndicatorAdapter.kt (98%) rename app/src/main/java/org/oxycblt/auxio/list/{recycler/SimpleItemCallback.kt => adapter/SimpleDiffCallback.kt} (91%) diff --git a/CHANGELOG.md b/CHANGELOG.md index beb8fbffd..602d4e17c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ #### What's New - Added ability to play/shuffle selections -- Visually refreshed header components +- Resigned header components - Resigned settings view #### What's Improved @@ -13,6 +13,7 @@ - Pressing the button will now clear the current selection before navigating back - Added support for non-standard `ARTISTS` tags - Play Next and Add To Queue now start playback if there is no queue to add +- Made resorting list animations consistent across app #### What's Fixed - Fixed unreliable ReplayGain adjustment application in certain situations @@ -20,6 +21,8 @@ file manager - Fixed notification not updating due to settings changes - Fixed genre picker from repeatedly showing up when device rotates +- Fixed multi-value genres not being recognized on vorbis files +- Fixed sharp-cornered widget bar appearing even when round mode was enabled #### What's Changed - Implemented new queue system diff --git a/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java b/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java index 4d5da721d..a472ce7ab 100644 --- a/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java +++ b/app/src/main/java/com/google/android/material/bottomsheet/BackportBottomSheetBehavior.java @@ -83,7 +83,7 @@ import java.util.Map; * window-like. For BottomSheetDialog use {@link BottomSheetDialog#setTitle(int)}, and for * BottomSheetDialogFragment use {@link ViewCompat#setAccessibilityPaneTitle(View, CharSequence)}. * - * Modified at several points by Alexander Capehart backport miscellaneous fixes not currently + * Modified at several points by Alexander Capehart to backport miscellaneous fixes not currently * obtainable in the currently used MDC library. */ public class BackportBottomSheetBehavior extends CoordinatorLayout.Behavior { diff --git a/app/src/main/java/com/google/android/material/divider/BackportMaterialDividerItemDecoration.java b/app/src/main/java/com/google/android/material/divider/BackportMaterialDividerItemDecoration.java index 62960dedc..066429513 100644 --- a/app/src/main/java/com/google/android/material/divider/BackportMaterialDividerItemDecoration.java +++ b/app/src/main/java/com/google/android/material/divider/BackportMaterialDividerItemDecoration.java @@ -53,6 +53,9 @@ import com.google.android.material.resources.MaterialResources; * layoutManager.getOrientation()); * recyclerView.addItemDecoration(dividerItemDecoration); * + * + * Modified at several points by Alexander Capehart to backport miscellaneous fixes not currently + * obtainable in the currently used MDC library. */ public class BackportMaterialDividerItemDecoration extends ItemDecoration { public static final int HORIZONTAL = LinearLayout.HORIZONTAL; diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index a395058b3..de445255a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -31,7 +31,6 @@ import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment -import org.oxycblt.auxio.list.recycler.BasicInstructions import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music @@ -141,12 +140,12 @@ class AlbumDetailFragment : override fun onOpenSortMenu(anchor: View) { openMenu(anchor, R.menu.menu_album_sort) { - val sort = detailModel.albumSortSort + val sort = detailModel.albumSongSort unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending setOnMenuItemClickListener { item -> item.isChecked = !item.isChecked - detailModel.albumSortSort = + detailModel.albumSongSort = if (item.itemId == R.id.option_sort_asc) { sort.withAscending(item.isChecked) } else { @@ -260,7 +259,9 @@ class AlbumDetailFragment : } private fun updateList(items: List) { - detailAdapter.submitList(items, BasicInstructions.DIFF) + detailAdapter.submitList( + items, detailModel.albumListInstructions ?: DetailListInstructions.Diff) + detailModel.finishAlbumListInstructions() } private fun updateSelection(selected: List) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index d07d51ab9..3438fa4bd 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -31,7 +31,6 @@ import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter import org.oxycblt.auxio.detail.recycler.DetailAdapter import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment -import org.oxycblt.auxio.list.recycler.BasicInstructions import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music @@ -236,7 +235,9 @@ class ArtistDetailFragment : } private fun updateList(items: List) { - detailAdapter.submitList(items, BasicInstructions.DIFF) + detailAdapter.submitList( + items, detailModel.artistListInstructions ?: DetailListInstructions.Diff) + detailModel.finishArtistListInstructions() } private fun updateSelection(selected: List) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/Detail.kt b/app/src/main/java/org/oxycblt/auxio/detail/Detail.kt index c1a586010..802f8ac3b 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/Detail.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/Detail.kt @@ -25,12 +25,14 @@ import org.oxycblt.auxio.music.storage.MimeType /** * A header variation that displays a button to open a sort menu. * @param titleRes The string resource to use as the header title + * @author Alexander Capehart (OxygenCobalt) */ data class SortHeader(@StringRes val titleRes: Int) : Item /** * A header variation that delimits between disc groups. * @param disc The disc number to be displayed on the header. + * @author Alexander Capehart (OxygenCobalt) */ data class DiscHeader(val disc: Int) : Item @@ -39,9 +41,25 @@ data class DiscHeader(val disc: Int) : Item * @param bitrateKbps The bit rate, in kilobytes-per-second. Null if it could not be parsed. * @param sampleRateHz The sample rate, in hertz. * @param resolvedMimeType The known mime type of the [Song] after it's file format was determined. + * @author Alexander Capehart (OxygenCobalt) */ data class SongProperties( val bitrateKbps: Int?, val sampleRateHz: Int?, val resolvedMimeType: MimeType ) + +/** + * Represents the specific way to update a list of items in the detail lists. + * @author Alexander Capehart (OxygenCobalt) + */ +sealed class DetailListInstructions { + /** Do a plain asynchronous diff. */ + object Diff : DetailListInstructions() + + /** + * Replace all the items starting at the given index. + * @param at The index to start replacing at. + */ + data class ReplaceRest(val at: Int) : DetailListInstructions() +} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index ac009935e..47c2cbc2c 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -78,14 +78,18 @@ class DetailViewModel(application: Application) : /** The current list data derived from [currentAlbum]. */ val albumList: StateFlow> get() = _albumList + /** Specifies how to update [albumList] when it changes. */ + var albumListInstructions: DetailListInstructions? = null + private set /** The current [Sort] used for [Song]s in [albumList]. */ - var albumSortSort: Sort + var albumSongSort: Sort get() = musicSettings.albumSongSort set(value) { musicSettings.albumSongSort = value - // Refresh the album list to reflect the new sort. - currentAlbum.value?.let(::refreshAlbumList) + // Refresh the album list to reflect the new sort. Make sure we only visually replace + // the song information, however. + currentAlbum.value?.let { refreshAlbumList(it, true) } } // --- ARTIST --- @@ -98,14 +102,18 @@ class DetailViewModel(application: Application) : private val _artistList = MutableStateFlow(listOf()) /** The current list derived from [currentArtist]. */ val artistList: StateFlow> = _artistList + /** Specifies how to update [artistList] when it changes. */ + var artistListInstructions: DetailListInstructions? = null + private set /** The current [Sort] used for [Song]s in [artistList]. */ var artistSongSort: Sort get() = musicSettings.artistSongSort set(value) { musicSettings.artistSongSort = value - // Refresh the artist list to reflect the new sort. - currentArtist.value?.let(::refreshArtistList) + // Refresh the artist list to reflect the new sort. Make sure we only visually replace + // the song information, however. + currentArtist.value?.let { refreshArtistList(it, true) } } // --- GENRE --- @@ -118,14 +126,18 @@ class DetailViewModel(application: Application) : private val _genreList = MutableStateFlow(listOf()) /** The current list data derived from [currentGenre]. */ val genreList: StateFlow> = _genreList + /** Specifies how to update [genreList] when it changes. */ + var genreListInstructions: DetailListInstructions? = null + private set /** The current [Sort] used for [Song]s in [genreList]. */ var genreSongSort: Sort get() = musicSettings.genreSongSort set(value) { musicSettings.genreSongSort = value - // Refresh the genre list to reflect the new sort. - currentGenre.value?.let(::refreshGenreList) + // Refresh the genre list to reflect the new sort. Make sure we only visually replace + // the song information, however. + currentGenre.value?.let { refreshGenreList(it, true) } } /** @@ -161,19 +173,19 @@ class DetailViewModel(application: Application) : val album = currentAlbum.value if (album != null) { - _currentAlbum.value = library.sanitize(album)?.also(::refreshAlbumList) + _currentAlbum.value = library.sanitize(album)?.also { refreshAlbumList(it, false) } logD("Updated genre to ${currentAlbum.value}") } val artist = currentArtist.value if (artist != null) { - _currentArtist.value = library.sanitize(artist)?.also(::refreshArtistList) + _currentArtist.value = library.sanitize(artist)?.also { refreshArtistList(it, false) } logD("Updated genre to ${currentArtist.value}") } val genre = currentGenre.value if (genre != null) { - _currentGenre.value = library.sanitize(genre)?.also(::refreshGenreList) + _currentGenre.value = library.sanitize(genre)?.also { refreshGenreList(it, false) } logD("Updated genre to ${currentGenre.value}") } } @@ -203,7 +215,7 @@ class DetailViewModel(application: Application) : return } logD("Opening Album [uid: $uid]") - _currentAlbum.value = requireMusic(uid)?.also(::refreshAlbumList) + _currentAlbum.value = requireMusic(uid)?.also { refreshAlbumList(it, false) } } /** @@ -217,7 +229,7 @@ class DetailViewModel(application: Application) : return } logD("Opening Artist [uid: $uid]") - _currentArtist.value = requireMusic(uid)?.also(::refreshArtistList) + _currentArtist.value = requireMusic(uid)?.also { refreshArtistList(it, false) } } /** @@ -231,7 +243,29 @@ class DetailViewModel(application: Application) : return } logD("Opening Genre [uid: $uid]") - _currentGenre.value = requireMusic(uid)?.also(::refreshGenreList) + _currentGenre.value = requireMusic(uid)?.also { refreshGenreList(it, false) } + } + + /** + * Signal that the specified [DetailListInstructions] in [albumListInstructions] were performed. + */ + fun finishAlbumListInstructions() { + albumListInstructions = null + } + + /** + * Signal that the specified [DetailListInstructions] in [artistListInstructions] were + * performed. + */ + fun finishArtistListInstructions() { + artistListInstructions = null + } + + /** + * Signal that the specified [DetailListInstructions] in [genreListInstructions] were performed. + */ + fun finishGenreListInstructions() { + genreListInstructions = null } private fun requireMusic(uid: Music.UID) = musicStore.library?.find(uid) @@ -314,14 +348,14 @@ class DetailViewModel(application: Application) : return SongProperties(bitrate, sampleRate, resolvedMimeType) } - private fun refreshAlbumList(album: Album) { + private fun refreshAlbumList(album: Album, replace: Boolean): Int { logD("Refreshing album data") val data = mutableListOf(album) data.add(SortHeader(R.string.lbl_songs)) - + val songsStartIndex = data.size // To create a good user experience regarding disc numbers, we group the album's // songs up by disc and then delimit the groups by a disc header. - val songs = albumSortSort.songs(album.songs) + val songs = albumSongSort.songs(album.songs) // Songs without disc tags become part of Disc 1. val byDisc = songs.groupBy { it.disc ?: 1 } if (byDisc.size > 1) { @@ -334,11 +368,17 @@ class DetailViewModel(application: Application) : // Album only has one disc, don't add any redundant headers data.addAll(songs) } - + albumListInstructions = + if (replace) { + DetailListInstructions.ReplaceRest(songsStartIndex) + } else { + DetailListInstructions.Diff + } _albumList.value = data + return songsStartIndex } - private fun refreshArtistList(artist: Artist) { + private fun refreshArtistList(artist: Artist, replace: Boolean) { logD("Refreshing artist data") val data = mutableListOf(artist) val albums = Sort(Sort.Mode.ByDate, false).albums(artist.albums) @@ -371,24 +411,40 @@ class DetailViewModel(application: Application) : data.addAll(entry.value) } + var songsStartIndex: Int? = null // Artists may not be linked to any songs, only include a header entry if we have any. if (artist.songs.isNotEmpty()) { logD("Songs present in this artist, adding header") data.add(SortHeader(R.string.lbl_songs)) + songsStartIndex = data.size data.addAll(artistSongSort.songs(artist.songs)) } + artistListInstructions = + if (replace) { + DetailListInstructions.ReplaceRest( + requireNotNull(songsStartIndex) { "Cannot replace empty artist song list" }) + } else { + DetailListInstructions.Diff + } _artistList.value = data.toList() } - private fun refreshGenreList(genre: Genre) { + private fun refreshGenreList(genre: Genre, replace: Boolean) { logD("Refreshing genre data") val data = mutableListOf(genre) // Genre is guaranteed to always have artists and songs. data.add(Header(R.string.lbl_artists)) data.addAll(genre.artists) data.add(SortHeader(R.string.lbl_songs)) + val songsStartIndex = data.size data.addAll(genreSongSort.songs(genre.songs)) + genreListInstructions = + if (replace) { + DetailListInstructions.ReplaceRest(songsStartIndex) + } else { + DetailListInstructions.Diff + } _genreList.value = data } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 15c7bed0f..65603b4bf 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -31,7 +31,6 @@ import org.oxycblt.auxio.detail.recycler.DetailAdapter import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment -import org.oxycblt.auxio.list.recycler.BasicInstructions import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre @@ -219,7 +218,9 @@ class GenreDetailFragment : } private fun updateList(items: List) { - detailAdapter.submitList(items, BasicInstructions.DIFF) + detailAdapter.submitList( + items, detailModel.genreListInstructions ?: DetailListInstructions.Diff) + detailModel.finishGenreListInstructions() } private fun updateSelection(selected: List) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt index 6693ca754..6a8611cb7 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt @@ -29,8 +29,8 @@ import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding import org.oxycblt.auxio.detail.DiscHeader import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.SelectableListListener -import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter -import org.oxycblt.auxio.list.recycler.SimpleItemCallback +import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter +import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.formatDurationMs @@ -94,7 +94,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene private companion object { /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = - object : SimpleItemCallback() { + object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { return when { oldItem is Album && newItem is Album -> @@ -169,7 +169,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = - object : SimpleItemCallback() { + object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Album, newItem: Album) = oldItem.rawName == newItem.rawName && oldItem.areArtistContentsTheSame(newItem) && @@ -210,7 +210,7 @@ private class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) : /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = - object : SimpleItemCallback() { + object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: DiscHeader, newItem: DiscHeader) = oldItem.disc == newItem.disc } @@ -277,7 +277,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = - object : SimpleItemCallback() { + object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Song, newItem: Song) = oldItem.rawName == newItem.rawName && oldItem.durationMs == newItem.durationMs } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt index 29c994c65..e706efc23 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt @@ -28,8 +28,8 @@ import org.oxycblt.auxio.databinding.ItemParentBinding import org.oxycblt.auxio.databinding.ItemSongBinding import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.SelectableListListener -import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter -import org.oxycblt.auxio.list.recycler.SimpleItemCallback +import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter +import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music @@ -83,7 +83,7 @@ class ArtistDetailAdapter(private val listener: Listener) : private companion object { /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = - object : SimpleItemCallback() { + object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { return when { oldItem is Artist && newItem is Artist -> @@ -165,7 +165,7 @@ private class ArtistDetailViewHolder private constructor(private val binding: It /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = - object : SimpleItemCallback() { + object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Artist, newItem: Artist) = oldItem.rawName == newItem.rawName && oldItem.areGenreContentsTheSame(newItem) && @@ -220,7 +220,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = - object : SimpleItemCallback() { + object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Album, newItem: Album) = oldItem.rawName == newItem.rawName && oldItem.dates == newItem.dates } @@ -269,7 +269,7 @@ private class ArtistSongViewHolder private constructor(private val binding: Item /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = - object : SimpleItemCallback() { + object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Song, newItem: Song) = oldItem.rawName == newItem.rawName && oldItem.album.rawName == newItem.album.rawName diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt index 19ee4da86..70541a004 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt @@ -20,16 +20,24 @@ package org.oxycblt.auxio.detail.recycler import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.TooltipCompat +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 org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.databinding.ItemSortHeaderBinding +import org.oxycblt.auxio.detail.DetailListInstructions import org.oxycblt.auxio.detail.SortHeader import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.SelectableListListener +import org.oxycblt.auxio.list.adapter.ListDiffer +import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter +import org.oxycblt.auxio.list.adapter.SimpleDiffCallback +import org.oxycblt.auxio.list.adapter.overwriteList import org.oxycblt.auxio.list.recycler.* -import org.oxycblt.auxio.list.recycler.BasicInstructions import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater @@ -45,8 +53,8 @@ abstract class DetailAdapter( private val listener: Listener<*>, diffCallback: DiffUtil.ItemCallback ) : - SelectionIndicatorAdapter( - ListDiffer.Async(diffCallback)), + SelectionIndicatorAdapter( + DetailListDiffer.Factory(diffCallback)), AuxioRecyclerView.SpanSizeLookup { override fun getItemViewType(position: Int) = @@ -102,7 +110,7 @@ abstract class DetailAdapter( protected companion object { /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = - object : SimpleItemCallback() { + object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { return when { oldItem is Header && newItem is Header -> @@ -116,6 +124,39 @@ abstract class DetailAdapter( } } +private class DetailListDiffer( + private val updateCallback: ListUpdateCallback, + diffCallback: DiffUtil.ItemCallback +) : ListDiffer { + private val inner = + AsyncListDiffer(updateCallback, AsyncDifferConfig.Builder(diffCallback).build()) + + override val currentList: List + get() = inner.currentList + + override fun submitList( + newList: List, + instructions: DetailListInstructions, + onDone: () -> Unit + ) { + when (instructions) { + is DetailListInstructions.Diff -> inner.submitList(newList, onDone) + is DetailListInstructions.ReplaceRest -> { + val amount = newList.size - instructions.at + updateCallback.onRemoved(instructions.at, amount) + inner.overwriteList(newList) + updateCallback.onInserted(instructions.at, amount) + } + } + } + + class Factory(private val diffCallback: DiffUtil.ItemCallback) : + ListDiffer.Factory() { + override fun new(adapter: RecyclerView.Adapter<*>) = + DetailListDiffer(AdapterListUpdateCallback(adapter), diffCallback) + } +} + /** * A [RecyclerView.ViewHolder] that displays a [SortHeader], a variation on [Header] that adds a * button opening a menu for sorting. Use [from] to create an instance. @@ -152,7 +193,7 @@ private class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) : /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = - object : SimpleItemCallback() { + object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: SortHeader, newItem: SortHeader) = oldItem.titleRes == newItem.titleRes } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt index 93eed81c7..51a86f335 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt @@ -25,8 +25,8 @@ import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemDetailBinding import org.oxycblt.auxio.list.Item +import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.list.recycler.ArtistViewHolder -import org.oxycblt.auxio.list.recycler.SimpleItemCallback import org.oxycblt.auxio.list.recycler.SongViewHolder import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre @@ -46,8 +46,8 @@ class GenreDetailAdapter(private val listener: Listener) : override fun getItemViewType(position: Int) = 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. + // a genre that will make the artists/songs specially formatted, so it doesn't matter + // what we use for their ViewHolders. is Genre -> GenreDetailViewHolder.VIEW_TYPE is Artist -> ArtistViewHolder.VIEW_TYPE is Song -> SongViewHolder.VIEW_TYPE @@ -81,7 +81,7 @@ class GenreDetailAdapter(private val listener: Listener) : private companion object { val DIFF_CALLBACK = - object : SimpleItemCallback() { + object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { return when { oldItem is Genre && newItem is Genre -> @@ -139,7 +139,7 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = - object : SimpleItemCallback() { + object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Genre, newItem: Genre) = oldItem.rawName == newItem.rawName && oldItem.songs.size == newItem.songs.size && diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index e6d0237dc..fa299eaad 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -22,7 +22,7 @@ import androidx.lifecycle.AndroidViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.home.tabs.Tab -import org.oxycblt.auxio.list.recycler.BasicInstructions +import org.oxycblt.auxio.list.adapter.BasicListInstructions import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.library.Library @@ -46,7 +46,7 @@ class HomeViewModel(application: Application) : val songsList: StateFlow> get() = _songsList /** Specifies how to update [songsList] when it changes. */ - var songsListInstructions: BasicInstructions? = null + var songsListInstructions: BasicListInstructions? = null private set private val _albumsLists = MutableStateFlow(listOf()) @@ -54,7 +54,7 @@ class HomeViewModel(application: Application) : val albumsList: StateFlow> get() = _albumsLists /** Specifies how to update [albumsList] when it changes. */ - var albumsListInstructions: BasicInstructions? = null + var albumsListInstructions: BasicListInstructions? = null private set private val _artistsList = MutableStateFlow(listOf()) @@ -66,7 +66,7 @@ class HomeViewModel(application: Application) : val artistsList: MutableStateFlow> get() = _artistsList /** Specifies how to update [artistsList] when it changes. */ - var artistsListInstructions: BasicInstructions? = null + var artistsListInstructions: BasicListInstructions? = null private set private val _genresList = MutableStateFlow(listOf()) @@ -74,7 +74,7 @@ class HomeViewModel(application: Application) : val genresList: StateFlow> get() = _genresList /** Specifies how to update [genresList] when it changes. */ - var genresListInstructions: BasicInstructions? = null + var genresListInstructions: BasicListInstructions? = null private set /** The [MusicMode] to use when playing a [Song] from the UI. */ @@ -120,11 +120,11 @@ class HomeViewModel(application: Application) : logD("Library changed, refreshing library") // Get the each list of items in the library to use as our list data. // Applying the preferred sorting to them. - songsListInstructions = BasicInstructions.DIFF + songsListInstructions = BasicListInstructions.DIFF _songsList.value = musicSettings.songSort.songs(library.songs) - albumsListInstructions = BasicInstructions.DIFF + albumsListInstructions = BasicListInstructions.DIFF _albumsLists.value = musicSettings.albumSort.albums(library.albums) - artistsListInstructions = BasicInstructions.DIFF + artistsListInstructions = BasicListInstructions.DIFF _artistsList.value = musicSettings.artistSort.artists( if (homeSettings.shouldHideCollaborators) { @@ -133,7 +133,7 @@ class HomeViewModel(application: Application) : } else { library.artists }) - genresListInstructions = BasicInstructions.DIFF + genresListInstructions = BasicListInstructions.DIFF _genresList.value = musicSettings.genreSort.genres(library.genres) } } @@ -173,45 +173,52 @@ class HomeViewModel(application: Application) : when (_currentTabMode.value) { MusicMode.SONGS -> { musicSettings.songSort = sort - songsListInstructions = BasicInstructions.REPLACE + songsListInstructions = BasicListInstructions.REPLACE _songsList.value = sort.songs(_songsList.value) } MusicMode.ALBUMS -> { musicSettings.albumSort = sort - albumsListInstructions = BasicInstructions.REPLACE + albumsListInstructions = BasicListInstructions.REPLACE _albumsLists.value = sort.albums(_albumsLists.value) } MusicMode.ARTISTS -> { musicSettings.artistSort = sort - artistsListInstructions = BasicInstructions.REPLACE + artistsListInstructions = BasicListInstructions.REPLACE _artistsList.value = sort.artists(_artistsList.value) } MusicMode.GENRES -> { musicSettings.genreSort = sort - genresListInstructions = BasicInstructions.REPLACE + genresListInstructions = BasicListInstructions.REPLACE _genresList.value = sort.genres(_genresList.value) } } } - /** Signal that the specified [BasicInstructions] in [songsListInstructions] were performed. */ + /** + * Signal that the specified [BasicListInstructions] in [songsListInstructions] were performed. + */ fun finishSongsListInstructions() { songsListInstructions = null } - /** Signal that the specified [BasicInstructions] in [albumsListInstructions] were performed. */ + /** + * Signal that the specified [BasicListInstructions] in [albumsListInstructions] were performed. + */ fun finishAlbumsListInstructions() { albumsListInstructions = null } /** - * Signal that the specified [BasicInstructions] in [artistsListInstructions] were performed. + * Signal that the specified [BasicListInstructions] in [artistsListInstructions] were + * performed. */ fun finishArtistsListInstructions() { artistsListInstructions = null } - /** Signal that the specified [BasicInstructions] in [genresListInstructions] were performed. */ + /** + * Signal that the specified [BasicListInstructions] in [genresListInstructions] were performed. + */ fun finishGenresListInstructions() { genresListInstructions = null } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt index cd030d36a..ff0e1fc06 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt @@ -30,10 +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.adapter.BasicListInstructions +import org.oxycblt.auxio.list.adapter.ListDiffer +import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.AlbumViewHolder -import org.oxycblt.auxio.list.recycler.BasicInstructions -import org.oxycblt.auxio.list.recycler.ListDiffer -import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.library.Sort import org.oxycblt.auxio.playback.formatDurationMs @@ -132,7 +132,8 @@ class AlbumListFragment : } private fun updateList(albums: List) { - albumAdapter.submitList(albums, homeModel.albumsListInstructions ?: BasicInstructions.DIFF) + albumAdapter.submitList( + albums, homeModel.albumsListInstructions ?: BasicListInstructions.DIFF) homeModel.finishAlbumsListInstructions() } @@ -150,7 +151,7 @@ class AlbumListFragment : * @param listener An [SelectableListListener] to bind interactions to. */ private class AlbumAdapter(private val listener: SelectableListListener) : - SelectionIndicatorAdapter( + SelectionIndicatorAdapter( ListDiffer.Async(AlbumViewHolder.DIFF_CALLBACK)) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt index 02592d916..8a9665b5b 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt @@ -28,10 +28,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.adapter.BasicListInstructions +import org.oxycblt.auxio.list.adapter.ListDiffer +import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.ArtistViewHolder -import org.oxycblt.auxio.list.recycler.BasicInstructions -import org.oxycblt.auxio.list.recycler.ListDiffer -import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicMode @@ -111,7 +111,7 @@ class ArtistListFragment : private fun updateList(artists: List) { artistAdapter.submitList( - artists, homeModel.artistsListInstructions ?: BasicInstructions.DIFF) + artists, homeModel.artistsListInstructions ?: BasicListInstructions.DIFF) homeModel.finishArtistsListInstructions() } @@ -129,7 +129,7 @@ class ArtistListFragment : * @param listener An [SelectableListListener] to bind interactions to. */ private class ArtistAdapter(private val listener: SelectableListListener) : - SelectionIndicatorAdapter( + SelectionIndicatorAdapter( ListDiffer.Async(ArtistViewHolder.DIFF_CALLBACK)) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt index a74df76fa..e69641f8b 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt @@ -28,10 +28,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.BasicInstructions +import org.oxycblt.auxio.list.adapter.BasicListInstructions +import org.oxycblt.auxio.list.adapter.ListDiffer +import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter 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.music.Genre import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicMode @@ -109,7 +109,8 @@ class GenreListFragment : } private fun updateList(artists: List) { - genreAdapter.submitList(artists, homeModel.genresListInstructions ?: BasicInstructions.DIFF) + genreAdapter.submitList( + artists, homeModel.genresListInstructions ?: BasicListInstructions.DIFF) homeModel.finishGenresListInstructions() } @@ -127,7 +128,7 @@ class GenreListFragment : * @param listener An [SelectableListListener] to bind interactions to. */ private class GenreAdapter(private val listener: SelectableListListener) : - SelectionIndicatorAdapter( + SelectionIndicatorAdapter( ListDiffer.Async(GenreViewHolder.DIFF_CALLBACK)) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = GenreViewHolder.from(parent) diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index b21b72ec7..b09bdf907 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -30,9 +30,9 @@ 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.BasicInstructions -import org.oxycblt.auxio.list.recycler.ListDiffer -import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter +import org.oxycblt.auxio.list.adapter.BasicListInstructions +import org.oxycblt.auxio.list.adapter.ListDiffer +import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.SongViewHolder import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicMode @@ -139,7 +139,7 @@ class SongListFragment : } private fun updateList(songs: List) { - songAdapter.submitList(songs, homeModel.songsListInstructions ?: BasicInstructions.DIFF) + songAdapter.submitList(songs, homeModel.songsListInstructions ?: BasicListInstructions.DIFF) homeModel.finishSongsListInstructions() } @@ -161,7 +161,7 @@ class SongListFragment : * @param listener An [SelectableListListener] to bind interactions to. */ private class SongAdapter(private val listener: SelectableListListener) : - SelectionIndicatorAdapter( + SelectionIndicatorAdapter( ListDiffer.Async(SongViewHolder.DIFF_CALLBACK)) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = diff --git a/app/src/main/java/org/oxycblt/auxio/list/Data.kt b/app/src/main/java/org/oxycblt/auxio/list/Data.kt index dc7741062..878a6a9d3 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Data.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Data.kt @@ -25,6 +25,5 @@ interface Item /** * A "header" used for delimiting groups of data. * @param titleRes The string resource used for the header's title. - * @param withDivider Whether to show a divider on the item. */ data class Header(@StringRes val titleRes: Int) : Item diff --git a/app/src/main/java/org/oxycblt/auxio/list/adapter/AdapterUtil.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/AdapterUtil.kt new file mode 100644 index 000000000..4e40e6191 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/AdapterUtil.kt @@ -0,0 +1,42 @@ +/* + * 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 . + */ + +package org.oxycblt.auxio.list.adapter + +import androidx.recyclerview.widget.AsyncListDiffer +import java.lang.reflect.Field +import org.oxycblt.auxio.util.lazyReflectedField +import org.oxycblt.auxio.util.requireIs + +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") + +/** + * Force-update an [AsyncListDiffer] with new data. It's hard to state how incredibly dangerous this + * is, so only use it when absolutely necessary. + * @param newList The new list to write to the [AsyncListDiffer]. + */ +fun AsyncListDiffer.overwriteList(newList: List) { + // Should update the generation field to prevent any previous jobs from conflicting, then + // updates the mutable list to it's nullable value, and then updates the read-only list to + // it's non-nullable value. + ASD_MAX_GENERATION_FIELD.set(this, requireIs(ASD_MAX_GENERATION_FIELD.get(this)) + 1) + ASD_MUTABLE_LIST_FIELD.set(this, newList.ifEmpty { null }) + ASD_READ_ONLY_LIST_FIELD.set(this, newList) +} diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/DiffAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/DiffAdapter.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/list/recycler/DiffAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/list/adapter/DiffAdapter.kt index 7642635a2..23e49344d 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/DiffAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/DiffAdapter.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.list.recycler +package org.oxycblt.auxio.list.adapter import androidx.recyclerview.widget.RecyclerView diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/ListDiffer.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/ListDiffer.kt similarity index 80% rename from app/src/main/java/org/oxycblt/auxio/list/recycler/ListDiffer.kt rename to app/src/main/java/org/oxycblt/auxio/list/adapter/ListDiffer.kt index ab6e3517f..ece76956d 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/ListDiffer.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/ListDiffer.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.list.recycler +package org.oxycblt.auxio.list.adapter import androidx.recyclerview.widget.AdapterListUpdateCallback import androidx.recyclerview.widget.AsyncDifferConfig @@ -23,9 +23,6 @@ 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. @@ -38,7 +35,7 @@ interface ListDiffer { /** * Dynamically determine how to update the list based on the given instructions. * @param newList The new list of [T] items to show. - * @param instructions The [BasicInstructions] specifying how to update the list. + * @param instructions The [BasicListInstructions] specifying how to update the list. * @param onDone Called when the update process is completed. */ fun submitList(newList: List, instructions: I, onDone: () -> Unit) @@ -62,20 +59,20 @@ interface ListDiffer { * internal list. */ class Async(private val diffCallback: DiffUtil.ItemCallback) : - Factory() { - override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer = + Factory() { + override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer = 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. + * likely to occur that would cause [Async] to suffer from race conditions. * @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the * internal list. */ class Blocking(private val diffCallback: DiffUtil.ItemCallback) : - Factory() { - override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer = + Factory() { + override fun new(adapter: RecyclerView.Adapter<*>): ListDiffer = RealBlockingListDiffer(AdapterListUpdateCallback(adapter), diffCallback) } } @@ -84,7 +81,7 @@ interface ListDiffer { * Represents the specific way to update a list of items. * @author Alexander Capehart (OxygenCobalt) */ -enum class BasicInstructions { +enum class BasicListInstructions { /** * (A)synchronously diff the list. This should be used for small diffs with little item * movement. @@ -98,11 +95,15 @@ enum class BasicInstructions { REPLACE } -private abstract class RealListDiffer() : ListDiffer { - override fun submitList(newList: List, instructions: BasicInstructions, onDone: () -> Unit) { +private abstract class BasicListDiffer() : ListDiffer { + override fun submitList( + newList: List, + instructions: BasicListInstructions, + onDone: () -> Unit + ) { when (instructions) { - BasicInstructions.DIFF -> diffList(newList, onDone) - BasicInstructions.REPLACE -> replaceList(newList, onDone) + BasicListInstructions.DIFF -> diffList(newList, onDone) + BasicListInstructions.REPLACE -> replaceList(newList, onDone) } } @@ -113,7 +114,7 @@ private abstract class RealListDiffer() : ListDiffer { private class RealAsyncListDiffer( private val updateCallback: ListUpdateCallback, diffCallback: DiffUtil.ItemCallback -) : RealListDiffer() { +) : BasicListDiffer() { private val inner = AsyncListDiffer(updateCallback, AsyncDifferConfig.Builder(diffCallback).build()) @@ -126,34 +127,19 @@ private class RealAsyncListDiffer( override fun replaceList(newList: List, onDone: () -> Unit) { if (inner.currentList != newList) { - // 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(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) + inner.overwriteList(newList) updateCallback.onInserted(0, newList.size) } onDone() } - - 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( private val updateCallback: ListUpdateCallback, private val diffCallback: DiffUtil.ItemCallback -) : RealListDiffer() { +) : BasicListDiffer() { override var currentList = listOf() override fun diffList(newList: List, onDone: () -> Unit) { diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt similarity index 99% rename from app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt index 00b151e29..588c5c9b6 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/PlayingIndicatorAdapter.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.list.recycler +package org.oxycblt.auxio.list.adapter import android.view.View import androidx.recyclerview.widget.RecyclerView diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt index a3ab91222..20239546c 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/SelectionIndicatorAdapter.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.list.recycler +package org.oxycblt.auxio.list.adapter import android.view.View import androidx.recyclerview.widget.RecyclerView diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/SimpleItemCallback.kt b/app/src/main/java/org/oxycblt/auxio/list/adapter/SimpleDiffCallback.kt similarity index 91% rename from app/src/main/java/org/oxycblt/auxio/list/recycler/SimpleItemCallback.kt rename to app/src/main/java/org/oxycblt/auxio/list/adapter/SimpleDiffCallback.kt index 9a289fc88..358c3b3f1 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/SimpleItemCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/adapter/SimpleDiffCallback.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.list.recycler +package org.oxycblt.auxio.list.adapter import androidx.recyclerview.widget.DiffUtil import org.oxycblt.auxio.list.Item @@ -25,6 +25,6 @@ import org.oxycblt.auxio.list.Item * whenever creating [DiffUtil.ItemCallback] implementations with an [Item] subclass. * @author Alexander Capehart (OxygenCobalt) */ -abstract class SimpleItemCallback : DiffUtil.ItemCallback() { +abstract class SimpleDiffCallback : DiffUtil.ItemCallback() { final override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem == newItem } diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/HeaderItemDecoration.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/HeaderItemDecoration.kt index 97eb278b8..b715b8b70 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/HeaderItemDecoration.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/HeaderItemDecoration.kt @@ -24,6 +24,7 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.divider.BackportMaterialDividerItemDecoration import org.oxycblt.auxio.R import org.oxycblt.auxio.list.Header +import org.oxycblt.auxio.list.adapter.DiffAdapter /** * A [BackportMaterialDividerItemDecoration] that sets up the divider configuration to correctly diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt index ef4075b67..d72bf6814 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt @@ -26,6 +26,8 @@ import org.oxycblt.auxio.databinding.ItemParentBinding import org.oxycblt.auxio.databinding.ItemSongBinding import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.SelectableListListener +import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter +import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.music.* import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getPlural @@ -72,7 +74,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) : /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = - object : SimpleItemCallback() { + object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Song, newItem: Song) = oldItem.rawName == newItem.rawName && oldItem.areArtistContentsTheSame(newItem) } @@ -119,7 +121,7 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = - object : SimpleItemCallback() { + object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Album, newItem: Album) = oldItem.rawName == newItem.rawName && oldItem.areArtistContentsTheSame(newItem) && @@ -178,7 +180,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = - object : SimpleItemCallback() { + object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Artist, newItem: Artist) = oldItem.rawName == newItem.rawName && oldItem.albums.size == newItem.albums.size && @@ -231,7 +233,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = - object : SimpleItemCallback() { + object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Genre, newItem: Genre): Boolean = oldItem.rawName == newItem.rawName && oldItem.songs.size == newItem.songs.size } @@ -267,7 +269,7 @@ class HeaderViewHolder private constructor(private val binding: ItemHeaderBindin /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = - object : SimpleItemCallback
() { + object : SimpleDiffCallback
() { override fun areContentsTheSame(oldItem: Header, newItem: Header): Boolean = oldItem.titleRes == newItem.titleRes } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt index a6cb99aa4..1898af497 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt @@ -27,10 +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.BasicInstructions -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.adapter.BasicListInstructions +import org.oxycblt.auxio.list.adapter.DiffAdapter +import org.oxycblt.auxio.list.adapter.ListDiffer +import org.oxycblt.auxio.list.adapter.PlayingIndicatorAdapter import org.oxycblt.auxio.list.recycler.SongViewHolder import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.util.* @@ -41,7 +41,7 @@ import org.oxycblt.auxio.util.* * @author Alexander Capehart (OxygenCobalt) */ class QueueAdapter(private val listener: EditableListListener) : - DiffAdapter( + DiffAdapter( 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 diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt index 3e705ced0..9f8feb3d8 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt @@ -27,7 +27,7 @@ import androidx.recyclerview.widget.RecyclerView import kotlin.math.min import org.oxycblt.auxio.databinding.FragmentQueueBinding import org.oxycblt.auxio.list.EditableListListener -import org.oxycblt.auxio.list.recycler.BasicInstructions +import org.oxycblt.auxio.list.adapter.BasicListInstructions import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.ViewBindingFragment @@ -101,7 +101,7 @@ class QueueFragment : ViewBindingFragment(), EditableListL // Replace or diff the queue depending on the type of change it is. val instructions = queueModel.instructions - queueAdapter.submitList(queue, instructions?.update ?: BasicInstructions.DIFF) + queueAdapter.submitList(queue, instructions?.update ?: BasicListInstructions.DIFF) // Update position in list (and thus past/future items) queueAdapter.setPosition(index, isPlaying) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt index c571d7846..c97beabc0 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt @@ -20,7 +20,7 @@ package org.oxycblt.auxio.playback.queue import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import org.oxycblt.auxio.list.recycler.BasicInstructions +import org.oxycblt.auxio.list.adapter.BasicListInstructions import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackStateManager @@ -57,7 +57,7 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener { override fun onQueueChanged(queue: Queue, change: Queue.ChangeResult) { // Queue changed trivially due to item mo -> Diff queue, stay at current index. - instructions = Instructions(BasicInstructions.DIFF, null) + instructions = Instructions(BasicListInstructions.DIFF, null) _queue.value = queue.resolve() if (change != Queue.ChangeResult.MAPPING) { // Index changed, make sure it remains updated without actually scrolling to it. @@ -67,14 +67,14 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener { override fun onQueueReordered(queue: Queue) { // Queue changed completely -> Replace queue, update index - instructions = Instructions(BasicInstructions.REPLACE, queue.index) + instructions = Instructions(BasicListInstructions.REPLACE, queue.index) _queue.value = queue.resolve() _index.value = queue.index } override fun onNewPlayback(queue: Queue, parent: MusicParent?) { // Entirely new queue -> Replace queue, update index - instructions = Instructions(BasicInstructions.REPLACE, queue.index) + instructions = Instructions(BasicListInstructions.REPLACE, queue.index) _queue.value = queue.resolve() _index.value = queue.index } @@ -124,5 +124,5 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener { instructions = null } - class Instructions(val update: BasicInstructions?, val scrollTo: Int?) + class Instructions(val update: BasicListInstructions?, val scrollTo: Int?) } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt index 0bf1ff72a..62c157bd8 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt @@ -20,6 +20,10 @@ package org.oxycblt.auxio.search import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.list.* +import org.oxycblt.auxio.list.adapter.BasicListInstructions +import org.oxycblt.auxio.list.adapter.ListDiffer +import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter +import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.list.recycler.* import org.oxycblt.auxio.music.* import org.oxycblt.auxio.util.logD @@ -30,7 +34,7 @@ import org.oxycblt.auxio.util.logD * @author Alexander Capehart (OxygenCobalt) */ class SearchAdapter(private val listener: SelectableListListener) : - SelectionIndicatorAdapter( + SelectionIndicatorAdapter( ListDiffer.Async(DIFF_CALLBACK)), AuxioRecyclerView.SpanSizeLookup { @@ -79,7 +83,7 @@ class SearchAdapter(private val listener: SelectableListListener) : val PAYLOAD_UPDATE_DIVIDER = 102249124 /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = - object : SimpleItemCallback() { + object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Item, newItem: Item) = when { oldItem is Song && newItem is Song -> diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index 6348419dc..eeaafb214 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -31,7 +31,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentSearchBinding import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment -import org.oxycblt.auxio.list.recycler.BasicInstructions +import org.oxycblt.auxio.list.adapter.BasicListInstructions import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre @@ -154,7 +154,7 @@ class SearchFragment : ListFragment() { // 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(), BasicInstructions.DIFF) { + searchAdapter.submitList(results.toMutableList(), BasicListInstructions.DIFF) { // 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.