diff --git a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt index b354da63f..75b8a01e1 100644 --- a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt +++ b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt @@ -20,35 +20,31 @@ package org.oxycblt.auxio /** A table containing all unique integer codes that Auxio uses. */ object IntegerTable { /** SongViewHolder */ - const val ITEM_TYPE_SONG = 0xA000 + const val VIEW_TYPE_SONG = 0xA000 /** AlbumViewHolder */ - const val ITEM_TYPE_ALBUM = 0xA001 + const val VIEW_TYPE_ALBUM = 0xA001 /** ArtistViewHolder */ - const val ITEM_TYPE_ARTIST = 0xA002 + const val VIEW_TYPE_ARTIST = 0xA002 /** GenreViewHolder */ - const val ITEM_TYPE_GENRE = 0xA003 + const val VIEW_TYPE_GENRE = 0xA003 /** HeaderViewHolder */ - const val ITEM_TYPE_HEADER = 0xA004 + const val VIEW_TYPE_HEADER = 0xA004 /** SortHeaderViewHolder */ - const val ITEM_TYPE_SORT_HEADER = 0xA005 - + const val VIEW_TYPE_SORT_HEADER = 0xA005 /** AlbumDetailViewHolder */ - const val ITEM_TYPE_ALBUM_DETAIL = 0xA006 + const val VIEW_TYPE_ALBUM_DETAIL = 0xA006 /** AlbumSongViewHolder */ - const val ITEM_TYPE_ALBUM_SONG = 0xA007 + const val VIEW_TYPE_ALBUM_SONG = 0xA007 /** ArtistDetailViewHolder */ - const val ITEM_TYPE_ARTIST_DETAIL = 0xA008 + const val VIEW_TYPE_ARTIST_DETAIL = 0xA008 /** ArtistAlbumViewHolder */ - const val ITEM_TYPE_ARTIST_ALBUM = 0xA009 + const val VIEW_TYPE_ARTIST_ALBUM = 0xA009 /** ArtistSongViewHolder */ - const val ITEM_TYPE_ARTIST_SONG = 0xA00A + const val VIEW_TYPE_ARTIST_SONG = 0xA00A /** GenreDetailViewHolder */ - const val ITEM_TYPE_GENRE_DETAIL = 0xA00B + const val VIEW_TYPE_GENRE_DETAIL = 0xA00B /** DiscHeaderViewHolder */ - const val ITEM_TYPE_DISC_HEADER = 0xA00C - - /** QueueSongViewHolder */ - const val ITEM_TYPE_QUEUE_SONG = 0xA00D + const val VIEW_TYPE_DISC_HEADER = 0xA00C /** "Music playback" notification code */ const val PLAYBACK_NOTIFICATION_CODE = 0xA0A0 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 83d21a4d7..50f84d186 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -88,7 +88,7 @@ class AlbumDetailFragment : binding.detailRecycler.apply { adapter = detailAdapter setSpanSizeLookup { pos -> - val item = detailAdapter.data.getItem(pos) + val item = detailModel.albumData.value[pos] item is Album || item is Header || item is SortHeader } } @@ -96,7 +96,7 @@ class AlbumDetailFragment : // -- VIEWMODEL SETUP --- collectImmediately(detailModel.currentAlbum, ::handleItemChange) - collectImmediately(detailModel.albumData, detailAdapter.data::submitList) + collectImmediately(detailModel.albumData, detailAdapter::submitList) collectImmediately(playbackModel.song, playbackModel.parent, ::updatePlayback) collect(navModel.exploreNavigationItem, ::handleNavigation) } @@ -227,7 +227,7 @@ class AlbumDetailFragment : /** Scroll to an song using its [id]. */ private fun scrollToItem(id: Long) { // Calculate where the item for the currently played song is - val pos = detailAdapter.data.currentList.indexOfFirst { it.id == id && it is Song } + val pos = detailModel.albumData.value.indexOfFirst { it.id == id && it is Song } if (pos != -1) { val binding = requireBinding() 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 0fd760e31..6ac312a5a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -83,7 +83,7 @@ class ArtistDetailFragment : binding.detailRecycler.apply { adapter = detailAdapter setSpanSizeLookup { pos -> - val item = detailAdapter.data.getItem(pos) + val item = detailModel.artistData.value[pos] item is Artist || item is Header || item is SortHeader } } @@ -91,7 +91,7 @@ class ArtistDetailFragment : // --- VIEWMODEL SETUP --- collectImmediately(detailModel.currentArtist, ::handleItemChange) - collectImmediately(detailModel.artistData, detailAdapter.data::submitList) + collectImmediately(detailModel.artistData, detailAdapter::submitList) collectImmediately(playbackModel.song, playbackModel.parent, ::updatePlayback) collect(navModel.exploreNavigationItem, ::handleNavigation) } 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 e287fd0c8..9d529c018 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -84,7 +84,7 @@ class GenreDetailFragment : binding.detailRecycler.apply { adapter = detailAdapter setSpanSizeLookup { pos -> - val item = detailAdapter.data.getItem(pos) + val item = detailModel.albumData.value[pos] item is Genre || item is Header || item is SortHeader } } @@ -92,7 +92,7 @@ class GenreDetailFragment : // --- VIEWMODEL SETUP --- collectImmediately(detailModel.currentGenre, ::handleItemChange) - collectImmediately(detailModel.genreData, detailAdapter.data::submitList) + collectImmediately(detailModel.genreData, detailAdapter::submitList) collectImmediately(playbackModel.song, playbackModel.parent, ::updatePlayback) collect(navModel.exploreNavigationItem, ::handleNavigation) } 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 1177502ca..7e14e2a3c 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 @@ -17,7 +17,8 @@ package org.oxycblt.auxio.detail.recycler -import android.content.Context +import android.view.View +import android.view.ViewGroup import androidx.core.view.isInvisible import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.IntegerTable @@ -28,7 +29,6 @@ import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding import org.oxycblt.auxio.detail.DiscHeader import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.ui.recycler.BindingViewHolder import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.SimpleItemCallback @@ -41,42 +41,40 @@ import org.oxycblt.auxio.util.inflater * An adapter for displaying [Album] information and it's children. * @author OxygenCobalt */ -class AlbumDetailAdapter(listener: Listener) : +class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFFER) { private var currentSong: Song? = null - override fun getCreatorFromItem(item: Item) = - super.getCreatorFromItem(item) - ?: when (item) { - is Album -> AlbumDetailViewHolder.CREATOR - is DiscHeader -> DiscHeaderViewHolder.CREATOR - is Song -> AlbumSongViewHolder.CREATOR - else -> null - } + override fun getItemViewType(position: Int) = + when (differ.currentList[position]) { + is Album -> AlbumDetailViewHolder.VIEW_TYPE + is DiscHeader -> DiscHeaderViewHolder.VIEW_TYPE + is Song -> AlbumSongViewHolder.VIEW_TYPE + else -> super.getItemViewType(position) + } - override fun getCreatorFromViewType(viewType: Int) = - super.getCreatorFromViewType(viewType) - ?: when (viewType) { - AlbumDetailViewHolder.CREATOR.viewType -> AlbumDetailViewHolder.CREATOR - DiscHeaderViewHolder.CREATOR.viewType -> DiscHeaderViewHolder.CREATOR - AlbumSongViewHolder.CREATOR.viewType -> AlbumSongViewHolder.CREATOR - else -> null - } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + when (viewType) { + AlbumDetailViewHolder.VIEW_TYPE -> AlbumDetailViewHolder.new(parent) + DiscHeaderViewHolder.VIEW_TYPE -> DiscHeaderViewHolder.new(parent) + AlbumSongViewHolder.VIEW_TYPE -> AlbumSongViewHolder.new(parent) + else -> super.onCreateViewHolder(parent, viewType) + } - override fun onBind( - viewHolder: RecyclerView.ViewHolder, - item: Item, - listener: Listener, + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, payload: List ) { - super.onBind(viewHolder, item, listener, payload) if (payload.isEmpty()) { - when (item) { - is Album -> (viewHolder as AlbumDetailViewHolder).bind(item, listener) - is DiscHeader -> (viewHolder as DiscHeaderViewHolder).bind(item, Unit) - is Song -> (viewHolder as AlbumSongViewHolder).bind(item, listener) + when (val item = differ.currentList[position]) { + is Album -> (holder as AlbumDetailViewHolder).bind(item, listener) + is DiscHeader -> (holder as DiscHeaderViewHolder).bind(item) + is Song -> (holder as AlbumSongViewHolder).bind(item, listener) } } + + super.onBindViewHolder(holder, position, payload) } override fun shouldHighlightViewHolder(item: Item) = item is Song && item.id == currentSong?.id @@ -111,9 +109,9 @@ class AlbumDetailAdapter(listener: Listener) : } private class AlbumDetailViewHolder private constructor(private val binding: ItemDetailBinding) : - BindingViewHolder(binding.root) { + RecyclerView.ViewHolder(binding.root) { - override fun bind(item: Album, listener: AlbumDetailAdapter.Listener) { + fun bind(item: Album, listener: AlbumDetailAdapter.Listener) { binding.detailCover.bind(item) binding.detailType.text = binding.context.getString(item.releaseType.stringRes) @@ -141,14 +139,10 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite } companion object { - val CREATOR = - object : Creator { - override val viewType: Int - get() = IntegerTable.ITEM_TYPE_ALBUM_DETAIL + const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ALBUM_DETAIL - override fun create(context: Context) = - AlbumDetailViewHolder(ItemDetailBinding.inflate(context.inflater)) - } + fun new(parent: View) = + AlbumDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater)) val DIFFER = object : SimpleItemCallback() { @@ -164,21 +158,17 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite } class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) : - BindingViewHolder(binding.root) { + RecyclerView.ViewHolder(binding.root) { - override fun bind(item: DiscHeader, listener: Unit) { + fun bind(item: DiscHeader) { binding.discNo.text = binding.context.getString(R.string.fmt_disc_no, item.disc) } companion object { - val CREATOR = - object : Creator { - override val viewType: Int - get() = IntegerTable.ITEM_TYPE_DISC_HEADER + const val VIEW_TYPE = IntegerTable.VIEW_TYPE_DISC_HEADER - override fun create(context: Context) = - DiscHeaderViewHolder(ItemDiscHeaderBinding.inflate(context.inflater)) - } + fun new(parent: View) = + DiscHeaderViewHolder(ItemDiscHeaderBinding.inflate(parent.context.inflater)) val DIFFER = object : SimpleItemCallback() { @@ -189,8 +179,8 @@ class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) : } private class AlbumSongViewHolder private constructor(private val binding: ItemAlbumSongBinding) : - BindingViewHolder(binding.root) { - override fun bind(item: Song, listener: MenuItemListener) { + RecyclerView.ViewHolder(binding.root) { + fun bind(item: Song, listener: MenuItemListener) { // Hide the track number view if the song does not have a track. if (item.track != null) { binding.songTrack.apply { @@ -218,14 +208,10 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA } companion object { - val CREATOR = - object : Creator { - override val viewType: Int - get() = IntegerTable.ITEM_TYPE_ALBUM_SONG + const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ALBUM_SONG - override fun create(context: Context) = - AlbumSongViewHolder(ItemAlbumSongBinding.inflate(context.inflater)) - } + fun new(parent: View) = + AlbumSongViewHolder(ItemAlbumSongBinding.inflate(parent.context.inflater)) val DIFFER = object : SimpleItemCallback() { 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 20df24e23..32da20b3e 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 @@ -17,7 +17,8 @@ package org.oxycblt.auxio.detail.recycler -import android.content.Context +import android.view.View +import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R @@ -29,7 +30,6 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.resolveYear import org.oxycblt.auxio.ui.recycler.ArtistViewHolder -import org.oxycblt.auxio.ui.recycler.BindingViewHolder import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.SimpleItemCallback @@ -42,44 +42,41 @@ import org.oxycblt.auxio.util.inflater * one actually contains both album information and song information. * @author OxygenCobalt */ -class ArtistDetailAdapter(listener: Listener) : +class ArtistDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFFER) { private var currentAlbum: Album? = null private var currentSong: Song? = null - override fun getCreatorFromItem(item: Item) = - super.getCreatorFromItem(item) - ?: when (item) { - is Artist -> ArtistDetailViewHolder.CREATOR - is Album -> ArtistAlbumViewHolder.CREATOR - is Song -> ArtistSongViewHolder.CREATOR - else -> null - } + override fun getItemViewType(position: Int) = + when (differ.currentList[position]) { + is Artist -> ArtistDetailViewHolder.VIEW_TYPE + is Album -> ArtistAlbumViewHolder.VIEW_TYPE + is Song -> ArtistSongViewHolder.VIEW_TYPE + else -> super.getItemViewType(position) + } - override fun getCreatorFromViewType(viewType: Int) = - super.getCreatorFromViewType(viewType) - ?: when (viewType) { - ArtistDetailViewHolder.CREATOR.viewType -> ArtistDetailViewHolder.CREATOR - ArtistAlbumViewHolder.CREATOR.viewType -> ArtistAlbumViewHolder.CREATOR - ArtistSongViewHolder.CREATOR.viewType -> ArtistSongViewHolder.CREATOR - else -> null - } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + when (viewType) { + ArtistDetailViewHolder.VIEW_TYPE -> ArtistDetailViewHolder.new(parent) + ArtistAlbumViewHolder.VIEW_TYPE -> ArtistAlbumViewHolder.new(parent) + ArtistSongViewHolder.VIEW_TYPE -> ArtistSongViewHolder.new(parent) + else -> super.onCreateViewHolder(parent, viewType) + } - override fun onBind( - viewHolder: RecyclerView.ViewHolder, - item: Item, - listener: Listener, + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, payload: List ) { - super.onBind(viewHolder, item, listener, payload) if (payload.isEmpty()) { - when (item) { - is Artist -> (viewHolder as ArtistDetailViewHolder).bind(item, listener) - is Album -> (viewHolder as ArtistAlbumViewHolder).bind(item, listener) - is Song -> (viewHolder as ArtistSongViewHolder).bind(item, listener) - else -> {} + when (val item = differ.currentList[position]) { + is Artist -> (holder as ArtistDetailViewHolder).bind(item, listener) + is Album -> (holder as ArtistAlbumViewHolder).bind(item, listener) + is Song -> (holder as ArtistSongViewHolder).bind(item, listener) } } + + super.onBindViewHolder(holder, position, payload) } override fun shouldHighlightViewHolder(item: Item) = @@ -119,9 +116,9 @@ class ArtistDetailAdapter(listener: Listener) : } private class ArtistDetailViewHolder private constructor(private val binding: ItemDetailBinding) : - BindingViewHolder(binding.root) { + RecyclerView.ViewHolder(binding.root) { - override fun bind(item: Artist, listener: DetailAdapter.Listener) { + fun bind(item: Artist, listener: DetailAdapter.Listener) { binding.detailCover.bind(item) binding.detailType.text = binding.context.getString(R.string.lbl_artist) binding.detailName.text = item.resolveName(binding.context) @@ -147,14 +144,10 @@ private class ArtistDetailViewHolder private constructor(private val binding: It } companion object { - val CREATOR = - object : Creator { - override val viewType: Int - get() = IntegerTable.ITEM_TYPE_ARTIST_DETAIL + const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST_DETAIL - override fun create(context: Context) = - ArtistDetailViewHolder(ItemDetailBinding.inflate(context.inflater)) - } + fun new(parent: View) = + ArtistDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater)) val DIFFER = ArtistViewHolder.DIFFER } @@ -163,8 +156,8 @@ private class ArtistDetailViewHolder private constructor(private val binding: It private class ArtistAlbumViewHolder private constructor( private val binding: ItemParentBinding, -) : BindingViewHolder(binding.root) { - override fun bind(item: Album, listener: MenuItemListener) { +) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: Album, listener: MenuItemListener) { binding.parentImage.bind(item) binding.parentName.text = item.resolveName(binding.context) binding.parentInfo.text = item.date.resolveYear(binding.context) @@ -177,14 +170,10 @@ private constructor( } companion object { - val CREATOR = - object : Creator { - override val viewType: Int - get() = IntegerTable.ITEM_TYPE_ARTIST_ALBUM + const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST_ALBUM - override fun create(context: Context) = - ArtistAlbumViewHolder(ItemParentBinding.inflate(context.inflater)) - } + fun new(parent: View) = + ArtistAlbumViewHolder(ItemParentBinding.inflate(parent.context.inflater)) val DIFFER = object : SimpleItemCallback() { @@ -197,8 +186,8 @@ private constructor( private class ArtistSongViewHolder private constructor( private val binding: ItemSongBinding, -) : BindingViewHolder(binding.root) { - override fun bind(item: Song, listener: MenuItemListener) { +) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: Song, listener: MenuItemListener) { binding.songAlbumCover.bind(item) binding.songName.text = item.resolveName(binding.context) binding.songInfo.text = item.album.resolveName(binding.context) @@ -211,14 +200,10 @@ private constructor( } companion object { - val CREATOR = - object : Creator { - override val viewType: Int - get() = IntegerTable.ITEM_TYPE_ARTIST_SONG + const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST_SONG - override fun create(context: Context) = - ArtistSongViewHolder(ItemSongBinding.inflate(context.inflater)) - } + fun new(parent: View) = + ArtistSongViewHolder(ItemSongBinding.inflate(parent.context.inflater)) val DIFFER = object : SimpleItemCallback() { 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 bdcb1025b..76c6a5d54 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 @@ -17,35 +17,71 @@ package org.oxycblt.auxio.detail.recycler -import android.content.Context import android.view.View +import android.view.ViewGroup import androidx.appcompat.widget.TooltipCompat +import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.databinding.ItemSortHeaderBinding import org.oxycblt.auxio.detail.SortHeader -import org.oxycblt.auxio.ui.recycler.AsyncBackingData -import org.oxycblt.auxio.ui.recycler.BindingViewHolder import org.oxycblt.auxio.ui.recycler.Header import org.oxycblt.auxio.ui.recycler.HeaderViewHolder import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.MenuItemListener -import org.oxycblt.auxio.ui.recycler.MultiAdapter import org.oxycblt.auxio.ui.recycler.SimpleItemCallback import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.logW abstract class DetailAdapter( - listener: L, + private val listener: L, diffCallback: DiffUtil.ItemCallback -) : MultiAdapter(listener) { - abstract fun shouldHighlightViewHolder(item: Item): Boolean +) : RecyclerView.Adapter() { + @Suppress("LeakingThis") override fun getItemCount() = differ.currentList.size + + override fun getItemViewType(position: Int) = + when (differ.currentList[position]) { + is Header -> HeaderViewHolder.VIEW_TYPE + is SortHeader -> SortHeaderViewHolder.VIEW_TYPE + else -> super.getItemViewType(position) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + when (viewType) { + HeaderViewHolder.VIEW_TYPE -> HeaderViewHolder.new(parent) + SortHeaderViewHolder.VIEW_TYPE -> SortHeaderViewHolder.new(parent) + else -> error("Invalid item type $viewType") + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) = + throw IllegalStateException() + + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + payload: List + ) { + val item = differ.currentList[position] + + if (payload.isEmpty()) { + when (item) { + is Header -> (holder as HeaderViewHolder).bind(item) + is SortHeader -> (holder as SortHeaderViewHolder).bind(item, listener) + } + } + + holder.itemView.isActivated = shouldHighlightViewHolder(item) + } + + protected val differ = AsyncListDiffer(this, diffCallback) + + protected abstract fun shouldHighlightViewHolder(item: Item): Boolean protected inline fun highlightImpl(oldItem: T?, newItem: T?) { if (oldItem != null) { - val pos = data.currentList.indexOfFirst { item -> item.id == oldItem.id && item is T } + val pos = differ.currentList.indexOfFirst { item -> item.id == oldItem.id && item is T } if (pos > -1) { notifyItemChanged(pos, PAYLOAD_HIGHLIGHT_CHANGED) @@ -55,7 +91,7 @@ abstract class DetailAdapter( } if (newItem != null) { - val pos = data.currentList.indexOfFirst { item -> item is T && item.id == newItem.id } + val pos = differ.currentList.indexOfFirst { item -> item is T && item.id == newItem.id } if (pos > -1) { notifyItemChanged(pos, PAYLOAD_HIGHLIGHT_CHANGED) @@ -65,36 +101,8 @@ abstract class DetailAdapter( } } - @Suppress("LeakingThis") override val data = AsyncBackingData(this, diffCallback) - - override fun getCreatorFromItem(item: Item) = - when (item) { - is Header -> HeaderViewHolder.CREATOR - is SortHeader -> SortHeaderViewHolder.CREATOR - else -> null - } - - override fun getCreatorFromViewType(viewType: Int) = - when (viewType) { - HeaderViewHolder.CREATOR.viewType -> HeaderViewHolder.CREATOR - SortHeaderViewHolder.CREATOR.viewType -> SortHeaderViewHolder.CREATOR - else -> null - } - - override fun onBind( - viewHolder: RecyclerView.ViewHolder, - item: Item, - listener: L, - payload: List - ) { - if (payload.isEmpty()) { - when (item) { - is Header -> (viewHolder as HeaderViewHolder).bind(item, Unit) - is SortHeader -> (viewHolder as SortHeaderViewHolder).bind(item, listener) - } - } - - viewHolder.itemView.isActivated = shouldHighlightViewHolder(item) + fun submitList(list: List) { + differ.submitList(list) } companion object { @@ -127,8 +135,8 @@ abstract class DetailAdapter( } class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) : - BindingViewHolder(binding.root) { - override fun bind(item: SortHeader, listener: DetailAdapter.Listener) { + RecyclerView.ViewHolder(binding.root) { + fun bind(item: SortHeader, listener: DetailAdapter.Listener) { binding.headerTitle.text = binding.context.getString(item.string) binding.headerButton.apply { TooltipCompat.setTooltipText(this, contentDescription) @@ -137,14 +145,10 @@ class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) : } companion object { - val CREATOR = - object : Creator { - override val viewType: Int - get() = IntegerTable.ITEM_TYPE_SORT_HEADER + const val VIEW_TYPE = IntegerTable.VIEW_TYPE_SORT_HEADER - override fun create(context: Context) = - SortHeaderViewHolder(ItemSortHeaderBinding.inflate(context.inflater)) - } + fun new(parent: View) = + SortHeaderViewHolder(ItemSortHeaderBinding.inflate(parent.context.inflater)) val DIFFER = object : SimpleItemCallback() { 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 1dda090e1..c02edf17f 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 @@ -17,14 +17,14 @@ package org.oxycblt.auxio.detail.recycler -import android.content.Context +import android.view.View +import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemDetailBinding import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.ui.recycler.BindingViewHolder import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.SimpleItemCallback import org.oxycblt.auxio.ui.recycler.SongViewHolder @@ -37,40 +37,37 @@ import org.oxycblt.auxio.util.inflater * An adapter for displaying genre information and it's children. * @author OxygenCobalt */ -class GenreDetailAdapter(listener: Listener) : +class GenreDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFFER) { private var currentSong: Song? = null - override fun getCreatorFromItem(item: Item) = - super.getCreatorFromItem(item) - ?: when (item) { - is Genre -> GenreDetailViewHolder.CREATOR - is Song -> SongViewHolder.CREATOR - else -> null - } + override fun getItemViewType(position: Int) = + when (differ.currentList[position]) { + is Genre -> GenreDetailViewHolder.VIEW_TYPE + is Song -> SongViewHolder.VIEW_TYPE + else -> super.getItemViewType(position) + } - override fun getCreatorFromViewType(viewType: Int) = - super.getCreatorFromViewType(viewType) - ?: when (viewType) { - GenreDetailViewHolder.CREATOR.viewType -> GenreDetailViewHolder.CREATOR - SongViewHolder.CREATOR.viewType -> SongViewHolder.CREATOR - else -> null - } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + when (viewType) { + GenreDetailViewHolder.VIEW_TYPE -> GenreDetailViewHolder.new(parent) + SongViewHolder.VIEW_TYPE -> SongViewHolder.new(parent) + else -> super.onCreateViewHolder(parent, viewType) + } - override fun onBind( - viewHolder: RecyclerView.ViewHolder, - item: Item, - listener: Listener, + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, payload: List ) { - super.onBind(viewHolder, item, listener, payload) if (payload.isEmpty()) { - when (item) { - is Genre -> (viewHolder as GenreDetailViewHolder).bind(item, listener) - is Song -> (viewHolder as SongViewHolder).bind(item, listener) - else -> {} + when (val item = differ.currentList[position]) { + is Genre -> (holder as GenreDetailViewHolder).bind(item, listener) + is Song -> (holder as SongViewHolder).bind(item, listener) } } + + super.onBindViewHolder(holder, position, payload) } override fun shouldHighlightViewHolder(item: Item) = item is Song && item.id == currentSong?.id @@ -99,8 +96,8 @@ class GenreDetailAdapter(listener: Listener) : } private class GenreDetailViewHolder private constructor(private val binding: ItemDetailBinding) : - BindingViewHolder(binding.root) { - override fun bind(item: Genre, listener: DetailAdapter.Listener) { + RecyclerView.ViewHolder(binding.root) { + fun bind(item: Genre, listener: DetailAdapter.Listener) { binding.detailCover.bind(item) binding.detailType.text = binding.context.getString(R.string.lbl_genre) binding.detailName.text = item.resolveName(binding.context) @@ -112,14 +109,10 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite } companion object { - val CREATOR = - object : Creator { - override val viewType: Int - get() = IntegerTable.ITEM_TYPE_GENRE_DETAIL + const val VIEW_TYPE = IntegerTable.VIEW_TYPE_GENRE_DETAIL - override fun create(context: Context) = - GenreDetailViewHolder(ItemDetailBinding.inflate(context.inflater)) - } + fun new(parent: View) = + GenreDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater)) val DIFFER = object : SimpleItemCallback() { 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 e3a374db1..518be4c84 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 @@ -20,6 +20,8 @@ package org.oxycblt.auxio.home.list import android.os.Bundle import android.text.format.DateUtils import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView import java.util.* import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding @@ -30,8 +32,7 @@ import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.recycler.AlbumViewHolder import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.MenuItemListener -import org.oxycblt.auxio.ui.recycler.MonoAdapter -import org.oxycblt.auxio.ui.recycler.SyncBackingData +import org.oxycblt.auxio.ui.recycler.SyncListDiffer import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.formatDurationMs import org.oxycblt.auxio.util.logEOrThrow @@ -54,7 +55,7 @@ class AlbumListFragment : HomeListFragment() { adapter = homeAdapter } - collectImmediately(homeModel.albums, homeAdapter.data::replaceList) + collectImmediately(homeModel.albums, homeAdapter::replaceList) } override fun getPopup(pos: Int): String? { @@ -107,9 +108,21 @@ class AlbumListFragment : HomeListFragment() { } } - class AlbumAdapter(listener: MenuItemListener) : - MonoAdapter(listener) { - override val data = SyncBackingData(this, AlbumViewHolder.DIFFER) - override val creator = AlbumViewHolder.CREATOR + private class AlbumAdapter(private val listener: MenuItemListener) : + RecyclerView.Adapter() { + private val differ = SyncListDiffer(this, AlbumViewHolder.DIFFER) + + override fun getItemCount() = differ.currentList.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + AlbumViewHolder.new(parent) + + override fun onBindViewHolder(holder: AlbumViewHolder, position: Int) { + holder.bind(differ.currentList[position], listener) + } + + fun replaceList(newList: List) { + differ.replaceList(newList) + } } } 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 bacbf2154..792f006b4 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 @@ -19,6 +19,8 @@ package org.oxycblt.auxio.home.list import android.os.Bundle import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.music.Artist @@ -28,8 +30,7 @@ import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.recycler.ArtistViewHolder import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.MenuItemListener -import org.oxycblt.auxio.ui.recycler.MonoAdapter -import org.oxycblt.auxio.ui.recycler.SyncBackingData +import org.oxycblt.auxio.ui.recycler.SyncListDiffer import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.formatDurationMs import org.oxycblt.auxio.util.logEOrThrow @@ -49,7 +50,7 @@ class ArtistListFragment : HomeListFragment() { adapter = homeAdapter } - collectImmediately(homeModel.artists, homeAdapter.data::replaceList) + collectImmediately(homeModel.artists, homeAdapter::replaceList) } override fun getPopup(pos: Int): String? { @@ -83,9 +84,21 @@ class ArtistListFragment : HomeListFragment() { } } - class ArtistAdapter(listener: MenuItemListener) : - MonoAdapter(listener) { - override val data = SyncBackingData(this, ArtistViewHolder.DIFFER) - override val creator = ArtistViewHolder.CREATOR + private class ArtistAdapter(private val listener: MenuItemListener) : + RecyclerView.Adapter() { + private val differ = SyncListDiffer(this, ArtistViewHolder.DIFFER) + + override fun getItemCount() = differ.currentList.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + ArtistViewHolder.new(parent) + + override fun onBindViewHolder(holder: ArtistViewHolder, position: Int) { + holder.bind(differ.currentList[position], listener) + } + + fun replaceList(newList: List) { + differ.replaceList(newList) + } } } 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 8c0e77b12..52787c4e4 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 @@ -19,6 +19,8 @@ package org.oxycblt.auxio.home.list import android.os.Bundle import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.music.Genre @@ -28,8 +30,7 @@ import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.recycler.GenreViewHolder import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.MenuItemListener -import org.oxycblt.auxio.ui.recycler.MonoAdapter -import org.oxycblt.auxio.ui.recycler.SyncBackingData +import org.oxycblt.auxio.ui.recycler.SyncListDiffer import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.formatDurationMs import org.oxycblt.auxio.util.logEOrThrow @@ -49,7 +50,7 @@ class GenreListFragment : HomeListFragment() { adapter = homeAdapter } - collectImmediately(homeModel.genres, homeAdapter.data::replaceList) + collectImmediately(homeModel.genres, homeAdapter::replaceList) } override fun getPopup(pos: Int): String? { @@ -83,9 +84,21 @@ class GenreListFragment : HomeListFragment() { } } - class GenreAdapter(listener: MenuItemListener) : - MonoAdapter(listener) { - override val data = SyncBackingData(this, GenreViewHolder.DIFFER) - override val creator = GenreViewHolder.CREATOR + private class GenreAdapter(private val listener: MenuItemListener) : + RecyclerView.Adapter() { + private val differ = SyncListDiffer(this, GenreViewHolder.DIFFER) + + override fun getItemCount() = differ.currentList.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + GenreViewHolder.new(parent) + + override fun onBindViewHolder(holder: GenreViewHolder, position: Int) { + holder.bind(differ.currentList[position], listener) + } + + fun replaceList(newList: List) { + differ.replaceList(newList) + } } } 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 095e4d11b..dd013e360 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 @@ -20,6 +20,8 @@ package org.oxycblt.auxio.home.list import android.os.Bundle import android.text.format.DateUtils import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView import java.util.Formatter import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding @@ -29,9 +31,8 @@ import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.MenuItemListener -import org.oxycblt.auxio.ui.recycler.MonoAdapter import org.oxycblt.auxio.ui.recycler.SongViewHolder -import org.oxycblt.auxio.ui.recycler.SyncBackingData +import org.oxycblt.auxio.ui.recycler.SyncListDiffer import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.formatDurationMs @@ -43,7 +44,7 @@ import org.oxycblt.auxio.util.secsToMs * @author */ class SongListFragment : HomeListFragment() { - private val homeAdapter = SongsAdapter(this) + private val homeAdapter = SongAdapter(this) private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } private val formatterSb = StringBuilder(50) private val formatter = Formatter(formatterSb) @@ -56,7 +57,7 @@ class SongListFragment : HomeListFragment() { adapter = homeAdapter } - collectImmediately(homeModel.songs, homeAdapter.data::replaceList) + collectImmediately(homeModel.songs, homeAdapter::replaceList) } override fun getPopup(pos: Int): String? { @@ -111,9 +112,21 @@ class SongListFragment : HomeListFragment() { } } - inner class SongsAdapter(listener: MenuItemListener) : - MonoAdapter(listener) { - override val data = SyncBackingData(this, SongViewHolder.DIFFER) - override val creator = SongViewHolder.CREATOR + private class SongAdapter(private val listener: MenuItemListener) : + RecyclerView.Adapter() { + private val differ = SyncListDiffer(this, SongViewHolder.DIFFER) + + override fun getItemCount() = differ.currentList.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + SongViewHolder.new(parent) + + override fun onBindViewHolder(holder: SongViewHolder, position: Int) { + holder.bind(differ.currentList[position], listener) + } + + fun replaceList(newList: List) { + differ.replaceList(newList) + } } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt index 4ef68fe1d..571e9b280 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt @@ -18,63 +18,65 @@ package org.oxycblt.auxio.home.tabs import android.annotation.SuppressLint -import android.content.Context import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.databinding.ItemTabBinding import org.oxycblt.auxio.ui.DisplayMode -import org.oxycblt.auxio.ui.recycler.BackingData -import org.oxycblt.auxio.ui.recycler.BindingViewHolder -import org.oxycblt.auxio.ui.recycler.MonoAdapter import org.oxycblt.auxio.util.inflater -class TabAdapter(listener: Listener) : - MonoAdapter(listener) { - override val data = TabData(this) - override val creator = TabViewHolder.CREATOR +class TabAdapter(private val listener: Listener) : RecyclerView.Adapter() { + var tabs = arrayOf() + private set + + override fun getItemCount() = tabs.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = TabViewHolder.new(parent) + + override fun onBindViewHolder(holder: TabViewHolder, position: Int) { + holder.bind(tabs[position], listener) + } + + @Suppress("NotifyDatasetChanged") + fun submitTabs(newTabs: Array) { + tabs = newTabs + notifyDataSetChanged() + } + + fun setTab(at: Int, tab: Tab) { + tabs[at] = tab + notifyItemChanged(at, PAYLOAD_TAB_CHANGED) + } + + fun moveItems(from: Int, to: Int) { + val t = tabs[to] + val f = tabs[from] + tabs[from] = t + tabs[to] = f + notifyItemMoved(from, to) + } interface Listener { fun onVisibilityToggled(displayMode: DisplayMode) fun onPickUpTab(viewHolder: RecyclerView.ViewHolder) } - class TabData(private val adapter: RecyclerView.Adapter<*>) : BackingData() { - var tabs = arrayOf() - private set - - override fun getItem(position: Int) = tabs[position] - override fun getItemCount() = tabs.size - - @Suppress("NotifyDatasetChanged") - fun submitTabs(newTabs: Array) { - tabs = newTabs - adapter.notifyDataSetChanged() - } - - fun setTab(at: Int, tab: Tab) { - tabs[at] = tab - adapter.notifyItemChanged(at, PAYLOAD_TAB_CHANGED) - } - - fun moveItems(from: Int, to: Int) { - val t = tabs[to] - val f = tabs[from] - tabs[from] = t - tabs[to] = f - adapter.notifyItemMoved(from, to) - } - } - companion object { val PAYLOAD_TAB_CHANGED = Any() } } class TabViewHolder private constructor(private val binding: ItemTabBinding) : - BindingViewHolder(binding.root) { + RecyclerView.ViewHolder(binding.root) { @SuppressLint("ClickableViewAccessibility") - override fun bind(item: Tab, listener: TabAdapter.Listener) { - binding.root.apply { setOnClickListener { listener.onVisibilityToggled(item.mode) } } + fun bind(item: Tab, listener: TabAdapter.Listener) { + // Actually make the item full-width, which it won't be in dialogs + binding.root.layoutParams = + RecyclerView.LayoutParams( + RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) + + binding.root.setOnClickListener { listener.onVisibilityToggled(item.mode) } binding.tabIcon.apply { setText(item.mode.string) @@ -92,13 +94,6 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) : } companion object { - val CREATOR = - object : Creator { - override val viewType: Int - get() = throw UnsupportedOperationException() - - override fun create(context: Context) = - TabViewHolder(ItemTabBinding.inflate(context.inflater)) - } + fun new(parent: View) = TabViewHolder(ItemTabBinding.inflate(parent.context.inflater)) } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt index 588342d1a..387250530 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt @@ -49,7 +49,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment(), TabAd .setTitle(R.string.set_lib_tabs) .setPositiveButton(R.string.lbl_ok) { _, _ -> logD("Committing tab changes") - settings.libTabs = tabAdapter.data.tabs + settings.libTabs = tabAdapter.tabs } .setNegativeButton(R.string.lbl_cancel, null) } @@ -58,9 +58,9 @@ class TabCustomizeDialog : ViewBindingDialogFragment(), TabAd val savedTabs = findSavedTabState(savedInstanceState) if (savedTabs != null) { logD("Found saved tab state") - tabAdapter.data.submitTabs(savedTabs) + tabAdapter.submitTabs(savedTabs) } else { - tabAdapter.data.submitTabs(settings.libTabs) + tabAdapter.submitTabs(settings.libTabs) } binding.tabRecycler.apply { @@ -71,7 +71,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment(), TabAd override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putInt(KEY_TABS, Tab.toSequence(tabAdapter.data.tabs)) + outState.putInt(KEY_TABS, Tab.toSequence(tabAdapter.tabs)) } override fun onDestroyBinding(binding: DialogTabsBinding) { @@ -83,10 +83,10 @@ class TabCustomizeDialog : ViewBindingDialogFragment(), TabAd // Tab viewholders bind with the initial tab state, which will drift from the actual // state of the tabs over editing. So, this callback simply provides the displayMode // for us to locate within the data and then update. - val index = tabAdapter.data.tabs.indexOfFirst { it.mode == displayMode } + val index = tabAdapter.tabs.indexOfFirst { it.mode == displayMode } if (index > -1) { - val tab = tabAdapter.data.tabs[index] - tabAdapter.data.setTab( + val tab = tabAdapter.tabs[index] + tabAdapter.setTab( index, when (tab) { is Tab.Visible -> Tab.Invisible(tab.mode) @@ -95,7 +95,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment(), TabAd } (requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = - tabAdapter.data.tabs.filterIsInstance().isNotEmpty() + tabAdapter.tabs.filterIsInstance().isNotEmpty() } override fun onPickUpTab(viewHolder: RecyclerView.ViewHolder) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt index 0d20446e7..bdf71f3b6 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt @@ -56,7 +56,7 @@ class TabDragCallback(private val adapter: TabAdapter) : ItemTouchHelper.Callbac viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder ): Boolean { - adapter.data.moveItems(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition) + adapter.moveItems(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition) return true } diff --git a/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirAdapter.kt index 05078773c..075a9d581 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirAdapter.kt @@ -17,74 +17,71 @@ package org.oxycblt.auxio.music.dirs -import android.content.Context +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.databinding.ItemMusicDirBinding import org.oxycblt.auxio.music.Directory -import org.oxycblt.auxio.ui.recycler.BackingData -import org.oxycblt.auxio.ui.recycler.BindingViewHolder -import org.oxycblt.auxio.ui.recycler.MonoAdapter import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater /** - * Adapter that shows the excluded directories and their "Clear" button. + * Adapter that shows the list of music folder and their "Clear" button. * @author OxygenCobalt */ -class MusicDirAdapter(listener: Listener) : - MonoAdapter(listener) { - override val data = ExcludedBackingData(this) - override val creator = MusicDirViewHolder.CREATOR +class MusicDirAdapter(private val listener: Listener) : RecyclerView.Adapter() { + private val _dirs = mutableListOf() + val dirs: List = _dirs + + override fun getItemCount() = dirs.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + MusicDirViewHolder.new(parent) + + override fun onBindViewHolder(holder: MusicDirViewHolder, position: Int) = + holder.bind(dirs[position], listener) + + fun add(dir: Directory) { + if (_dirs.contains(dir)) { + return + } + + _dirs.add(dir) + notifyItemInserted(_dirs.lastIndex) + } + + fun addAll(dirs: List) { + val oldLastIndex = dirs.lastIndex + _dirs.addAll(dirs) + notifyItemRangeInserted(oldLastIndex, dirs.size) + } + + fun remove(dir: Directory) { + val idx = _dirs.indexOf(dir) + _dirs.removeAt(idx) + notifyItemRemoved(idx) + } interface Listener { fun onRemoveDirectory(dir: Directory) } - - class ExcludedBackingData(private val adapter: MusicDirAdapter) : BackingData() { - private val _currentList = mutableListOf() - val currentList: List = _currentList - - override fun getItemCount(): Int = _currentList.size - override fun getItem(position: Int): Directory = _currentList[position] - - fun add(dir: Directory) { - if (_currentList.contains(dir)) { - return - } - - _currentList.add(dir) - adapter.notifyItemInserted(_currentList.lastIndex) - } - - fun addAll(dirs: List) { - val oldLastIndex = dirs.lastIndex - _currentList.addAll(dirs) - adapter.notifyItemRangeInserted(oldLastIndex, dirs.size) - } - - fun remove(dir: Directory) { - val idx = _currentList.indexOf(dir) - _currentList.removeAt(idx) - adapter.notifyItemRemoved(idx) - } - } } /** The viewholder for [MusicDirAdapter]. Not intended for use in other adapters. */ class MusicDirViewHolder private constructor(private val binding: ItemMusicDirBinding) : - BindingViewHolder(binding.root) { - override fun bind(item: Directory, listener: MusicDirAdapter.Listener) { + RecyclerView.ViewHolder(binding.root) { + fun bind(item: Directory, listener: MusicDirAdapter.Listener) { + // Actually make the item full-width, which it won't be in dialogs + binding.root.layoutParams = + RecyclerView.LayoutParams( + RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) + binding.dirPath.text = item.resolveName(binding.context) binding.dirDelete.setOnClickListener { listener.onRemoveDirectory(item) } } companion object { - val CREATOR = - object : Creator { - override val viewType: Int - get() = throw UnsupportedOperationException() - - override fun create(context: Context) = - MusicDirViewHolder(ItemMusicDirBinding.inflate(context.inflater)) - } + fun new(parent: View) = + MusicDirViewHolder(ItemMusicDirBinding.inflate(parent.context.inflater)) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt index 652db6ee6..30465aa99 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt @@ -60,9 +60,7 @@ class MusicDirsDialog : .setPositiveButton(R.string.lbl_save) { _, _ -> val dirs = settings.getMusicDirs(storageManager) val newDirs = - MusicDirs( - dirs = dirAdapter.data.currentList, - shouldInclude = isInclude(requireBinding())) + MusicDirs(dirs = dirAdapter.dirs, shouldInclude = isInclude(requireBinding())) if (dirs != newDirs) { logD("Committing changes") settings.setMusicDirs(newDirs) @@ -105,7 +103,7 @@ class MusicDirsDialog : } } - dirAdapter.data.addAll(dirs.dirs) + dirAdapter.addAll(dirs.dirs) requireBinding().dirsEmpty.isVisible = dirs.dirs.isEmpty() binding.folderModeGroup.apply { @@ -124,7 +122,7 @@ class MusicDirsDialog : override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putStringArrayList( - KEY_PENDING_DIRS, ArrayList(dirAdapter.data.currentList.map { it.toString() })) + KEY_PENDING_DIRS, ArrayList(dirAdapter.dirs.map { it.toString() })) outState.putBoolean(KEY_PENDING_MODE, isInclude(requireBinding())) } @@ -134,8 +132,8 @@ class MusicDirsDialog : } override fun onRemoveDirectory(dir: Directory) { - dirAdapter.data.remove(dir) - requireBinding().dirsEmpty.isVisible = dirAdapter.data.currentList.isEmpty() + dirAdapter.remove(dir) + requireBinding().dirsEmpty.isVisible = dirAdapter.dirs.isEmpty() } private fun addDocTreePath(uri: Uri?) { @@ -147,7 +145,7 @@ class MusicDirsDialog : val dir = parseExcludedUri(uri) if (dir != null) { - dirAdapter.data.add(dir) + dirAdapter.add(dir) requireBinding().dirsEmpty.isVisible = false } else { requireContext().showToast(R.string.err_bad_dir) 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 6b3785e29..aeeb95990 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 @@ -18,26 +18,31 @@ package org.oxycblt.auxio.playback.queue import android.annotation.SuppressLint -import android.content.Context import android.graphics.drawable.LayerDrawable import android.view.MotionEvent import android.view.View +import android.view.ViewGroup import androidx.core.view.isInvisible import androidx.recyclerview.widget.RecyclerView import com.google.android.material.shape.MaterialShapeDrawable -import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemQueueSongBinding import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.ui.recycler.* import org.oxycblt.auxio.util.* -class QueueAdapter(listener: QueueItemListener) : - MonoAdapter(listener) { +class QueueAdapter(private val listener: QueueItemListener) : + RecyclerView.Adapter() { + private var differ = SyncListDiffer(this, QueueSongViewHolder.DIFFER) private var currentIndex = 0 - override val data = SyncBackingData(this, QueueSongViewHolder.DIFFER) - override val creator = QueueSongViewHolder.CREATOR + override fun getItemCount() = differ.currentList.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + QueueSongViewHolder.new(parent) + + override fun onBindViewHolder(holder: QueueSongViewHolder, position: Int) = + throw IllegalStateException() override fun onBindViewHolder( viewHolder: QueueSongViewHolder, @@ -45,13 +50,21 @@ class QueueAdapter(listener: QueueItemListener) : payload: List ) { if (payload.isEmpty()) { - super.onBindViewHolder(viewHolder, position, payload) + viewHolder.bind(differ.currentList[position], listener) } viewHolder.isEnabled = position > currentIndex viewHolder.isActivated = position == currentIndex } + fun submitList(newList: List) { + differ.submitList(newList) + } + + fun replaceList(newList: List) { + differ.replaceList(newList) + } + fun updateIndex(index: Int) { when { index < currentIndex -> { @@ -79,7 +92,7 @@ interface QueueItemListener { class QueueSongViewHolder private constructor( private val binding: ItemQueueSongBinding, -) : BindingViewHolder(binding.root) { +) : RecyclerView.ViewHolder(binding.root) { val bodyView: View get() = binding.body val backgroundView: View @@ -92,23 +105,6 @@ private constructor( alpha = 0 } - var isEnabled: Boolean - get() = binding.songAlbumCover.isEnabled - set(value) { - // Don't want to disable clicking, just indicate the body and handle is disabled - binding.songAlbumCover.isEnabled = value - binding.songName.isEnabled = value - binding.songInfo.isEnabled = value - binding.songDragHandle.isEnabled = value - } - - var isActivated: Boolean - get() = binding.interactBody.isActivated - set(value) { - // Activation does not affect clicking, make everything activated. - binding.interactBody.isActivated = value - } - init { binding.body.background = LayerDrawable( @@ -121,7 +117,7 @@ private constructor( } @SuppressLint("ClickableViewAccessibility") - override fun bind(item: Song, listener: QueueItemListener) { + fun bind(item: Song, listener: QueueItemListener) { binding.songAlbumCover.bind(item) binding.songName.text = item.resolveName(binding.context) binding.songInfo.text = item.resolveIndividualArtistName(binding.context) @@ -140,15 +136,26 @@ private constructor( } } - companion object { - val CREATOR = - object : Creator { - override val viewType: Int - get() = IntegerTable.ITEM_TYPE_QUEUE_SONG + var isEnabled: Boolean + get() = binding.songAlbumCover.isEnabled + set(value) { + // Don't want to disable clicking, just indicate the body and handle is disabled + binding.songAlbumCover.isEnabled = value + binding.songName.isEnabled = value + binding.songInfo.isEnabled = value + binding.songDragHandle.isEnabled = value + } - override fun create(context: Context): QueueSongViewHolder = - QueueSongViewHolder(ItemQueueSongBinding.inflate(context.inflater)) - } + var isActivated: Boolean + get() = binding.interactBody.isActivated + set(value) { + // Activation does not affect clicking, make everything activated. + binding.interactBody.isActivated = value + } + + companion object { + fun new(parent: View) = + QueueSongViewHolder(ItemQueueSongBinding.inflate(parent.context.inflater)) val DIFFER = SongViewHolder.DIFFER } 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 13d0c1978..285292d5c 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 @@ -85,10 +85,10 @@ class QueueFragment : ViewBindingFragment(), QueueItemList val replaceQueue = queueModel.replaceQueue if (replaceQueue == true) { logD("Replacing queue") - queueAdapter.data.replaceList(queue) + queueAdapter.replaceList(queue) } else { logD("Diffing queue") - queueAdapter.data.submitList(queue) + queueAdapter.submitList(queue) } binding.queueDivider.isInvisible = diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index 28d39bbf9..ca2bbe723 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -200,6 +200,7 @@ class PlaybackService : playbackManager.isPlaying = false playbackManager.unregisterInternalPlayer(this) + musicStore.addCallback(this) settings.release() unregisterReceiver(systemReceiver) serviceJob.cancel() 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 2e51cc4d5..095375abc 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt @@ -17,6 +17,8 @@ package org.oxycblt.auxio.search +import android.view.ViewGroup +import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist @@ -24,55 +26,52 @@ import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.ui.recycler.AlbumViewHolder import org.oxycblt.auxio.ui.recycler.ArtistViewHolder -import org.oxycblt.auxio.ui.recycler.AsyncBackingData import org.oxycblt.auxio.ui.recycler.GenreViewHolder import org.oxycblt.auxio.ui.recycler.Header import org.oxycblt.auxio.ui.recycler.HeaderViewHolder import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.MenuItemListener -import org.oxycblt.auxio.ui.recycler.MultiAdapter import org.oxycblt.auxio.ui.recycler.SimpleItemCallback import org.oxycblt.auxio.ui.recycler.SongViewHolder -class SearchAdapter(listener: MenuItemListener) : MultiAdapter(listener) { - override val data = AsyncBackingData(this, DIFFER) +class SearchAdapter(private val listener: MenuItemListener) : + RecyclerView.Adapter() { + private val differ = AsyncListDiffer(this, DIFFER) - override fun getCreatorFromItem(item: Item) = - when (item) { - is Song -> SongViewHolder.CREATOR - is Album -> AlbumViewHolder.CREATOR - is Artist -> ArtistViewHolder.CREATOR - is Genre -> GenreViewHolder.CREATOR - is Header -> HeaderViewHolder.CREATOR - else -> null + override fun getItemCount() = differ.currentList.size + + override fun getItemViewType(position: Int) = + when (differ.currentList[position]) { + is Song -> SongViewHolder.VIEW_TYPE + is Album -> AlbumViewHolder.VIEW_TYPE + is Artist -> ArtistViewHolder.VIEW_TYPE + is Genre -> HeaderViewHolder.VIEW_TYPE + is Header -> HeaderViewHolder.VIEW_TYPE + else -> super.getItemViewType(position) } - override fun getCreatorFromViewType(viewType: Int) = + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { - SongViewHolder.CREATOR.viewType -> SongViewHolder.CREATOR - AlbumViewHolder.CREATOR.viewType -> AlbumViewHolder.CREATOR - ArtistViewHolder.CREATOR.viewType -> ArtistViewHolder.CREATOR - GenreViewHolder.CREATOR.viewType -> GenreViewHolder.CREATOR - HeaderViewHolder.CREATOR.viewType -> HeaderViewHolder.CREATOR - else -> null + SongViewHolder.VIEW_TYPE -> SongViewHolder.new(parent) + AlbumViewHolder.VIEW_TYPE -> AlbumViewHolder.new(parent) + ArtistViewHolder.VIEW_TYPE -> ArtistViewHolder.new(parent) + GenreViewHolder.VIEW_TYPE -> GenreViewHolder.new(parent) + HeaderViewHolder.VIEW_TYPE -> HeaderViewHolder.new(parent) + else -> error("Invalid item type $viewType") } - override fun onBind( - viewHolder: RecyclerView.ViewHolder, - item: Item, - listener: MenuItemListener, - payload: List - ) { - when (item) { - is Song -> (viewHolder as SongViewHolder).bind(item, listener) - is Album -> (viewHolder as AlbumViewHolder).bind(item, listener) - is Artist -> (viewHolder as ArtistViewHolder).bind(item, listener) - is Genre -> (viewHolder as GenreViewHolder).bind(item, listener) - is Header -> (viewHolder as HeaderViewHolder).bind(item, Unit) - else -> {} + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (val item = differ.currentList[position]) { + is Song -> (holder as SongViewHolder).bind(item, listener) + is Album -> (holder as AlbumViewHolder).bind(item, listener) + is Artist -> (holder as ArtistViewHolder).bind(item, listener) + is Genre -> (holder as GenreViewHolder).bind(item, listener) + is Header -> (holder as HeaderViewHolder).bind(item) } } + fun submitList(list: List, callback: () -> Unit) = differ.submitList(list, callback) + companion object { private val DIFFER = object : SimpleItemCallback() { 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 549265919..d4834cc34 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -106,7 +106,7 @@ class SearchFragment : binding.searchRecycler.apply { adapter = searchAdapter - setSpanSizeLookup { pos -> searchAdapter.data.getItem(pos) is Header } + setSpanSizeLookup { pos -> searchModel.searchResults.value[pos] is Header } } // --- VIEWMODEL SETUP --- @@ -154,7 +154,7 @@ class SearchFragment : private fun updateResults(results: List) { val binding = requireBinding() - searchAdapter.data.submitList(results.toMutableList()) { + searchAdapter.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. diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt index a58600062..f71e721ba 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt @@ -17,13 +17,12 @@ package org.oxycblt.auxio.ui.accent -import android.content.Context +import android.view.View +import android.view.ViewGroup import androidx.appcompat.widget.TooltipCompat +import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemAccentBinding -import org.oxycblt.auxio.ui.recycler.BackingData -import org.oxycblt.auxio.ui.recycler.BindingViewHolder -import org.oxycblt.auxio.ui.recycler.MonoAdapter import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getColorCompat import org.oxycblt.auxio.util.inflater @@ -32,25 +31,29 @@ import org.oxycblt.auxio.util.inflater * An adapter that displays the accent palette. * @author OxygenCobalt */ -class AccentAdapter(listener: Listener) : - MonoAdapter(listener) { +class AccentAdapter(private val listener: Listener) : RecyclerView.Adapter() { var selectedAccent: Accent? = null private set - override val data = AccentData() - override val creator = AccentViewHolder.CREATOR + override fun getItemCount() = Accent.MAX - override fun onBind( - viewHolder: AccentViewHolder, - item: Accent, - listener: Listener, - payload: List + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = AccentViewHolder.new(parent) + + override fun onBindViewHolder(holder: AccentViewHolder, position: Int) = + throw IllegalStateException() + + override fun onBindViewHolder( + holder: AccentViewHolder, + position: Int, + payloads: MutableList ) { - if (payload.isEmpty()) { - super.onBind(viewHolder, item, listener, payload) + val item = Accent.from(position) + + if (payloads.isEmpty()) { + holder.bind(item, listener) } - viewHolder.setSelected(item == selectedAccent) + holder.setSelected(item == selectedAccent) } fun setSelectedAccent(accent: Accent) { @@ -64,20 +67,15 @@ class AccentAdapter(listener: Listener) : fun onAccentSelected(accent: Accent) } - class AccentData : BackingData() { - override fun getItem(position: Int) = Accent.from(position) - override fun getItemCount() = Accent.MAX - } - companion object { val PAYLOAD_SELECTION_CHANGED = Any() } } class AccentViewHolder private constructor(private val binding: ItemAccentBinding) : - BindingViewHolder(binding.root) { + RecyclerView.ViewHolder(binding.root) { - override fun bind(item: Accent, listener: AccentAdapter.Listener) { + fun bind(item: Accent, listener: AccentAdapter.Listener) { setSelected(false) binding.accent.apply { @@ -101,13 +99,6 @@ class AccentViewHolder private constructor(private val binding: ItemAccentBindin } companion object { - val CREATOR = - object : Creator { - override val viewType: Int - get() = throw UnsupportedOperationException() - - override fun create(context: Context) = - AccentViewHolder(ItemAccentBinding.inflate(context.inflater)) - } + fun new(parent: View) = AccentViewHolder(ItemAccentBinding.inflate(parent.context.inflater)) } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/fastscroll/FastScrollPopupView.kt b/app/src/main/java/org/oxycblt/auxio/ui/fastscroll/FastScrollPopupView.kt index a2e533d72..e5b07584d 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/fastscroll/FastScrollPopupView.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/fastscroll/FastScrollPopupView.kt @@ -38,6 +38,10 @@ import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getDimenSize import org.oxycblt.auxio.util.isRtl +/** + * Internal view responsible for the fast scroller popup. + * @author OxygenCobalt, Hai Zhang + */ class FastScrollPopupView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0) : diff --git a/app/src/main/java/org/oxycblt/auxio/ui/recycler/RecyclerFramework.kt b/app/src/main/java/org/oxycblt/auxio/ui/recycler/RecyclerFramework.kt index d2a86bc18..3d3843ea1 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/recycler/RecyclerFramework.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/recycler/RecyclerFramework.kt @@ -17,145 +17,13 @@ package org.oxycblt.auxio.ui.recycler -import android.content.Context import android.view.View -import android.view.ViewGroup import androidx.annotation.StringRes import androidx.recyclerview.widget.AdapterListUpdateCallback import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -// TODO: Unify music updates and sorts under replace - -/** - * An adapter for one viewholder tied to one type of data. All functionality is derived from the - * overridden values. - * @author OxygenCobalt - */ -abstract class MonoAdapter>(private val listener: L) : - RecyclerView.Adapter() { - /** The data that the adapter will source to bind viewholders. */ - abstract val data: BackingData - /** The creator instance that all viewholders will be derived from. */ - protected abstract val creator: BindingViewHolder.Creator - - /** - * An optional override to further modify the given [viewHolder]. The normal operation is to - * bind the viewholder. - */ - open fun onBind(viewHolder: VH, item: T, listener: L, payload: List) { - viewHolder.bind(item, listener) - } - - override fun getItemCount(): Int = data.getItemCount() - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - creator.create(parent.context) - - override fun onBindViewHolder(holder: VH, position: Int) = throw UnsupportedOperationException() - - override fun onBindViewHolder(viewHolder: VH, position: Int, payload: List) { - onBind(viewHolder, data.getItem(position), listener, payload) - } -} - -private typealias AnyCreator = BindingViewHolder.Creator - -/** - * 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". - * @author OxygenCobalt - */ -abstract class MultiAdapter(private val listener: L) : - RecyclerView.Adapter() { - - /** The data that the adapter will source to bind viewholders. */ - abstract val data: BackingData - - /** - * 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, - payload: List - ) - - 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) = - throw UnsupportedOperationException() - - override fun onBindViewHolder( - viewHolder: RecyclerView.ViewHolder, - position: Int, - payload: List - ) { - onBind(viewHolder, data.getItem(position), listener, payload) - } -} - -/** - * 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. - * @author OxygenCobalt - */ -abstract class BindingViewHolder(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 { - 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. @@ -174,85 +42,49 @@ data class Header( get() = string.toLong() } -/** - * 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 { - /** Get an item at [position]. */ - abstract fun getItem(position: Int): T - /** Get the total length of the backing data. */ - abstract fun getItemCount(): Int +/** An interface for detecting if an item has been clicked once. */ +interface ItemClickListener { + /** Called when an item is clicked once. */ + fun onItemClick(item: Item) } -/** - * A list-backed [BackingData] that is modified synchronously. This is generally the recommended - * option for most adapters. - * @author OxygenCobalt - */ -class SyncBackingData(adapter: RecyclerView.Adapter<*>, diffCallback: DiffUtil.ItemCallback) : - BackingData() { - private var differ = SyncListDiffer(adapter, diffCallback) - /** The current list backing this adapter. */ - val currentList: List - get() = differ.currentList - - override fun getItem(position: Int): T = differ.currentList[position] - override fun getItemCount(): Int = differ.currentList.size - - /** Submit a list normally, doing a diff synchronously. Only use this for trivial changes. */ - fun submitList(newList: List) { - differ.currentList = newList - } - - /** - * Replace this list with a new list. This is useful for very large list diffs that would - * generally be too chaotic and slow to provide a good UX. - */ - fun replaceList(newList: List) { - if (newList == differ.currentList) { - return - } - - differ.currentList = emptyList() - differ.currentList = newList - } +/** 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) } /** * Like [AsyncListDiffer], but synchronous. This may seem like it would be inefficient, but in * practice Auxio's lists tend to be small enough to the point where this does not matter, and - * situations that would be inefficient are ruled out with [SyncBackingData.replaceList]. + * situations that would be inefficient are ruled out with [replaceList]. */ -private class SyncListDiffer( +class SyncListDiffer( adapter: RecyclerView.Adapter<*>, private val diffCallback: DiffUtil.ItemCallback ) { private val updateCallback = AdapterListUpdateCallback(adapter) - private var _currentList: List = emptyList() - var currentList: List - get() = _currentList - set(newList) { - if (newList === _currentList || newList.isEmpty() && _currentList.isEmpty()) { + var currentList: List = emptyList() + private set(newList) { + if (newList === currentList || newList.isEmpty() && currentList.isEmpty()) { return } if (newList.isEmpty()) { - val oldListSize = _currentList.size - _currentList = emptyList() + val oldListSize = currentList.size + field = emptyList() updateCallback.onRemoved(0, oldListSize) return } - if (_currentList.isEmpty()) { - _currentList = newList + if (currentList.isEmpty()) { + field = newList updateCallback.onInserted(0, newList.size) return } - val oldList = _currentList + val oldList = currentList val result = DiffUtil.calculateDiff( object : DiffUtil.Callback() { @@ -306,35 +138,26 @@ private class SyncListDiffer( } }) - _currentList = newList + field = newList result.dispatchUpdatesTo(updateCallback) } -} -/** - * 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. - * @author OxygenCobalt - */ -class AsyncBackingData( - adapter: RecyclerView.Adapter<*>, - diffCallback: DiffUtil.ItemCallback -) : BackingData() { - private var differ = AsyncListDiffer(adapter, diffCallback) - /** The current list backing this adapter. */ - val currentList: List - get() = differ.currentList - - override fun getItem(position: Int): T = differ.currentList[position] - override fun getItemCount(): Int = differ.currentList.size + /** Submit a list normally, doing a diff synchronously. Only use this for trivial changes. */ + fun submitList(newList: List) { + currentList = newList + } /** - * Submit a list for [AsyncListDiffer] to calculate. Any previous calls of [submitList] will be - * dropped. + * Replace this list with a new list. This is useful for very large list diffs that would + * generally be too chaotic and slow to provide a good UX. */ - fun submitList(newList: List, onDone: () -> Unit = {}) { - differ.submitList(newList, onDone) + fun replaceList(newList: List) { + if (newList == currentList) { + return + } + + currentList = emptyList() + currentList = newList } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/recycler/ViewHolders.kt b/app/src/main/java/org/oxycblt/auxio/ui/recycler/ViewHolders.kt index e1e0935fa..49feac7d1 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/recycler/ViewHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/recycler/ViewHolders.kt @@ -17,7 +17,8 @@ package org.oxycblt.auxio.ui.recycler -import android.content.Context +import android.view.View +import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemHeaderBinding @@ -36,8 +37,8 @@ import org.oxycblt.auxio.util.inflater * @author OxygenCobalt */ class SongViewHolder private constructor(private val binding: ItemSongBinding) : - BindingViewHolder(binding.root) { - override fun bind(item: Song, listener: MenuItemListener) { + RecyclerView.ViewHolder(binding.root) { + fun bind(item: Song, listener: MenuItemListener) { binding.songAlbumCover.bind(item) binding.songName.text = item.resolveName(binding.context) binding.songInfo.text = item.resolveIndividualArtistName(binding.context) @@ -50,14 +51,9 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) : } companion object { - val CREATOR = - object : Creator { - override val viewType: Int - get() = IntegerTable.ITEM_TYPE_SONG + const val VIEW_TYPE = IntegerTable.VIEW_TYPE_SONG - override fun create(context: Context) = - SongViewHolder(ItemSongBinding.inflate(context.inflater)) - } + fun new(parent: View) = SongViewHolder(ItemSongBinding.inflate(parent.context.inflater)) val DIFFER = object : SimpleItemCallback() { @@ -75,9 +71,9 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) : class AlbumViewHolder private constructor( private val binding: ItemParentBinding, -) : BindingViewHolder(binding.root) { +) : RecyclerView.ViewHolder(binding.root) { - override fun bind(item: Album, listener: MenuItemListener) { + fun bind(item: Album, listener: MenuItemListener) { binding.parentImage.bind(item) binding.parentName.text = item.resolveName(binding.context) binding.parentInfo.text = item.artist.resolveName(binding.context) @@ -90,14 +86,9 @@ private constructor( } companion object { - val CREATOR = - object : Creator { - override val viewType: Int - get() = IntegerTable.ITEM_TYPE_ALBUM + const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ALBUM - override fun create(context: Context) = - AlbumViewHolder(ItemParentBinding.inflate(context.inflater)) - } + fun new(parent: View) = AlbumViewHolder(ItemParentBinding.inflate(parent.context.inflater)) val DIFFER = object : SimpleItemCallback() { @@ -114,9 +105,9 @@ private constructor( * @author OxygenCobalt */ class ArtistViewHolder private constructor(private val binding: ItemParentBinding) : - BindingViewHolder(binding.root) { + RecyclerView.ViewHolder(binding.root) { - override fun bind(item: Artist, listener: MenuItemListener) { + fun bind(item: Artist, listener: MenuItemListener) { binding.parentImage.bind(item) binding.parentName.text = item.resolveName(binding.context) binding.parentInfo.text = @@ -133,14 +124,9 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin } companion object { - val CREATOR = - object : Creator { - override val viewType: Int - get() = IntegerTable.ITEM_TYPE_ARTIST + const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST - override fun create(context: Context) = - ArtistViewHolder(ItemParentBinding.inflate(context.inflater)) - } + fun new(parent: View) = ArtistViewHolder(ItemParentBinding.inflate(parent.context.inflater)) val DIFFER = object : SimpleItemCallback() { @@ -159,9 +145,9 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin class GenreViewHolder private constructor( private val binding: ItemParentBinding, -) : BindingViewHolder(binding.root) { +) : RecyclerView.ViewHolder(binding.root) { - override fun bind(item: Genre, listener: MenuItemListener) { + fun bind(item: Genre, listener: MenuItemListener) { binding.parentImage.bind(item) binding.parentName.text = item.resolveName(binding.context) binding.parentInfo.text = @@ -175,14 +161,9 @@ private constructor( } companion object { - val CREATOR = - object : Creator { - override val viewType: Int - get() = IntegerTable.ITEM_TYPE_GENRE + const val VIEW_TYPE = IntegerTable.VIEW_TYPE_GENRE - override fun create(context: Context) = - GenreViewHolder(ItemParentBinding.inflate(context.inflater)) - } + fun new(parent: View) = GenreViewHolder(ItemParentBinding.inflate(parent.context.inflater)) val DIFFER = object : SimpleItemCallback() { @@ -197,21 +178,16 @@ private constructor( * @author OxygenCobalt */ class HeaderViewHolder private constructor(private val binding: ItemHeaderBinding) : - BindingViewHolder(binding.root) { + RecyclerView.ViewHolder(binding.root) { - override fun bind(item: Header, listener: Unit) { + fun bind(item: Header) { binding.title.text = binding.context.getString(item.string) } companion object { - val CREATOR = - object : Creator { - override val viewType: Int - get() = IntegerTable.ITEM_TYPE_HEADER + const val VIEW_TYPE = IntegerTable.VIEW_TYPE_HEADER - override fun create(context: Context) = - HeaderViewHolder(ItemHeaderBinding.inflate(context.inflater)) - } + fun new(parent: View) = HeaderViewHolder(ItemHeaderBinding.inflate(parent.context.inflater)) val DIFFER = object : SimpleItemCallback
() { diff --git a/info/ARCHITECTURE.md b/info/ARCHITECTURE.md index f077e2c63..d01342ee9 100644 --- a/info/ARCHITECTURE.md +++ b/info/ARCHITECTURE.md @@ -78,10 +78,9 @@ own function, with the binding being obtained by calling `requireBinding`. At times it may be more appropriate to use a `View` instead of a fragment. This is okay as long as view-binding is still used. -Auxio uses `RecyclerView` for all list information. Due to the complexities of Auxio, the way one -defines an adapter differs quite heavily from the normal library. Generally, start with -`MonoAdapter` for a list with one type of data and `MultiAdapter` for lists with many types of data, -then follow the documentation to see how to fully implement the class. +Auxio uses `RecyclerView` for all list information. To manage some complexity, there are a few +conventions that are used when creating adapters. These can be seen in the `RecyclerFramework` +file and in adapter implementations. #### Object communication Auxio's codebase is mostly centered around 4 different types of code that communicates with