recycler: spin off data into separate class
Spin off the data instances into their own class called BackingData. This is to isolate the sane adapters that rely on one type of diffing from the insane adapters that use synchronous and asynchronous diffing simultaniously. It also allows some of the more esoteric adapters to implement their own backing data without much trouble or leaky abstractions.
This commit is contained in:
parent
ee1a234e76
commit
8100f294d7
22 changed files with 346 additions and 204 deletions
|
@ -22,6 +22,8 @@ import coil.size.Size
|
|||
import coil.size.pxOrElse
|
||||
import coil.transform.Transformation
|
||||
import kotlin.math.min
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logE
|
||||
|
||||
/**
|
||||
* A transformation that performs a center crop-style transformation on an image, however unlike the
|
||||
|
@ -45,8 +47,13 @@ class SquareFrameTransform : Transformation {
|
|||
val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize)
|
||||
|
||||
if (dstSize != desiredWidth || dstSize != desiredHeight) {
|
||||
// Desired size differs from the cropped size, resize the bitmap.
|
||||
return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)
|
||||
logD("RETARD YOU STUPID FUCKING IDIOT $desiredWidth $desiredHeight")
|
||||
try {
|
||||
// Desired size differs from the cropped size, resize the bitmap.
|
||||
return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)
|
||||
} catch (e: Exception) {
|
||||
logE(e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
|
||||
return dst
|
||||
|
|
|
@ -73,14 +73,16 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailItemListener {
|
|||
requireBinding().detailRecycler.apply {
|
||||
adapter = detailAdapter
|
||||
applySpans { pos ->
|
||||
val item = detailAdapter.currentList[pos]
|
||||
val item = detailAdapter.data.currentList[pos]
|
||||
item is Header || item is SortHeader || item is Album
|
||||
}
|
||||
}
|
||||
|
||||
// -- VIEWMODEL SETUP ---
|
||||
|
||||
detailModel.albumData.observe(viewLifecycleOwner) { list -> detailAdapter.submitList(list) }
|
||||
detailModel.albumData.observe(viewLifecycleOwner) { list ->
|
||||
detailAdapter.data.submitList(list)
|
||||
}
|
||||
|
||||
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
||||
handleNavigation(item, detailAdapter)
|
||||
|
@ -168,7 +170,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailItemListener {
|
|||
/** Scroll to an song using its [id]. */
|
||||
private fun scrollToItem(id: Long, adapter: AlbumDetailAdapter) {
|
||||
// Calculate where the item for the currently played song is
|
||||
val pos = adapter.currentList.indexOfFirst { it.id == id && it is Song }
|
||||
val pos = adapter.data.currentList.indexOfFirst { it.id == id && it is Song }
|
||||
|
||||
if (pos != -1) {
|
||||
val binding = requireBinding()
|
||||
|
|
|
@ -55,7 +55,7 @@ class ArtistDetailFragment : DetailFragment(), DetailItemListener {
|
|||
adapter = detailAdapter
|
||||
applySpans { pos ->
|
||||
// If the item is an ActionHeader we need to also make the item full-width
|
||||
val item = detailAdapter.currentList[pos]
|
||||
val item = detailAdapter.data.currentList[pos]
|
||||
item is Header || item is SortHeader || item is Artist
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ class ArtistDetailFragment : DetailFragment(), DetailItemListener {
|
|||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
detailModel.artistData.observe(viewLifecycleOwner) { list ->
|
||||
detailAdapter.submitList(list)
|
||||
detailAdapter.data.submitList(list)
|
||||
}
|
||||
|
||||
detailModel.navToItem.observe(viewLifecycleOwner, ::handleNavigation)
|
||||
|
|
|
@ -53,14 +53,16 @@ class GenreDetailFragment : DetailFragment(), DetailItemListener {
|
|||
binding.detailRecycler.apply {
|
||||
adapter = detailAdapter
|
||||
applySpans { pos ->
|
||||
val item = detailAdapter.currentList[pos]
|
||||
val item = detailAdapter.data.currentList[pos]
|
||||
item is Header || item is SortHeader || item is Genre
|
||||
}
|
||||
}
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
detailModel.genreData.observe(viewLifecycleOwner) { list -> detailAdapter.submitList(list) }
|
||||
detailModel.genreData.observe(viewLifecycleOwner) { list ->
|
||||
detailAdapter.data.submitList(list)
|
||||
}
|
||||
|
||||
detailModel.navToItem.observe(viewLifecycleOwner, ::handleNavigation)
|
||||
|
||||
|
|
|
@ -30,8 +30,8 @@ import org.oxycblt.auxio.music.Song
|
|||
import org.oxycblt.auxio.music.toDuration
|
||||
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.ItemDiffCallback
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.SimpleItemCallback
|
||||
import org.oxycblt.auxio.util.getPluralSafe
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
import org.oxycblt.auxio.util.textSafe
|
||||
|
@ -98,7 +98,7 @@ class AlbumDetailAdapter(listener: AlbumDetailItemListener) :
|
|||
|
||||
companion object {
|
||||
private val DIFFER =
|
||||
object : ItemDiffCallback<Item>() {
|
||||
object : SimpleItemCallback<Item>() {
|
||||
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
return when {
|
||||
oldItem is Album && newItem is Album ->
|
||||
|
@ -154,7 +154,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
|
|||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Album>() {
|
||||
object : SimpleItemCallback<Album>() {
|
||||
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
|
||||
oldItem.resolvedName == newItem.resolvedName &&
|
||||
oldItem.resolvedArtistName == newItem.resolvedArtistName &&
|
||||
|
@ -214,7 +214,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
|
|||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Song>() {
|
||||
object : SimpleItemCallback<Song>() {
|
||||
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
|
||||
oldItem.resolvedName == newItem.resolvedName &&
|
||||
oldItem.duration == newItem.duration
|
||||
|
|
|
@ -32,8 +32,8 @@ import org.oxycblt.auxio.music.Song
|
|||
import org.oxycblt.auxio.ui.ArtistViewHolder
|
||||
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.ItemDiffCallback
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.SimpleItemCallback
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getPluralSafe
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
@ -123,7 +123,7 @@ class ArtistDetailAdapter(listener: DetailItemListener) :
|
|||
|
||||
companion object {
|
||||
private val DIFFER =
|
||||
object : ItemDiffCallback<Item>() {
|
||||
object : SimpleItemCallback<Item>() {
|
||||
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
return when {
|
||||
oldItem is Artist && newItem is Artist ->
|
||||
|
@ -209,7 +209,7 @@ private constructor(
|
|||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Album>() {
|
||||
object : SimpleItemCallback<Album>() {
|
||||
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
|
||||
oldItem.resolvedName == newItem.resolvedName && oldItem.year == newItem.year
|
||||
}
|
||||
|
@ -249,7 +249,7 @@ private constructor(
|
|||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Song>() {
|
||||
object : SimpleItemCallback<Song>() {
|
||||
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
|
||||
oldItem.resolvedName == newItem.resolvedName &&
|
||||
oldItem.resolvedAlbumName == newItem.resolvedAlbumName
|
||||
|
|
|
@ -25,13 +25,14 @@ import androidx.recyclerview.widget.DiffUtil
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.databinding.ItemSortHeaderBinding
|
||||
import org.oxycblt.auxio.ui.AsyncBackingData
|
||||
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||
import org.oxycblt.auxio.ui.Header
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.ItemDiffCallback
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.MultiAdapter
|
||||
import org.oxycblt.auxio.ui.NewHeaderViewHolder
|
||||
import org.oxycblt.auxio.ui.SimpleItemCallback
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getViewHolderAt
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
@ -41,7 +42,7 @@ import org.oxycblt.auxio.util.textSafe
|
|||
abstract class DetailAdapter<L : DetailItemListener>(
|
||||
listener: L,
|
||||
diffCallback: DiffUtil.ItemCallback<Item>
|
||||
) : MultiAdapter<L>(listener, diffCallback) {
|
||||
) : MultiAdapter<L>(listener) {
|
||||
abstract fun onHighlightViewHolder(viewHolder: Highlightable, item: Item)
|
||||
|
||||
protected inline fun <reified T : Item> highlightItem(
|
||||
|
@ -54,7 +55,7 @@ abstract class DetailAdapter<L : DetailItemListener>(
|
|||
|
||||
// Use existing data instead of having to re-sort it.
|
||||
// We also have to account for the album count when searching for the ViewHolder.
|
||||
val pos = mCurrentList.indexOfFirst { item -> item.id == newItem.id && item is T }
|
||||
val pos = data.currentList.indexOfFirst { item -> item.id == newItem.id && item is T }
|
||||
|
||||
// Check if the ViewHolder for this song is visible, if it is then highlight it.
|
||||
// If the ViewHolder is not visible, then the adapter should take care of it if
|
||||
|
@ -70,6 +71,8 @@ abstract class DetailAdapter<L : DetailItemListener>(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LeakingThis") override val data = AsyncBackingData(this, diffCallback)
|
||||
|
||||
override fun getCreatorFromItem(item: Item) =
|
||||
when (item) {
|
||||
is Header -> NewHeaderViewHolder.CREATOR
|
||||
|
@ -97,7 +100,7 @@ abstract class DetailAdapter<L : DetailItemListener>(
|
|||
|
||||
companion object {
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Item>() {
|
||||
object : SimpleItemCallback<Item>() {
|
||||
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
return when {
|
||||
oldItem is Header && newItem is Header ->
|
||||
|
@ -134,7 +137,7 @@ class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
|
|||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<SortHeader>() {
|
||||
object : SimpleItemCallback<SortHeader>() {
|
||||
override fun areItemsTheSame(oldItem: SortHeader, newItem: SortHeader) =
|
||||
oldItem.string == newItem.string
|
||||
}
|
||||
|
|
|
@ -29,8 +29,8 @@ import org.oxycblt.auxio.music.Genre
|
|||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.ItemDiffCallback
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.SimpleItemCallback
|
||||
import org.oxycblt.auxio.ui.SongViewHolder
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getPluralSafe
|
||||
|
@ -100,7 +100,7 @@ class GenreDetailAdapter(listener: DetailItemListener) :
|
|||
|
||||
companion object {
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Item>() {
|
||||
object : SimpleItemCallback<Item>() {
|
||||
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||
return when {
|
||||
oldItem is Genre && newItem is Genre ->
|
||||
|
@ -137,7 +137,7 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
|
|||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Genre>() {
|
||||
object : SimpleItemCallback<Genre>() {
|
||||
override fun areItemsTheSame(oldItem: Genre, newItem: Genre) =
|
||||
oldItem.resolvedName == newItem.resolvedName &&
|
||||
oldItem.songs.size == newItem.songs.size &&
|
||||
|
|
|
@ -18,17 +18,17 @@
|
|||
package org.oxycblt.auxio.home.list
|
||||
|
||||
import android.view.View
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.home.HomeFragmentDirections
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.ui.AlbumViewHolder
|
||||
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.MonoAdapter
|
||||
import org.oxycblt.auxio.ui.PrimitiveBackingData
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.ui.newMenu
|
||||
import org.oxycblt.auxio.ui.sliceArticle
|
||||
|
@ -38,10 +38,16 @@ import org.oxycblt.auxio.ui.sliceArticle
|
|||
* @author
|
||||
*/
|
||||
class AlbumListFragment : HomeListFragment<Album>() {
|
||||
override val recyclerId: Int = R.id.home_album_list
|
||||
override val homeAdapter = AlbumAdapter(this)
|
||||
override val homeData: LiveData<List<Album>>
|
||||
get() = homeModel.albums
|
||||
private val homeAdapter = AlbumAdapter(this)
|
||||
|
||||
override fun setupRecycler(recycler: RecyclerView) {
|
||||
recycler.apply {
|
||||
id = R.id.home_album_list
|
||||
adapter = homeAdapter
|
||||
}
|
||||
|
||||
homeModel.albums.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) }
|
||||
}
|
||||
|
||||
override fun getPopup(pos: Int): String? {
|
||||
val album = homeModel.albums.value!![pos]
|
||||
|
@ -72,8 +78,8 @@ class AlbumListFragment : HomeListFragment<Album>() {
|
|||
}
|
||||
|
||||
class AlbumAdapter(listener: MenuItemListener) :
|
||||
MonoAdapter<Album, MenuItemListener, AlbumViewHolder>(listener, AlbumViewHolder.DIFFER) {
|
||||
override val creator: BindingViewHolder.Creator<AlbumViewHolder>
|
||||
get() = AlbumViewHolder.CREATOR
|
||||
MonoAdapter<Album, MenuItemListener, AlbumViewHolder>(listener) {
|
||||
override val data = PrimitiveBackingData<Album>(this)
|
||||
override val creator = AlbumViewHolder.CREATOR
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
package org.oxycblt.auxio.home.list
|
||||
|
||||
import android.view.View
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.home.HomeFragmentDirections
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
|
@ -27,6 +27,7 @@ import org.oxycblt.auxio.ui.ArtistViewHolder
|
|||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.MonoAdapter
|
||||
import org.oxycblt.auxio.ui.PrimitiveBackingData
|
||||
import org.oxycblt.auxio.ui.newMenu
|
||||
import org.oxycblt.auxio.ui.sliceArticle
|
||||
|
||||
|
@ -35,10 +36,16 @@ import org.oxycblt.auxio.ui.sliceArticle
|
|||
* @author
|
||||
*/
|
||||
class ArtistListFragment : HomeListFragment<Artist>() {
|
||||
override val recyclerId: Int = R.id.home_artist_list
|
||||
override val homeAdapter = ArtistAdapter(this)
|
||||
override val homeData: LiveData<List<Artist>>
|
||||
get() = homeModel.artists
|
||||
private val homeAdapter = ArtistAdapter(this)
|
||||
|
||||
override fun setupRecycler(recycler: RecyclerView) {
|
||||
recycler.apply {
|
||||
id = R.id.home_artist_list
|
||||
adapter = homeAdapter
|
||||
}
|
||||
|
||||
homeModel.artists.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) }
|
||||
}
|
||||
|
||||
override fun getPopup(pos: Int) =
|
||||
homeModel.artists.value!![pos].resolvedName.sliceArticle().first().uppercase()
|
||||
|
@ -53,7 +60,8 @@ class ArtistListFragment : HomeListFragment<Artist>() {
|
|||
}
|
||||
|
||||
class ArtistAdapter(listener: MenuItemListener) :
|
||||
MonoAdapter<Artist, MenuItemListener, ArtistViewHolder>(listener, ArtistViewHolder.DIFFER) {
|
||||
MonoAdapter<Artist, MenuItemListener, ArtistViewHolder>(listener) {
|
||||
override val data = PrimitiveBackingData<Artist>(this)
|
||||
override val creator = ArtistViewHolder.CREATOR
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
package org.oxycblt.auxio.home.list
|
||||
|
||||
import android.view.View
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.home.HomeFragmentDirections
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
|
@ -27,6 +27,7 @@ import org.oxycblt.auxio.ui.GenreViewHolder
|
|||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.MonoAdapter
|
||||
import org.oxycblt.auxio.ui.PrimitiveBackingData
|
||||
import org.oxycblt.auxio.ui.newMenu
|
||||
import org.oxycblt.auxio.ui.sliceArticle
|
||||
|
||||
|
@ -35,10 +36,16 @@ import org.oxycblt.auxio.ui.sliceArticle
|
|||
* @author
|
||||
*/
|
||||
class GenreListFragment : HomeListFragment<Genre>() {
|
||||
override val recyclerId = R.id.home_genre_list
|
||||
override val homeAdapter = GenreAdapter(this)
|
||||
override val homeData: LiveData<List<Genre>>
|
||||
get() = homeModel.genres
|
||||
private val homeAdapter = GenreAdapter(this)
|
||||
|
||||
override fun setupRecycler(recycler: RecyclerView) {
|
||||
recycler.apply {
|
||||
id = R.id.home_genre_list
|
||||
adapter = homeAdapter
|
||||
}
|
||||
|
||||
homeModel.genres.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) }
|
||||
}
|
||||
|
||||
override fun getPopup(pos: Int) =
|
||||
homeModel.genres.value!![pos].resolvedName.sliceArticle().first().uppercase()
|
||||
|
@ -53,7 +60,8 @@ class GenreListFragment : HomeListFragment<Genre>() {
|
|||
}
|
||||
|
||||
class GenreAdapter(listener: MenuItemListener) :
|
||||
MonoAdapter<Genre, MenuItemListener, GenreViewHolder>(listener, GenreViewHolder.DIFFER) {
|
||||
MonoAdapter<Genre, MenuItemListener, GenreViewHolder>(listener) {
|
||||
override val data = PrimitiveBackingData<Genre>(this)
|
||||
override val creator = GenreViewHolder.CREATOR
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,17 +21,14 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.home.HomeViewModel
|
||||
import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.MonoAdapter
|
||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.applySpans
|
||||
|
||||
/**
|
||||
* A Base [Fragment] implementing the base features shared across all list fragments in the home UI.
|
||||
|
@ -42,11 +39,7 @@ abstract class HomeListFragment<T : Item> :
|
|||
MenuItemListener,
|
||||
FastScrollRecyclerView.PopupProvider,
|
||||
FastScrollRecyclerView.OnFastScrollListener {
|
||||
/** The popup provider to use for the fast scroller view. */
|
||||
abstract val recyclerId: Int
|
||||
abstract val homeAdapter:
|
||||
MonoAdapter<T, MenuItemListener, out BindingViewHolder<T, MenuItemListener>>
|
||||
abstract val homeData: LiveData<List<T>>
|
||||
abstract fun setupRecycler(recycler: RecyclerView)
|
||||
|
||||
protected val homeModel: HomeViewModel by activityViewModels()
|
||||
protected val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
|
@ -55,18 +48,9 @@ abstract class HomeListFragment<T : Item> :
|
|||
FragmentHomeListBinding.inflate(inflater)
|
||||
|
||||
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||
binding.homeRecycler.apply {
|
||||
id = recyclerId
|
||||
adapter = homeAdapter
|
||||
applySpans()
|
||||
}
|
||||
|
||||
setupRecycler(binding.homeRecycler)
|
||||
binding.homeRecycler.popupProvider = this
|
||||
binding.homeRecycler.onDragListener = this
|
||||
|
||||
homeData.observe(viewLifecycleOwner) { list ->
|
||||
homeAdapter.submitListHard(list.toMutableList())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyBinding(binding: FragmentHomeListBinding) {
|
||||
|
|
|
@ -18,13 +18,14 @@
|
|||
package org.oxycblt.auxio.home.list
|
||||
|
||||
import android.view.View
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.MonoAdapter
|
||||
import org.oxycblt.auxio.ui.PrimitiveBackingData
|
||||
import org.oxycblt.auxio.ui.SongViewHolder
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.ui.newMenu
|
||||
|
@ -35,10 +36,16 @@ import org.oxycblt.auxio.ui.sliceArticle
|
|||
* @author
|
||||
*/
|
||||
class SongListFragment : HomeListFragment<Song>() {
|
||||
override val recyclerId = R.id.home_song_list
|
||||
override val homeAdapter = SongsAdapter(this)
|
||||
override val homeData: LiveData<List<Song>>
|
||||
get() = homeModel.songs
|
||||
private val homeAdapter = SongsAdapter(this)
|
||||
|
||||
override fun setupRecycler(recycler: RecyclerView) {
|
||||
recycler.apply {
|
||||
id = R.id.home_song_list
|
||||
adapter = homeAdapter
|
||||
}
|
||||
|
||||
homeModel.songs.observe(viewLifecycleOwner) { list -> homeAdapter.data.submitList(list) }
|
||||
}
|
||||
|
||||
override fun getPopup(pos: Int): String {
|
||||
val song = homeModel.songs.value!![pos]
|
||||
|
@ -71,7 +78,8 @@ class SongListFragment : HomeListFragment<Song>() {
|
|||
}
|
||||
|
||||
inner class SongsAdapter(listener: MenuItemListener) :
|
||||
MonoAdapter<Song, MenuItemListener, SongViewHolder>(listener, SongViewHolder.DIFFER) {
|
||||
MonoAdapter<Song, MenuItemListener, SongViewHolder>(listener) {
|
||||
override val data = PrimitiveBackingData<Song>(this)
|
||||
override val creator = SongViewHolder.CREATOR
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.oxycblt.auxio.coil.bindAlbumCover
|
|||
import org.oxycblt.auxio.databinding.ItemQueueSongBinding
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.ui.BindingViewHolder
|
||||
import org.oxycblt.auxio.ui.HybridBackingData
|
||||
import org.oxycblt.auxio.ui.MonoAdapter
|
||||
import org.oxycblt.auxio.ui.SongViewHolder
|
||||
import org.oxycblt.auxio.util.disableDropShadowCompat
|
||||
|
@ -37,9 +38,9 @@ import org.oxycblt.auxio.util.inflater
|
|||
import org.oxycblt.auxio.util.stateList
|
||||
import org.oxycblt.auxio.util.textSafe
|
||||
|
||||
class NewQueueAdapter(listener: QueueItemListener) :
|
||||
MonoAdapter<Song, QueueItemListener, QueueSongViewHolder>(
|
||||
listener, QueueSongViewHolder.DIFFER) {
|
||||
class QueueAdapter(listener: QueueItemListener) :
|
||||
MonoAdapter<Song, QueueItemListener, QueueSongViewHolder>(listener) {
|
||||
override val data = HybridBackingData(this, QueueSongViewHolder.DIFFER)
|
||||
override val creator = QueueSongViewHolder.CREATOR
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ import org.oxycblt.auxio.util.logD
|
|||
*/
|
||||
class QueueDragCallback(
|
||||
private val playbackModel: PlaybackViewModel,
|
||||
private val queueAdapter: NewQueueAdapter
|
||||
private val queueAdapter: QueueAdapter
|
||||
) : ItemTouchHelper.Callback() {
|
||||
private var shouldLift = true
|
||||
|
||||
|
@ -154,12 +154,12 @@ class QueueDragCallback(
|
|||
val from = viewHolder.bindingAdapterPosition
|
||||
val to = target.bindingAdapterPosition
|
||||
|
||||
return playbackModel.moveQueueDataItems(from, to) { queueAdapter.moveItems(from, to) }
|
||||
return playbackModel.moveQueueDataItems(from, to) { queueAdapter.data.moveItems(from, to) }
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
playbackModel.removeQueueDataItem(viewHolder.bindingAdapterPosition) {
|
||||
queueAdapter.removeItem(viewHolder.bindingAdapterPosition)
|
||||
queueAdapter.data.removeItem(viewHolder.bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ import org.oxycblt.auxio.util.requireAttached
|
|||
*/
|
||||
class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemListener {
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
private var queueAdapter = NewQueueAdapter(this)
|
||||
private var queueAdapter = QueueAdapter(this)
|
||||
private var touchHelper: ItemTouchHelper? = null
|
||||
private var callback: QueueDragCallback? = null
|
||||
|
||||
|
@ -72,7 +72,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
|
|||
return
|
||||
}
|
||||
|
||||
queueAdapter.submitList(queue.toMutableList())
|
||||
queueAdapter.data.submitList(queue.toMutableList())
|
||||
}
|
||||
|
||||
private fun requireTouchHelper(): ItemTouchHelper {
|
||||
|
|
|
@ -24,17 +24,19 @@ import org.oxycblt.auxio.music.Genre
|
|||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.ui.AlbumViewHolder
|
||||
import org.oxycblt.auxio.ui.ArtistViewHolder
|
||||
import org.oxycblt.auxio.ui.AsyncBackingData
|
||||
import org.oxycblt.auxio.ui.GenreViewHolder
|
||||
import org.oxycblt.auxio.ui.Header
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.ItemDiffCallback
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.MultiAdapter
|
||||
import org.oxycblt.auxio.ui.NewHeaderViewHolder
|
||||
import org.oxycblt.auxio.ui.SimpleItemCallback
|
||||
import org.oxycblt.auxio.ui.SongViewHolder
|
||||
|
||||
class NeoSearchAdapter(listener: MenuItemListener) :
|
||||
MultiAdapter<MenuItemListener>(listener, DIFFER) {
|
||||
class SearchAdapter(listener: MenuItemListener) : MultiAdapter<MenuItemListener>(listener) {
|
||||
override val data = AsyncBackingData(this, DIFFER)
|
||||
|
||||
override fun getCreatorFromItem(item: Item) =
|
||||
when (item) {
|
||||
is Song -> SongViewHolder.CREATOR
|
||||
|
@ -72,7 +74,7 @@ class NeoSearchAdapter(listener: MenuItemListener) :
|
|||
|
||||
companion object {
|
||||
private val DIFFER =
|
||||
object : ItemDiffCallback<Item>() {
|
||||
object : SimpleItemCallback<Item>() {
|
||||
override fun areItemsTheSame(oldItem: Item, newItem: Item) =
|
||||
when {
|
||||
oldItem is Song && newItem is Song ->
|
||||
|
|
|
@ -58,7 +58,7 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
|
|||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
|
||||
private val searchAdapter = NeoSearchAdapter(this)
|
||||
private val searchAdapter = SearchAdapter(this)
|
||||
private var imm: InputMethodManager? = null
|
||||
private var launchedKeyboard = false
|
||||
|
||||
|
@ -103,7 +103,7 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
|
|||
|
||||
binding.searchRecycler.apply {
|
||||
adapter = searchAdapter
|
||||
applySpans { pos -> searchAdapter.currentList[pos] is Header }
|
||||
applySpans { pos -> searchAdapter.data.currentList[pos] is Header }
|
||||
}
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
|
@ -161,7 +161,7 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
|
|||
|
||||
val binding = requireBinding()
|
||||
|
||||
searchAdapter.submitList(results.toMutableList()) {
|
||||
searchAdapter.data.submitList(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.
|
||||
|
|
|
@ -26,31 +26,212 @@ import androidx.recyclerview.widget.DiffUtil
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
/**
|
||||
* An adapter enabling both asynchronous list updates and synchronous list updates.
|
||||
*
|
||||
* DiffUtil is a joke. The animations are chaotic and gaudy, it does not preserve the scroll
|
||||
* position of the RecyclerView, it refuses to play along with item movements, and the speed gains
|
||||
* are minimal. We would rather want to use the slower yet more reliable notifyX in nearly all
|
||||
* cases, however DiffUtil does have some use in places such as search, so we still want the ability
|
||||
* to use a differ while also having access to the basic adapter primitives as well. This class
|
||||
* achieves it through some terrible reflection magic, and is more or less the base for all adapters
|
||||
* in the app.
|
||||
*
|
||||
* TODO: Delegate data management to the internal adapters so that we can isolate the horrible hacks
|
||||
* to the specific adapters that use need them.
|
||||
* An adapter for one viewholder tied to one type of data. All functionality is derived from the
|
||||
* overridden values.
|
||||
*/
|
||||
abstract class HybridAdapter<T, VH : RecyclerView.ViewHolder>(
|
||||
diffCallback: DiffUtil.ItemCallback<T>
|
||||
) : RecyclerView.Adapter<VH>() {
|
||||
protected var mCurrentList = mutableListOf<T>()
|
||||
abstract class MonoAdapter<T, L, VH : BindingViewHolder<T, L>>(private val listener: L) :
|
||||
RecyclerView.Adapter<VH>() {
|
||||
/** The data that the adapter will source to bind viewholders. */
|
||||
abstract val data: BackingData<T>
|
||||
/** The creator instance that all viewholders will be derived from. */
|
||||
protected abstract val creator: BindingViewHolder.Creator<VH>
|
||||
|
||||
override fun getItemCount(): Int = data.getItemCount()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
creator.create(parent.context)
|
||||
|
||||
override fun onBindViewHolder(viewHolder: VH, position: Int) {
|
||||
viewHolder.bind(data.getItem(position), listener)
|
||||
}
|
||||
}
|
||||
|
||||
private typealias AnyCreator = BindingViewHolder.Creator<out RecyclerView.ViewHolder>
|
||||
|
||||
/**
|
||||
* An adapter for many viewholders tied to many types of data. Deriving this is more complicated
|
||||
* than [MonoAdapter], as less overrides can be provided "for free".
|
||||
*/
|
||||
abstract class MultiAdapter<L>(private val listener: L) :
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
/** The data that the adapter will source to bind viewholders. */
|
||||
abstract val data: BackingData<Item>
|
||||
|
||||
/**
|
||||
* Get any creator from the given item. This is used to derive the view type. If there is no
|
||||
* creator for the given item, return null.
|
||||
*/
|
||||
protected abstract fun getCreatorFromItem(item: Item): AnyCreator?
|
||||
/**
|
||||
* Get any creator from the given view type. This is used to create the viewholder itself.
|
||||
* Ideally, one should compare the viewType to every creator's view type and return the one that
|
||||
* matches. In cases where the view type is unexpected, return null.
|
||||
*/
|
||||
protected abstract fun getCreatorFromViewType(viewType: Int): AnyCreator?
|
||||
|
||||
/**
|
||||
* Bind the given viewholder to an item. Casting must be done on the consumer's end due to
|
||||
* bounds on [BindingViewHolder].
|
||||
*/
|
||||
protected abstract fun onBind(viewHolder: RecyclerView.ViewHolder, item: Item, listener: L)
|
||||
|
||||
override fun getItemCount(): Int = data.getItemCount()
|
||||
|
||||
override fun getItemViewType(position: Int) =
|
||||
requireNotNull(getCreatorFromItem(data.getItem(position))) {
|
||||
"Unable to get view type for item ${data.getItem(position)}"
|
||||
}
|
||||
.viewType
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
requireNotNull(getCreatorFromViewType(viewType)) {
|
||||
"Unable to create viewholder for view type $viewType"
|
||||
}
|
||||
.create(parent.context)
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
onBind(holder, data.getItem(position), listener)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A variation of [RecyclerView.ViewHolder] that enables ViewBinding. This is be used to provide a
|
||||
* universal surface for binding data to a ViewHolder, and can be used with [MonoAdapter] to get an
|
||||
* entire adapter implementation for free.
|
||||
*/
|
||||
abstract class BindingViewHolder<T, L>(root: View) : RecyclerView.ViewHolder(root) {
|
||||
abstract fun bind(item: T, listener: L)
|
||||
|
||||
init {
|
||||
// Force the layout to *actually* be the screen width
|
||||
root.layoutParams =
|
||||
RecyclerView.LayoutParams(
|
||||
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
interface Creator<VH : RecyclerView.ViewHolder> {
|
||||
val viewType: Int
|
||||
fun create(context: Context): VH
|
||||
}
|
||||
}
|
||||
|
||||
/** An interface for detecting if an item has been clicked once. */
|
||||
interface ItemClickListener {
|
||||
/** Called when an item is clicked once. */
|
||||
fun onItemClick(item: Item)
|
||||
}
|
||||
|
||||
/** An interface for detecting if an item has had it's menu opened. */
|
||||
interface MenuItemListener : ItemClickListener {
|
||||
/** Called when an item desires to open a menu relating to it. */
|
||||
fun onOpenMenu(item: Item, anchor: View)
|
||||
}
|
||||
|
||||
/**
|
||||
* The base for all items in Auxio. Any datatype can derive this type and gain some behavior not
|
||||
* provided for free by the normal adapter implementations, such as certain types of diffing.
|
||||
*/
|
||||
abstract class Item {
|
||||
/** A unique ID for this item. ***THIS IS NOT A MEDIASTORE ID!** */
|
||||
abstract val id: Long
|
||||
}
|
||||
|
||||
/** A data object used solely for the "Header" UI element. */
|
||||
data class Header(
|
||||
override val id: Long,
|
||||
/** The string resource used for the header. */
|
||||
@StringRes val string: Int
|
||||
) : Item()
|
||||
|
||||
/**
|
||||
* Represents data that backs a [MonoAdapter] or [MultiAdapter]. This can be implemented by any
|
||||
* datatype to customize the organization or editing of data in a way that works best for the
|
||||
* specific adapter.
|
||||
*/
|
||||
abstract class BackingData<T> {
|
||||
/** Get an item at [position]. */
|
||||
abstract fun getItem(position: Int): T
|
||||
/** Get the total length of the backing data. */
|
||||
abstract fun getItemCount(): Int
|
||||
}
|
||||
|
||||
/**
|
||||
* A list-backed [BackingData] that is modified using adapter primitives. Useful in cases where
|
||||
* [AsyncBackingData] is not preferable due to bugs involving diffing.
|
||||
*/
|
||||
class PrimitiveBackingData<T>(private val adapter: RecyclerView.Adapter<*>) : BackingData<T>() {
|
||||
private var mCurrentList = mutableListOf<T>()
|
||||
/** The current list backing this adapter. */
|
||||
val currentList: List<T>
|
||||
get() = mCurrentList
|
||||
|
||||
// Probably okay to leak this here since it's just a callback.
|
||||
@Suppress("LeakingThis") private val differ = AsyncListDiffer(this, diffCallback)
|
||||
override fun getItem(position: Int): T = mCurrentList[position]
|
||||
override fun getItemCount(): Int = mCurrentList.size
|
||||
|
||||
protected fun getItem(position: Int): T = mCurrentList[position]
|
||||
/**
|
||||
* Update the list with a [newList]. This calls [RecyclerView.Adapter.notifyDataSetChanged]
|
||||
* internally, which is inefficient but also the most reliable update callback.
|
||||
*/
|
||||
@Suppress("NotifyDatasetChanged")
|
||||
fun submitList(newList: List<T>) {
|
||||
mCurrentList = newList.toMutableList()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an item from [from] to [to]. This calls [RecyclerView.Adapter.notifyItemMoved]
|
||||
* internally.
|
||||
*/
|
||||
fun moveItems(from: Int, to: Int) {
|
||||
mCurrentList.add(to, mCurrentList.removeAt(from))
|
||||
adapter.notifyItemMoved(from, to)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A list-backed [BackingData] that is modified with [AsyncListDiffer]. This is useful in cases
|
||||
* where data updates are rapid-fire and unpredictable, and where the benefits of asynchronously
|
||||
* diffing the adapter outweigh the shortcomings.
|
||||
*/
|
||||
class AsyncBackingData<T>(
|
||||
adapter: RecyclerView.Adapter<*>,
|
||||
diffCallback: DiffUtil.ItemCallback<T>
|
||||
) : BackingData<T>() {
|
||||
private var differ = AsyncListDiffer(adapter, diffCallback)
|
||||
/** The current list backing this adapter. */
|
||||
val currentList: List<T>
|
||||
get() = differ.currentList
|
||||
|
||||
override fun getItem(position: Int): T = differ.currentList[position]
|
||||
override fun getItemCount(): Int = differ.currentList.size
|
||||
|
||||
/**
|
||||
* Submit a list for [AsyncListDiffer] to calculate. Any previous calls of [submitList] will be
|
||||
* dropped.
|
||||
*/
|
||||
fun submitList(newList: List<T>, onDone: () -> Unit = {}) {
|
||||
differ.submitList(newList, onDone)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A list-backed [BackingData] that can be modified with both adapter primitives and
|
||||
* [AsyncListDiffer]. Never use this class unless absolutely necessary, such as when dealing with
|
||||
* item dragging. This is mostly because the class is a terrible hacky mess that could easily crash
|
||||
* the app if you are not careful with it. You have been warned.
|
||||
*/
|
||||
class HybridBackingData<T>(
|
||||
private val adapter: RecyclerView.Adapter<*>,
|
||||
diffCallback: DiffUtil.ItemCallback<T>
|
||||
) : BackingData<T>() {
|
||||
private var mCurrentList = mutableListOf<T>()
|
||||
val currentList: List<T>
|
||||
get() = mCurrentList
|
||||
|
||||
private val differ = AsyncListDiffer(adapter, diffCallback)
|
||||
|
||||
override fun getItem(position: Int): T = mCurrentList[position]
|
||||
override fun getItemCount(): Int = mCurrentList.size
|
||||
|
||||
fun submitList(newData: List<T>, onDone: () -> Unit = {}) {
|
||||
|
@ -60,25 +241,25 @@ abstract class HybridAdapter<T, VH : RecyclerView.ViewHolder>(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("NotifyDatasetChanged")
|
||||
fun submitListHard(newList: List<T>) {
|
||||
if (newList != mCurrentList) {
|
||||
mCurrentList = newList.toMutableList()
|
||||
differ.rewriteListUnsafe(mCurrentList)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
// @Suppress("NotifyDatasetChanged")
|
||||
// fun submitListHard(newList: List<T>) {
|
||||
// if (newList != mCurrentList) {
|
||||
// mCurrentList = newList.toMutableList()
|
||||
// differ.rewriteListUnsafe(mCurrentList)
|
||||
// adapter.notifyDataSetChanged()
|
||||
// }
|
||||
// }
|
||||
|
||||
fun moveItems(from: Int, to: Int) {
|
||||
mCurrentList.add(to, mCurrentList.removeAt(from))
|
||||
differ.rewriteListUnsafe(mCurrentList)
|
||||
notifyItemMoved(from, to)
|
||||
adapter.notifyItemMoved(from, to)
|
||||
}
|
||||
|
||||
fun removeItem(at: Int) {
|
||||
mCurrentList.removeAt(at)
|
||||
differ.rewriteListUnsafe(mCurrentList)
|
||||
notifyItemRemoved(at)
|
||||
adapter.notifyItemRemoved(at)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -108,87 +289,13 @@ abstract class HybridAdapter<T, VH : RecyclerView.ViewHolder>(
|
|||
}
|
||||
}
|
||||
|
||||
abstract class MonoAdapter<T, L, VH : BindingViewHolder<T, L>>(
|
||||
private val listener: L,
|
||||
diffCallback: DiffUtil.ItemCallback<T>
|
||||
) : HybridAdapter<T, VH>(diffCallback) {
|
||||
protected abstract val creator: BindingViewHolder.Creator<VH>
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
creator.create(parent.context)
|
||||
|
||||
override fun onBindViewHolder(viewHolder: VH, position: Int) {
|
||||
viewHolder.bind(getItem(position), listener)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class MultiAdapter<L>(private val listener: L, diffCallback: DiffUtil.ItemCallback<Item>) :
|
||||
HybridAdapter<Item, RecyclerView.ViewHolder>(diffCallback) {
|
||||
abstract fun getCreatorFromItem(
|
||||
item: Item
|
||||
): BindingViewHolder.Creator<out RecyclerView.ViewHolder>?
|
||||
abstract fun getCreatorFromViewType(
|
||||
viewType: Int
|
||||
): BindingViewHolder.Creator<out RecyclerView.ViewHolder>?
|
||||
abstract fun onBind(viewHolder: RecyclerView.ViewHolder, item: Item, listener: L)
|
||||
|
||||
override fun getItemViewType(position: Int) =
|
||||
requireNotNull(getCreatorFromItem(getItem(position))) {
|
||||
"Unable to get view type for item ${getItem(position)}"
|
||||
}
|
||||
.viewType
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
requireNotNull(getCreatorFromViewType(viewType)) {
|
||||
"Unable to create viewholder for view type $viewType"
|
||||
}
|
||||
.create(parent.context)
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
onBind(holder, getItem(position), listener)
|
||||
}
|
||||
}
|
||||
|
||||
/** The base for all items in Auxio. */
|
||||
abstract class Item {
|
||||
/** A unique ID for this item. ***THIS IS NOT A MEDIASTORE ID!** */
|
||||
abstract val id: Long
|
||||
}
|
||||
|
||||
/** A data object used solely for the "Header" UI element. */
|
||||
data class Header(
|
||||
override val id: Long,
|
||||
/** The string resource used for the header. */
|
||||
@StringRes val string: Int
|
||||
) : Item()
|
||||
|
||||
abstract class ItemDiffCallback<T : Item> : DiffUtil.ItemCallback<T>() {
|
||||
/**
|
||||
* A base [DiffUtil.ItemCallback] that automatically provides an implementation of
|
||||
* [areContentsTheSame] any object that is derived from [Item].
|
||||
*/
|
||||
abstract class SimpleItemCallback<T : Item> : DiffUtil.ItemCallback<T>() {
|
||||
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
|
||||
if (oldItem.javaClass != newItem.javaClass) return false
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
}
|
||||
|
||||
interface ItemClickListener {
|
||||
fun onItemClick(item: Item)
|
||||
}
|
||||
|
||||
interface MenuItemListener : ItemClickListener {
|
||||
fun onOpenMenu(item: Item, anchor: View)
|
||||
}
|
||||
|
||||
abstract class BindingViewHolder<T, L>(root: View) : RecyclerView.ViewHolder(root) {
|
||||
abstract fun bind(item: T, listener: L)
|
||||
|
||||
init {
|
||||
// Force the layout to *actually* be the screen width
|
||||
root.layoutParams =
|
||||
RecyclerView.LayoutParams(
|
||||
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
interface Creator<VH : RecyclerView.ViewHolder> {
|
||||
val viewType: Int
|
||||
fun create(context: Context): VH
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.oxycblt.auxio.util.getPluralSafe
|
|||
import org.oxycblt.auxio.util.inflater
|
||||
import org.oxycblt.auxio.util.textSafe
|
||||
|
||||
/** The shared ViewHolder for a [Song]. */
|
||||
class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||
BindingViewHolder<Song, MenuItemListener>(binding.root) {
|
||||
override fun bind(item: Song, listener: MenuItemListener) {
|
||||
|
@ -61,7 +62,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
|||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Song>() {
|
||||
object : SimpleItemCallback<Song>() {
|
||||
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
|
||||
oldItem.resolvedName == newItem.resolvedName &&
|
||||
oldItem.resolvedArtistName == oldItem.resolvedArtistName
|
||||
|
@ -69,7 +70,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
|||
}
|
||||
}
|
||||
|
||||
/** The Shared ViewHolder for a [Album]. Instantiation should be done with [from]. */
|
||||
/** The Shared ViewHolder for a [Album]. */
|
||||
class AlbumViewHolder
|
||||
private constructor(
|
||||
private val binding: ItemParentBinding,
|
||||
|
@ -99,7 +100,7 @@ private constructor(
|
|||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Album>() {
|
||||
object : SimpleItemCallback<Album>() {
|
||||
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
|
||||
oldItem.resolvedName == newItem.resolvedName &&
|
||||
oldItem.resolvedArtistName == newItem.resolvedArtistName
|
||||
|
@ -139,7 +140,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
|
|||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Artist>() {
|
||||
object : SimpleItemCallback<Artist>() {
|
||||
override fun areItemsTheSame(oldItem: Artist, newItem: Artist) =
|
||||
oldItem.resolvedName == newItem.resolvedName &&
|
||||
oldItem.albums.size == newItem.albums.size &&
|
||||
|
@ -179,7 +180,7 @@ private constructor(
|
|||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Genre>() {
|
||||
object : SimpleItemCallback<Genre>() {
|
||||
override fun areItemsTheSame(oldItem: Genre, newItem: Genre): Boolean =
|
||||
oldItem.resolvedName == newItem.resolvedName &&
|
||||
oldItem.songs.size == newItem.songs.size
|
||||
|
@ -187,7 +188,7 @@ private constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/** The Shared ViewHolder for a [Header]. Instantiation should be done with [from] */
|
||||
/** The Shared ViewHolder for a [Header]. */
|
||||
class NewHeaderViewHolder private constructor(private val binding: ItemHeaderBinding) :
|
||||
BindingViewHolder<Header, Unit>(binding.root) {
|
||||
|
||||
|
@ -206,7 +207,7 @@ class NewHeaderViewHolder private constructor(private val binding: ItemHeaderBin
|
|||
}
|
||||
|
||||
val DIFFER =
|
||||
object : ItemDiffCallback<Header>() {
|
||||
object : SimpleItemCallback<Header>() {
|
||||
override fun areItemsTheSame(oldItem: Header, newItem: Header): Boolean =
|
||||
oldItem.string == newItem.string
|
||||
}
|
||||
|
|
|
@ -66,6 +66,7 @@ class WidgetProvider : AppWidgetProvider() {
|
|||
}
|
||||
|
||||
loadWidgetBitmap(context, song) { bitmap ->
|
||||
logD(bitmap == null)
|
||||
val state =
|
||||
WidgetState(
|
||||
song,
|
||||
|
|
|
@ -100,9 +100,11 @@ Attempting to use it as a `MediaStore` ID will result in errors.
|
|||
- Any field or method beginning with `internal` is off-limits. These fields are meant for use within `MusicLoader` and generally
|
||||
provide poor UX to the user. The only reason they are public is to make the loading process not have to rely on separate "Raw"
|
||||
objects.
|
||||
- Generally, `name` is used when saving music data to storage, while `resolvedName` is used when displaying music data to the user.
|
||||
- For `Song` instances in particular, prefer `resolvedAlbumName` and `resolvedArtistName` over `album.resolvedName` and `album.artist.resolvedName`
|
||||
- For `Album` instances in particular, prefer `resolvedArtistName` over `artist.resolvedName`
|
||||
- Generally, `rawName` is used when doing internal work, such as saving music data, while `resolvedName` is used when displaying music data to the user.
|
||||
- For `Song` instances in particular, prefer `resolvedAlbumName` and `resolvedArtistName` over `album.resolvedName` and `album.artist.resolvedName`,
|
||||
as these resolve the name in context of the song.
|
||||
- For `Album` instances in particular, prefer `resolvedArtistName` over `artist.resolvedName`, which don't actually do anything but add consistency
|
||||
to the `Song` function
|
||||
|
||||
#### Music Access
|
||||
All music on a system is asynchronously loaded into the shared object `MusicStore`. Because of this, **`MusicStore` may not be available at all times**.
|
||||
|
|
Loading…
Reference in a new issue