recycler: unwind abstractions

Unwind the RecyclerView abstractions.

The framework was far too suffocating and prevented the addition of
new changes. Remove it.
This commit is contained in:
Alexander Capehart 2022-09-01 18:24:59 -06:00
parent 3db68d47a6
commit 9d58076a0a
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
27 changed files with 524 additions and 718 deletions

View file

@ -20,35 +20,31 @@ package org.oxycblt.auxio
/** A table containing all unique integer codes that Auxio uses. */ /** A table containing all unique integer codes that Auxio uses. */
object IntegerTable { object IntegerTable {
/** SongViewHolder */ /** SongViewHolder */
const val ITEM_TYPE_SONG = 0xA000 const val VIEW_TYPE_SONG = 0xA000
/** AlbumViewHolder */ /** AlbumViewHolder */
const val ITEM_TYPE_ALBUM = 0xA001 const val VIEW_TYPE_ALBUM = 0xA001
/** ArtistViewHolder */ /** ArtistViewHolder */
const val ITEM_TYPE_ARTIST = 0xA002 const val VIEW_TYPE_ARTIST = 0xA002
/** GenreViewHolder */ /** GenreViewHolder */
const val ITEM_TYPE_GENRE = 0xA003 const val VIEW_TYPE_GENRE = 0xA003
/** HeaderViewHolder */ /** HeaderViewHolder */
const val ITEM_TYPE_HEADER = 0xA004 const val VIEW_TYPE_HEADER = 0xA004
/** SortHeaderViewHolder */ /** SortHeaderViewHolder */
const val ITEM_TYPE_SORT_HEADER = 0xA005 const val VIEW_TYPE_SORT_HEADER = 0xA005
/** AlbumDetailViewHolder */ /** AlbumDetailViewHolder */
const val ITEM_TYPE_ALBUM_DETAIL = 0xA006 const val VIEW_TYPE_ALBUM_DETAIL = 0xA006
/** AlbumSongViewHolder */ /** AlbumSongViewHolder */
const val ITEM_TYPE_ALBUM_SONG = 0xA007 const val VIEW_TYPE_ALBUM_SONG = 0xA007
/** ArtistDetailViewHolder */ /** ArtistDetailViewHolder */
const val ITEM_TYPE_ARTIST_DETAIL = 0xA008 const val VIEW_TYPE_ARTIST_DETAIL = 0xA008
/** ArtistAlbumViewHolder */ /** ArtistAlbumViewHolder */
const val ITEM_TYPE_ARTIST_ALBUM = 0xA009 const val VIEW_TYPE_ARTIST_ALBUM = 0xA009
/** ArtistSongViewHolder */ /** ArtistSongViewHolder */
const val ITEM_TYPE_ARTIST_SONG = 0xA00A const val VIEW_TYPE_ARTIST_SONG = 0xA00A
/** GenreDetailViewHolder */ /** GenreDetailViewHolder */
const val ITEM_TYPE_GENRE_DETAIL = 0xA00B const val VIEW_TYPE_GENRE_DETAIL = 0xA00B
/** DiscHeaderViewHolder */ /** DiscHeaderViewHolder */
const val ITEM_TYPE_DISC_HEADER = 0xA00C const val VIEW_TYPE_DISC_HEADER = 0xA00C
/** QueueSongViewHolder */
const val ITEM_TYPE_QUEUE_SONG = 0xA00D
/** "Music playback" notification code */ /** "Music playback" notification code */
const val PLAYBACK_NOTIFICATION_CODE = 0xA0A0 const val PLAYBACK_NOTIFICATION_CODE = 0xA0A0

View file

@ -88,7 +88,7 @@ class AlbumDetailFragment :
binding.detailRecycler.apply { binding.detailRecycler.apply {
adapter = detailAdapter adapter = detailAdapter
setSpanSizeLookup { pos -> setSpanSizeLookup { pos ->
val item = detailAdapter.data.getItem(pos) val item = detailModel.albumData.value[pos]
item is Album || item is Header || item is SortHeader item is Album || item is Header || item is SortHeader
} }
} }
@ -96,7 +96,7 @@ class AlbumDetailFragment :
// -- VIEWMODEL SETUP --- // -- VIEWMODEL SETUP ---
collectImmediately(detailModel.currentAlbum, ::handleItemChange) collectImmediately(detailModel.currentAlbum, ::handleItemChange)
collectImmediately(detailModel.albumData, detailAdapter.data::submitList) collectImmediately(detailModel.albumData, detailAdapter::submitList)
collectImmediately(playbackModel.song, playbackModel.parent, ::updatePlayback) collectImmediately(playbackModel.song, playbackModel.parent, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation) collect(navModel.exploreNavigationItem, ::handleNavigation)
} }
@ -227,7 +227,7 @@ class AlbumDetailFragment :
/** Scroll to an song using its [id]. */ /** Scroll to an song using its [id]. */
private fun scrollToItem(id: Long) { private fun scrollToItem(id: Long) {
// Calculate where the item for the currently played song is // 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) { if (pos != -1) {
val binding = requireBinding() val binding = requireBinding()

View file

@ -83,7 +83,7 @@ class ArtistDetailFragment :
binding.detailRecycler.apply { binding.detailRecycler.apply {
adapter = detailAdapter adapter = detailAdapter
setSpanSizeLookup { pos -> setSpanSizeLookup { pos ->
val item = detailAdapter.data.getItem(pos) val item = detailModel.artistData.value[pos]
item is Artist || item is Header || item is SortHeader item is Artist || item is Header || item is SortHeader
} }
} }
@ -91,7 +91,7 @@ class ArtistDetailFragment :
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
collectImmediately(detailModel.currentArtist, ::handleItemChange) collectImmediately(detailModel.currentArtist, ::handleItemChange)
collectImmediately(detailModel.artistData, detailAdapter.data::submitList) collectImmediately(detailModel.artistData, detailAdapter::submitList)
collectImmediately(playbackModel.song, playbackModel.parent, ::updatePlayback) collectImmediately(playbackModel.song, playbackModel.parent, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation) collect(navModel.exploreNavigationItem, ::handleNavigation)
} }

View file

@ -84,7 +84,7 @@ class GenreDetailFragment :
binding.detailRecycler.apply { binding.detailRecycler.apply {
adapter = detailAdapter adapter = detailAdapter
setSpanSizeLookup { pos -> setSpanSizeLookup { pos ->
val item = detailAdapter.data.getItem(pos) val item = detailModel.albumData.value[pos]
item is Genre || item is Header || item is SortHeader item is Genre || item is Header || item is SortHeader
} }
} }
@ -92,7 +92,7 @@ class GenreDetailFragment :
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
collectImmediately(detailModel.currentGenre, ::handleItemChange) collectImmediately(detailModel.currentGenre, ::handleItemChange)
collectImmediately(detailModel.genreData, detailAdapter.data::submitList) collectImmediately(detailModel.genreData, detailAdapter::submitList)
collectImmediately(playbackModel.song, playbackModel.parent, ::updatePlayback) collectImmediately(playbackModel.song, playbackModel.parent, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation) collect(navModel.exploreNavigationItem, ::handleNavigation)
} }

View file

@ -17,7 +17,8 @@
package org.oxycblt.auxio.detail.recycler 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.core.view.isInvisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable 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.detail.DiscHeader
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Song 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.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback 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. * An adapter for displaying [Album] information and it's children.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class AlbumDetailAdapter(listener: Listener) : class AlbumDetailAdapter(private val listener: Listener) :
DetailAdapter<AlbumDetailAdapter.Listener>(listener, DIFFER) { DetailAdapter<AlbumDetailAdapter.Listener>(listener, DIFFER) {
private var currentSong: Song? = null private var currentSong: Song? = null
override fun getCreatorFromItem(item: Item) = override fun getItemViewType(position: Int) =
super.getCreatorFromItem(item) when (differ.currentList[position]) {
?: when (item) { is Album -> AlbumDetailViewHolder.VIEW_TYPE
is Album -> AlbumDetailViewHolder.CREATOR is DiscHeader -> DiscHeaderViewHolder.VIEW_TYPE
is DiscHeader -> DiscHeaderViewHolder.CREATOR is Song -> AlbumSongViewHolder.VIEW_TYPE
is Song -> AlbumSongViewHolder.CREATOR else -> super.getItemViewType(position)
else -> null }
}
override fun getCreatorFromViewType(viewType: Int) = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
super.getCreatorFromViewType(viewType) when (viewType) {
?: when (viewType) { AlbumDetailViewHolder.VIEW_TYPE -> AlbumDetailViewHolder.new(parent)
AlbumDetailViewHolder.CREATOR.viewType -> AlbumDetailViewHolder.CREATOR DiscHeaderViewHolder.VIEW_TYPE -> DiscHeaderViewHolder.new(parent)
DiscHeaderViewHolder.CREATOR.viewType -> DiscHeaderViewHolder.CREATOR AlbumSongViewHolder.VIEW_TYPE -> AlbumSongViewHolder.new(parent)
AlbumSongViewHolder.CREATOR.viewType -> AlbumSongViewHolder.CREATOR else -> super.onCreateViewHolder(parent, viewType)
else -> null }
}
override fun onBind( override fun onBindViewHolder(
viewHolder: RecyclerView.ViewHolder, holder: RecyclerView.ViewHolder,
item: Item, position: Int,
listener: Listener,
payload: List<Any> payload: List<Any>
) { ) {
super.onBind(viewHolder, item, listener, payload)
if (payload.isEmpty()) { if (payload.isEmpty()) {
when (item) { when (val item = differ.currentList[position]) {
is Album -> (viewHolder as AlbumDetailViewHolder).bind(item, listener) is Album -> (holder as AlbumDetailViewHolder).bind(item, listener)
is DiscHeader -> (viewHolder as DiscHeaderViewHolder).bind(item, Unit) is DiscHeader -> (holder as DiscHeaderViewHolder).bind(item)
is Song -> (viewHolder as AlbumSongViewHolder).bind(item, listener) is Song -> (holder as AlbumSongViewHolder).bind(item, listener)
} }
} }
super.onBindViewHolder(holder, position, payload)
} }
override fun shouldHighlightViewHolder(item: Item) = item is Song && item.id == currentSong?.id override fun 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) : private class AlbumDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
BindingViewHolder<Album, AlbumDetailAdapter.Listener>(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.detailCover.bind(item)
binding.detailType.text = binding.context.getString(item.releaseType.stringRes) binding.detailType.text = binding.context.getString(item.releaseType.stringRes)
@ -141,14 +139,10 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
} }
companion object { companion object {
val CREATOR = const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ALBUM_DETAIL
object : Creator<AlbumDetailViewHolder> {
override val viewType: Int
get() = IntegerTable.ITEM_TYPE_ALBUM_DETAIL
override fun create(context: Context) = fun new(parent: View) =
AlbumDetailViewHolder(ItemDetailBinding.inflate(context.inflater)) AlbumDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater))
}
val DIFFER = val DIFFER =
object : SimpleItemCallback<Album>() { object : SimpleItemCallback<Album>() {
@ -164,21 +158,17 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
} }
class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) : class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
BindingViewHolder<DiscHeader, Unit>(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) binding.discNo.text = binding.context.getString(R.string.fmt_disc_no, item.disc)
} }
companion object { companion object {
val CREATOR = const val VIEW_TYPE = IntegerTable.VIEW_TYPE_DISC_HEADER
object : Creator<DiscHeaderViewHolder> {
override val viewType: Int
get() = IntegerTable.ITEM_TYPE_DISC_HEADER
override fun create(context: Context) = fun new(parent: View) =
DiscHeaderViewHolder(ItemDiscHeaderBinding.inflate(context.inflater)) DiscHeaderViewHolder(ItemDiscHeaderBinding.inflate(parent.context.inflater))
}
val DIFFER = val DIFFER =
object : SimpleItemCallback<DiscHeader>() { object : SimpleItemCallback<DiscHeader>() {
@ -189,8 +179,8 @@ class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
} }
private class AlbumSongViewHolder private constructor(private val binding: ItemAlbumSongBinding) : private class AlbumSongViewHolder private constructor(private val binding: ItemAlbumSongBinding) :
BindingViewHolder<Song, MenuItemListener>(binding.root) { RecyclerView.ViewHolder(binding.root) {
override fun bind(item: Song, listener: MenuItemListener) { fun bind(item: Song, listener: MenuItemListener) {
// Hide the track number view if the song does not have a track. // Hide the track number view if the song does not have a track.
if (item.track != null) { if (item.track != null) {
binding.songTrack.apply { binding.songTrack.apply {
@ -218,14 +208,10 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
} }
companion object { companion object {
val CREATOR = const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ALBUM_SONG
object : Creator<AlbumSongViewHolder> {
override val viewType: Int
get() = IntegerTable.ITEM_TYPE_ALBUM_SONG
override fun create(context: Context) = fun new(parent: View) =
AlbumSongViewHolder(ItemAlbumSongBinding.inflate(context.inflater)) AlbumSongViewHolder(ItemAlbumSongBinding.inflate(parent.context.inflater))
}
val DIFFER = val DIFFER =
object : SimpleItemCallback<Song>() { object : SimpleItemCallback<Song>() {

View file

@ -17,7 +17,8 @@
package org.oxycblt.auxio.detail.recycler package org.oxycblt.auxio.detail.recycler
import android.content.Context import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R 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.Song
import org.oxycblt.auxio.music.resolveYear import org.oxycblt.auxio.music.resolveYear
import org.oxycblt.auxio.ui.recycler.ArtistViewHolder 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.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback 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. * one actually contains both album information and song information.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class ArtistDetailAdapter(listener: Listener) : class ArtistDetailAdapter(private val listener: Listener) :
DetailAdapter<DetailAdapter.Listener>(listener, DIFFER) { DetailAdapter<DetailAdapter.Listener>(listener, DIFFER) {
private var currentAlbum: Album? = null private var currentAlbum: Album? = null
private var currentSong: Song? = null private var currentSong: Song? = null
override fun getCreatorFromItem(item: Item) = override fun getItemViewType(position: Int) =
super.getCreatorFromItem(item) when (differ.currentList[position]) {
?: when (item) { is Artist -> ArtistDetailViewHolder.VIEW_TYPE
is Artist -> ArtistDetailViewHolder.CREATOR is Album -> ArtistAlbumViewHolder.VIEW_TYPE
is Album -> ArtistAlbumViewHolder.CREATOR is Song -> ArtistSongViewHolder.VIEW_TYPE
is Song -> ArtistSongViewHolder.CREATOR else -> super.getItemViewType(position)
else -> null }
}
override fun getCreatorFromViewType(viewType: Int) = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
super.getCreatorFromViewType(viewType) when (viewType) {
?: when (viewType) { ArtistDetailViewHolder.VIEW_TYPE -> ArtistDetailViewHolder.new(parent)
ArtistDetailViewHolder.CREATOR.viewType -> ArtistDetailViewHolder.CREATOR ArtistAlbumViewHolder.VIEW_TYPE -> ArtistAlbumViewHolder.new(parent)
ArtistAlbumViewHolder.CREATOR.viewType -> ArtistAlbumViewHolder.CREATOR ArtistSongViewHolder.VIEW_TYPE -> ArtistSongViewHolder.new(parent)
ArtistSongViewHolder.CREATOR.viewType -> ArtistSongViewHolder.CREATOR else -> super.onCreateViewHolder(parent, viewType)
else -> null }
}
override fun onBind( override fun onBindViewHolder(
viewHolder: RecyclerView.ViewHolder, holder: RecyclerView.ViewHolder,
item: Item, position: Int,
listener: Listener,
payload: List<Any> payload: List<Any>
) { ) {
super.onBind(viewHolder, item, listener, payload)
if (payload.isEmpty()) { if (payload.isEmpty()) {
when (item) { when (val item = differ.currentList[position]) {
is Artist -> (viewHolder as ArtistDetailViewHolder).bind(item, listener) is Artist -> (holder as ArtistDetailViewHolder).bind(item, listener)
is Album -> (viewHolder as ArtistAlbumViewHolder).bind(item, listener) is Album -> (holder as ArtistAlbumViewHolder).bind(item, listener)
is Song -> (viewHolder as ArtistSongViewHolder).bind(item, listener) is Song -> (holder as ArtistSongViewHolder).bind(item, listener)
else -> {}
} }
} }
super.onBindViewHolder(holder, position, payload)
} }
override fun shouldHighlightViewHolder(item: Item) = override fun shouldHighlightViewHolder(item: Item) =
@ -119,9 +116,9 @@ class ArtistDetailAdapter(listener: Listener) :
} }
private class ArtistDetailViewHolder private constructor(private val binding: ItemDetailBinding) : private class ArtistDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
BindingViewHolder<Artist, DetailAdapter.Listener>(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.detailCover.bind(item)
binding.detailType.text = binding.context.getString(R.string.lbl_artist) binding.detailType.text = binding.context.getString(R.string.lbl_artist)
binding.detailName.text = item.resolveName(binding.context) binding.detailName.text = item.resolveName(binding.context)
@ -147,14 +144,10 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
} }
companion object { companion object {
val CREATOR = const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST_DETAIL
object : Creator<ArtistDetailViewHolder> {
override val viewType: Int
get() = IntegerTable.ITEM_TYPE_ARTIST_DETAIL
override fun create(context: Context) = fun new(parent: View) =
ArtistDetailViewHolder(ItemDetailBinding.inflate(context.inflater)) ArtistDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater))
}
val DIFFER = ArtistViewHolder.DIFFER val DIFFER = ArtistViewHolder.DIFFER
} }
@ -163,8 +156,8 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
private class ArtistAlbumViewHolder private class ArtistAlbumViewHolder
private constructor( private constructor(
private val binding: ItemParentBinding, private val binding: ItemParentBinding,
) : BindingViewHolder<Album, MenuItemListener>(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
override fun bind(item: Album, listener: MenuItemListener) { fun bind(item: Album, listener: MenuItemListener) {
binding.parentImage.bind(item) binding.parentImage.bind(item)
binding.parentName.text = item.resolveName(binding.context) binding.parentName.text = item.resolveName(binding.context)
binding.parentInfo.text = item.date.resolveYear(binding.context) binding.parentInfo.text = item.date.resolveYear(binding.context)
@ -177,14 +170,10 @@ private constructor(
} }
companion object { companion object {
val CREATOR = const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST_ALBUM
object : Creator<ArtistAlbumViewHolder> {
override val viewType: Int
get() = IntegerTable.ITEM_TYPE_ARTIST_ALBUM
override fun create(context: Context) = fun new(parent: View) =
ArtistAlbumViewHolder(ItemParentBinding.inflate(context.inflater)) ArtistAlbumViewHolder(ItemParentBinding.inflate(parent.context.inflater))
}
val DIFFER = val DIFFER =
object : SimpleItemCallback<Album>() { object : SimpleItemCallback<Album>() {
@ -197,8 +186,8 @@ private constructor(
private class ArtistSongViewHolder private class ArtistSongViewHolder
private constructor( private constructor(
private val binding: ItemSongBinding, private val binding: ItemSongBinding,
) : BindingViewHolder<Song, MenuItemListener>(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
override fun bind(item: Song, listener: MenuItemListener) { fun bind(item: Song, listener: MenuItemListener) {
binding.songAlbumCover.bind(item) binding.songAlbumCover.bind(item)
binding.songName.text = item.resolveName(binding.context) binding.songName.text = item.resolveName(binding.context)
binding.songInfo.text = item.album.resolveName(binding.context) binding.songInfo.text = item.album.resolveName(binding.context)
@ -211,14 +200,10 @@ private constructor(
} }
companion object { companion object {
val CREATOR = const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST_SONG
object : Creator<ArtistSongViewHolder> {
override val viewType: Int
get() = IntegerTable.ITEM_TYPE_ARTIST_SONG
override fun create(context: Context) = fun new(parent: View) =
ArtistSongViewHolder(ItemSongBinding.inflate(context.inflater)) ArtistSongViewHolder(ItemSongBinding.inflate(parent.context.inflater))
}
val DIFFER = val DIFFER =
object : SimpleItemCallback<Song>() { object : SimpleItemCallback<Song>() {

View file

@ -17,35 +17,71 @@
package org.oxycblt.auxio.detail.recycler package org.oxycblt.auxio.detail.recycler
import android.content.Context
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.TooltipCompat import androidx.appcompat.widget.TooltipCompat
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.databinding.ItemSortHeaderBinding import org.oxycblt.auxio.databinding.ItemSortHeaderBinding
import org.oxycblt.auxio.detail.SortHeader 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.Header
import org.oxycblt.auxio.ui.recycler.HeaderViewHolder import org.oxycblt.auxio.ui.recycler.HeaderViewHolder
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener 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.SimpleItemCallback
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
abstract class DetailAdapter<L : DetailAdapter.Listener>( abstract class DetailAdapter<L : DetailAdapter.Listener>(
listener: L, private val listener: L,
diffCallback: DiffUtil.ItemCallback<Item> diffCallback: DiffUtil.ItemCallback<Item>
) : MultiAdapter<L>(listener) { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
abstract fun shouldHighlightViewHolder(item: Item): Boolean @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<Any>
) {
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 <reified T : Item> highlightImpl(oldItem: T?, newItem: T?) { protected inline fun <reified T : Item> highlightImpl(oldItem: T?, newItem: T?) {
if (oldItem != null) { 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) { if (pos > -1) {
notifyItemChanged(pos, PAYLOAD_HIGHLIGHT_CHANGED) notifyItemChanged(pos, PAYLOAD_HIGHLIGHT_CHANGED)
@ -55,7 +91,7 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
} }
if (newItem != null) { 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) { if (pos > -1) {
notifyItemChanged(pos, PAYLOAD_HIGHLIGHT_CHANGED) notifyItemChanged(pos, PAYLOAD_HIGHLIGHT_CHANGED)
@ -65,36 +101,8 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
} }
} }
@Suppress("LeakingThis") override val data = AsyncBackingData(this, diffCallback) fun submitList(list: List<Item>) {
differ.submitList(list)
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<Any>
) {
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)
} }
companion object { companion object {
@ -127,8 +135,8 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
} }
class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) : class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
BindingViewHolder<SortHeader, DetailAdapter.Listener>(binding.root) { RecyclerView.ViewHolder(binding.root) {
override fun bind(item: SortHeader, listener: DetailAdapter.Listener) { fun bind(item: SortHeader, listener: DetailAdapter.Listener) {
binding.headerTitle.text = binding.context.getString(item.string) binding.headerTitle.text = binding.context.getString(item.string)
binding.headerButton.apply { binding.headerButton.apply {
TooltipCompat.setTooltipText(this, contentDescription) TooltipCompat.setTooltipText(this, contentDescription)
@ -137,14 +145,10 @@ class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
} }
companion object { companion object {
val CREATOR = const val VIEW_TYPE = IntegerTable.VIEW_TYPE_SORT_HEADER
object : Creator<SortHeaderViewHolder> {
override val viewType: Int
get() = IntegerTable.ITEM_TYPE_SORT_HEADER
override fun create(context: Context) = fun new(parent: View) =
SortHeaderViewHolder(ItemSortHeaderBinding.inflate(context.inflater)) SortHeaderViewHolder(ItemSortHeaderBinding.inflate(parent.context.inflater))
}
val DIFFER = val DIFFER =
object : SimpleItemCallback<SortHeader>() { object : SimpleItemCallback<SortHeader>() {

View file

@ -17,14 +17,14 @@
package org.oxycblt.auxio.detail.recycler package org.oxycblt.auxio.detail.recycler
import android.content.Context import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemDetailBinding import org.oxycblt.auxio.databinding.ItemDetailBinding
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song 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.Item
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
import org.oxycblt.auxio.ui.recycler.SongViewHolder 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. * An adapter for displaying genre information and it's children.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class GenreDetailAdapter(listener: Listener) : class GenreDetailAdapter(private val listener: Listener) :
DetailAdapter<DetailAdapter.Listener>(listener, DIFFER) { DetailAdapter<DetailAdapter.Listener>(listener, DIFFER) {
private var currentSong: Song? = null private var currentSong: Song? = null
override fun getCreatorFromItem(item: Item) = override fun getItemViewType(position: Int) =
super.getCreatorFromItem(item) when (differ.currentList[position]) {
?: when (item) { is Genre -> GenreDetailViewHolder.VIEW_TYPE
is Genre -> GenreDetailViewHolder.CREATOR is Song -> SongViewHolder.VIEW_TYPE
is Song -> SongViewHolder.CREATOR else -> super.getItemViewType(position)
else -> null }
}
override fun getCreatorFromViewType(viewType: Int) = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
super.getCreatorFromViewType(viewType) when (viewType) {
?: when (viewType) { GenreDetailViewHolder.VIEW_TYPE -> GenreDetailViewHolder.new(parent)
GenreDetailViewHolder.CREATOR.viewType -> GenreDetailViewHolder.CREATOR SongViewHolder.VIEW_TYPE -> SongViewHolder.new(parent)
SongViewHolder.CREATOR.viewType -> SongViewHolder.CREATOR else -> super.onCreateViewHolder(parent, viewType)
else -> null }
}
override fun onBind( override fun onBindViewHolder(
viewHolder: RecyclerView.ViewHolder, holder: RecyclerView.ViewHolder,
item: Item, position: Int,
listener: Listener,
payload: List<Any> payload: List<Any>
) { ) {
super.onBind(viewHolder, item, listener, payload)
if (payload.isEmpty()) { if (payload.isEmpty()) {
when (item) { when (val item = differ.currentList[position]) {
is Genre -> (viewHolder as GenreDetailViewHolder).bind(item, listener) is Genre -> (holder as GenreDetailViewHolder).bind(item, listener)
is Song -> (viewHolder as SongViewHolder).bind(item, listener) is Song -> (holder as SongViewHolder).bind(item, listener)
else -> {}
} }
} }
super.onBindViewHolder(holder, position, payload)
} }
override fun shouldHighlightViewHolder(item: Item) = item is Song && item.id == currentSong?.id 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) : private class GenreDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
BindingViewHolder<Genre, DetailAdapter.Listener>(binding.root) { RecyclerView.ViewHolder(binding.root) {
override fun bind(item: Genre, listener: DetailAdapter.Listener) { fun bind(item: Genre, listener: DetailAdapter.Listener) {
binding.detailCover.bind(item) binding.detailCover.bind(item)
binding.detailType.text = binding.context.getString(R.string.lbl_genre) binding.detailType.text = binding.context.getString(R.string.lbl_genre)
binding.detailName.text = item.resolveName(binding.context) binding.detailName.text = item.resolveName(binding.context)
@ -112,14 +109,10 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
} }
companion object { companion object {
val CREATOR = const val VIEW_TYPE = IntegerTable.VIEW_TYPE_GENRE_DETAIL
object : Creator<GenreDetailViewHolder> {
override val viewType: Int
get() = IntegerTable.ITEM_TYPE_GENRE_DETAIL
override fun create(context: Context) = fun new(parent: View) =
GenreDetailViewHolder(ItemDetailBinding.inflate(context.inflater)) GenreDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater))
}
val DIFFER = val DIFFER =
object : SimpleItemCallback<Genre>() { object : SimpleItemCallback<Genre>() {

View file

@ -20,6 +20,8 @@ package org.oxycblt.auxio.home.list
import android.os.Bundle import android.os.Bundle
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import java.util.* import java.util.*
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding 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.AlbumViewHolder
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.MonoAdapter import org.oxycblt.auxio.ui.recycler.SyncListDiffer
import org.oxycblt.auxio.ui.recycler.SyncBackingData
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.formatDurationMs import org.oxycblt.auxio.util.formatDurationMs
import org.oxycblt.auxio.util.logEOrThrow import org.oxycblt.auxio.util.logEOrThrow
@ -54,7 +55,7 @@ class AlbumListFragment : HomeListFragment<Album>() {
adapter = homeAdapter adapter = homeAdapter
} }
collectImmediately(homeModel.albums, homeAdapter.data::replaceList) collectImmediately(homeModel.albums, homeAdapter::replaceList)
} }
override fun getPopup(pos: Int): String? { override fun getPopup(pos: Int): String? {
@ -107,9 +108,21 @@ class AlbumListFragment : HomeListFragment<Album>() {
} }
} }
class AlbumAdapter(listener: MenuItemListener) : private class AlbumAdapter(private val listener: MenuItemListener) :
MonoAdapter<Album, MenuItemListener, AlbumViewHolder>(listener) { RecyclerView.Adapter<AlbumViewHolder>() {
override val data = SyncBackingData(this, AlbumViewHolder.DIFFER) private val differ = SyncListDiffer(this, AlbumViewHolder.DIFFER)
override val creator = AlbumViewHolder.CREATOR
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<Album>) {
differ.replaceList(newList)
}
} }
} }

View file

@ -19,6 +19,8 @@ package org.oxycblt.auxio.home.list
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.Artist 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.ArtistViewHolder
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.MonoAdapter import org.oxycblt.auxio.ui.recycler.SyncListDiffer
import org.oxycblt.auxio.ui.recycler.SyncBackingData
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.formatDurationMs import org.oxycblt.auxio.util.formatDurationMs
import org.oxycblt.auxio.util.logEOrThrow import org.oxycblt.auxio.util.logEOrThrow
@ -49,7 +50,7 @@ class ArtistListFragment : HomeListFragment<Artist>() {
adapter = homeAdapter adapter = homeAdapter
} }
collectImmediately(homeModel.artists, homeAdapter.data::replaceList) collectImmediately(homeModel.artists, homeAdapter::replaceList)
} }
override fun getPopup(pos: Int): String? { override fun getPopup(pos: Int): String? {
@ -83,9 +84,21 @@ class ArtistListFragment : HomeListFragment<Artist>() {
} }
} }
class ArtistAdapter(listener: MenuItemListener) : private class ArtistAdapter(private val listener: MenuItemListener) :
MonoAdapter<Artist, MenuItemListener, ArtistViewHolder>(listener) { RecyclerView.Adapter<ArtistViewHolder>() {
override val data = SyncBackingData(this, ArtistViewHolder.DIFFER) private val differ = SyncListDiffer(this, ArtistViewHolder.DIFFER)
override val creator = ArtistViewHolder.CREATOR
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<Artist>) {
differ.replaceList(newList)
}
} }
} }

View file

@ -19,6 +19,8 @@ package org.oxycblt.auxio.home.list
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.Genre 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.GenreViewHolder
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.MonoAdapter import org.oxycblt.auxio.ui.recycler.SyncListDiffer
import org.oxycblt.auxio.ui.recycler.SyncBackingData
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.formatDurationMs import org.oxycblt.auxio.util.formatDurationMs
import org.oxycblt.auxio.util.logEOrThrow import org.oxycblt.auxio.util.logEOrThrow
@ -49,7 +50,7 @@ class GenreListFragment : HomeListFragment<Genre>() {
adapter = homeAdapter adapter = homeAdapter
} }
collectImmediately(homeModel.genres, homeAdapter.data::replaceList) collectImmediately(homeModel.genres, homeAdapter::replaceList)
} }
override fun getPopup(pos: Int): String? { override fun getPopup(pos: Int): String? {
@ -83,9 +84,21 @@ class GenreListFragment : HomeListFragment<Genre>() {
} }
} }
class GenreAdapter(listener: MenuItemListener) : private class GenreAdapter(private val listener: MenuItemListener) :
MonoAdapter<Genre, MenuItemListener, GenreViewHolder>(listener) { RecyclerView.Adapter<GenreViewHolder>() {
override val data = SyncBackingData(this, GenreViewHolder.DIFFER) private val differ = SyncListDiffer(this, GenreViewHolder.DIFFER)
override val creator = GenreViewHolder.CREATOR
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<Genre>) {
differ.replaceList(newList)
}
} }
} }

View file

@ -20,6 +20,8 @@ package org.oxycblt.auxio.home.list
import android.os.Bundle import android.os.Bundle
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import java.util.Formatter import java.util.Formatter
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding 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.Sort
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener 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.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.collectImmediately
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.formatDurationMs import org.oxycblt.auxio.util.formatDurationMs
@ -43,7 +44,7 @@ import org.oxycblt.auxio.util.secsToMs
* @author * @author
*/ */
class SongListFragment : HomeListFragment<Song>() { class SongListFragment : HomeListFragment<Song>() {
private val homeAdapter = SongsAdapter(this) private val homeAdapter = SongAdapter(this)
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
private val formatterSb = StringBuilder(50) private val formatterSb = StringBuilder(50)
private val formatter = Formatter(formatterSb) private val formatter = Formatter(formatterSb)
@ -56,7 +57,7 @@ class SongListFragment : HomeListFragment<Song>() {
adapter = homeAdapter adapter = homeAdapter
} }
collectImmediately(homeModel.songs, homeAdapter.data::replaceList) collectImmediately(homeModel.songs, homeAdapter::replaceList)
} }
override fun getPopup(pos: Int): String? { override fun getPopup(pos: Int): String? {
@ -111,9 +112,21 @@ class SongListFragment : HomeListFragment<Song>() {
} }
} }
inner class SongsAdapter(listener: MenuItemListener) : private class SongAdapter(private val listener: MenuItemListener) :
MonoAdapter<Song, MenuItemListener, SongViewHolder>(listener) { RecyclerView.Adapter<SongViewHolder>() {
override val data = SyncBackingData(this, SongViewHolder.DIFFER) private val differ = SyncListDiffer(this, SongViewHolder.DIFFER)
override val creator = SongViewHolder.CREATOR
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<Song>) {
differ.replaceList(newList)
}
} }
} }

View file

@ -18,63 +18,65 @@
package org.oxycblt.auxio.home.tabs package org.oxycblt.auxio.home.tabs
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemTabBinding import org.oxycblt.auxio.databinding.ItemTabBinding
import org.oxycblt.auxio.ui.DisplayMode 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 import org.oxycblt.auxio.util.inflater
class TabAdapter(listener: Listener) : class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewHolder>() {
MonoAdapter<Tab, TabAdapter.Listener, TabViewHolder>(listener) { var tabs = arrayOf<Tab>()
override val data = TabData(this) private set
override val creator = TabViewHolder.CREATOR
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<Tab>) {
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 { interface Listener {
fun onVisibilityToggled(displayMode: DisplayMode) fun onVisibilityToggled(displayMode: DisplayMode)
fun onPickUpTab(viewHolder: RecyclerView.ViewHolder) fun onPickUpTab(viewHolder: RecyclerView.ViewHolder)
} }
class TabData(private val adapter: RecyclerView.Adapter<*>) : BackingData<Tab>() {
var tabs = arrayOf<Tab>()
private set
override fun getItem(position: Int) = tabs[position]
override fun getItemCount() = tabs.size
@Suppress("NotifyDatasetChanged")
fun submitTabs(newTabs: Array<Tab>) {
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 { companion object {
val PAYLOAD_TAB_CHANGED = Any() val PAYLOAD_TAB_CHANGED = Any()
} }
} }
class TabViewHolder private constructor(private val binding: ItemTabBinding) : class TabViewHolder private constructor(private val binding: ItemTabBinding) :
BindingViewHolder<Tab, TabAdapter.Listener>(binding.root) { RecyclerView.ViewHolder(binding.root) {
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun bind(item: Tab, listener: TabAdapter.Listener) { fun bind(item: Tab, listener: TabAdapter.Listener) {
binding.root.apply { setOnClickListener { listener.onVisibilityToggled(item.mode) } } // 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 { binding.tabIcon.apply {
setText(item.mode.string) setText(item.mode.string)
@ -92,13 +94,6 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) :
} }
companion object { companion object {
val CREATOR = fun new(parent: View) = TabViewHolder(ItemTabBinding.inflate(parent.context.inflater))
object : Creator<TabViewHolder> {
override val viewType: Int
get() = throw UnsupportedOperationException()
override fun create(context: Context) =
TabViewHolder(ItemTabBinding.inflate(context.inflater))
}
} }
} }

View file

@ -49,7 +49,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
.setTitle(R.string.set_lib_tabs) .setTitle(R.string.set_lib_tabs)
.setPositiveButton(R.string.lbl_ok) { _, _ -> .setPositiveButton(R.string.lbl_ok) { _, _ ->
logD("Committing tab changes") logD("Committing tab changes")
settings.libTabs = tabAdapter.data.tabs settings.libTabs = tabAdapter.tabs
} }
.setNegativeButton(R.string.lbl_cancel, null) .setNegativeButton(R.string.lbl_cancel, null)
} }
@ -58,9 +58,9 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
val savedTabs = findSavedTabState(savedInstanceState) val savedTabs = findSavedTabState(savedInstanceState)
if (savedTabs != null) { if (savedTabs != null) {
logD("Found saved tab state") logD("Found saved tab state")
tabAdapter.data.submitTabs(savedTabs) tabAdapter.submitTabs(savedTabs)
} else { } else {
tabAdapter.data.submitTabs(settings.libTabs) tabAdapter.submitTabs(settings.libTabs)
} }
binding.tabRecycler.apply { binding.tabRecycler.apply {
@ -71,7 +71,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) 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) { override fun onDestroyBinding(binding: DialogTabsBinding) {
@ -83,10 +83,10 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
// Tab viewholders bind with the initial tab state, which will drift from the actual // 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 // state of the tabs over editing. So, this callback simply provides the displayMode
// for us to locate within the data and then update. // 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) { if (index > -1) {
val tab = tabAdapter.data.tabs[index] val tab = tabAdapter.tabs[index]
tabAdapter.data.setTab( tabAdapter.setTab(
index, index,
when (tab) { when (tab) {
is Tab.Visible -> Tab.Invisible(tab.mode) is Tab.Visible -> Tab.Invisible(tab.mode)
@ -95,7 +95,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
} }
(requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = (requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled =
tabAdapter.data.tabs.filterIsInstance<Tab.Visible>().isNotEmpty() tabAdapter.tabs.filterIsInstance<Tab.Visible>().isNotEmpty()
} }
override fun onPickUpTab(viewHolder: RecyclerView.ViewHolder) { override fun onPickUpTab(viewHolder: RecyclerView.ViewHolder) {

View file

@ -56,7 +56,7 @@ class TabDragCallback(private val adapter: TabAdapter) : ItemTouchHelper.Callbac
viewHolder: RecyclerView.ViewHolder, viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder target: RecyclerView.ViewHolder
): Boolean { ): Boolean {
adapter.data.moveItems(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition) adapter.moveItems(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
return true return true
} }

View file

@ -17,74 +17,71 @@
package org.oxycblt.auxio.music.dirs 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.databinding.ItemMusicDirBinding
import org.oxycblt.auxio.music.Directory 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.context
import org.oxycblt.auxio.util.inflater 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 * @author OxygenCobalt
*/ */
class MusicDirAdapter(listener: Listener) : class MusicDirAdapter(private val listener: Listener) : RecyclerView.Adapter<MusicDirViewHolder>() {
MonoAdapter<Directory, MusicDirAdapter.Listener, MusicDirViewHolder>(listener) { private val _dirs = mutableListOf<Directory>()
override val data = ExcludedBackingData(this) val dirs: List<Directory> = _dirs
override val creator = MusicDirViewHolder.CREATOR
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<Directory>) {
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 { interface Listener {
fun onRemoveDirectory(dir: Directory) fun onRemoveDirectory(dir: Directory)
} }
class ExcludedBackingData(private val adapter: MusicDirAdapter) : BackingData<Directory>() {
private val _currentList = mutableListOf<Directory>()
val currentList: List<Directory> = _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<Directory>) {
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. */ /** The viewholder for [MusicDirAdapter]. Not intended for use in other adapters. */
class MusicDirViewHolder private constructor(private val binding: ItemMusicDirBinding) : class MusicDirViewHolder private constructor(private val binding: ItemMusicDirBinding) :
BindingViewHolder<Directory, MusicDirAdapter.Listener>(binding.root) { RecyclerView.ViewHolder(binding.root) {
override fun bind(item: Directory, listener: MusicDirAdapter.Listener) { 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.dirPath.text = item.resolveName(binding.context)
binding.dirDelete.setOnClickListener { listener.onRemoveDirectory(item) } binding.dirDelete.setOnClickListener { listener.onRemoveDirectory(item) }
} }
companion object { companion object {
val CREATOR = fun new(parent: View) =
object : Creator<MusicDirViewHolder> { MusicDirViewHolder(ItemMusicDirBinding.inflate(parent.context.inflater))
override val viewType: Int
get() = throw UnsupportedOperationException()
override fun create(context: Context) =
MusicDirViewHolder(ItemMusicDirBinding.inflate(context.inflater))
}
} }
} }

View file

@ -60,9 +60,7 @@ class MusicDirsDialog :
.setPositiveButton(R.string.lbl_save) { _, _ -> .setPositiveButton(R.string.lbl_save) { _, _ ->
val dirs = settings.getMusicDirs(storageManager) val dirs = settings.getMusicDirs(storageManager)
val newDirs = val newDirs =
MusicDirs( MusicDirs(dirs = dirAdapter.dirs, shouldInclude = isInclude(requireBinding()))
dirs = dirAdapter.data.currentList,
shouldInclude = isInclude(requireBinding()))
if (dirs != newDirs) { if (dirs != newDirs) {
logD("Committing changes") logD("Committing changes")
settings.setMusicDirs(newDirs) settings.setMusicDirs(newDirs)
@ -105,7 +103,7 @@ class MusicDirsDialog :
} }
} }
dirAdapter.data.addAll(dirs.dirs) dirAdapter.addAll(dirs.dirs)
requireBinding().dirsEmpty.isVisible = dirs.dirs.isEmpty() requireBinding().dirsEmpty.isVisible = dirs.dirs.isEmpty()
binding.folderModeGroup.apply { binding.folderModeGroup.apply {
@ -124,7 +122,7 @@ class MusicDirsDialog :
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putStringArrayList( 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())) outState.putBoolean(KEY_PENDING_MODE, isInclude(requireBinding()))
} }
@ -134,8 +132,8 @@ class MusicDirsDialog :
} }
override fun onRemoveDirectory(dir: Directory) { override fun onRemoveDirectory(dir: Directory) {
dirAdapter.data.remove(dir) dirAdapter.remove(dir)
requireBinding().dirsEmpty.isVisible = dirAdapter.data.currentList.isEmpty() requireBinding().dirsEmpty.isVisible = dirAdapter.dirs.isEmpty()
} }
private fun addDocTreePath(uri: Uri?) { private fun addDocTreePath(uri: Uri?) {
@ -147,7 +145,7 @@ class MusicDirsDialog :
val dir = parseExcludedUri(uri) val dir = parseExcludedUri(uri)
if (dir != null) { if (dir != null) {
dirAdapter.data.add(dir) dirAdapter.add(dir)
requireBinding().dirsEmpty.isVisible = false requireBinding().dirsEmpty.isVisible = false
} else { } else {
requireContext().showToast(R.string.err_bad_dir) requireContext().showToast(R.string.err_bad_dir)

View file

@ -18,26 +18,31 @@
package org.oxycblt.auxio.playback.queue package org.oxycblt.auxio.playback.queue
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.LayerDrawable import android.graphics.drawable.LayerDrawable
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemQueueSongBinding import org.oxycblt.auxio.databinding.ItemQueueSongBinding
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.recycler.* import org.oxycblt.auxio.ui.recycler.*
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
class QueueAdapter(listener: QueueItemListener) : class QueueAdapter(private val listener: QueueItemListener) :
MonoAdapter<Song, QueueItemListener, QueueSongViewHolder>(listener) { RecyclerView.Adapter<QueueSongViewHolder>() {
private var differ = SyncListDiffer(this, QueueSongViewHolder.DIFFER)
private var currentIndex = 0 private var currentIndex = 0
override val data = SyncBackingData(this, QueueSongViewHolder.DIFFER) override fun getItemCount() = differ.currentList.size
override val creator = QueueSongViewHolder.CREATOR
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
QueueSongViewHolder.new(parent)
override fun onBindViewHolder(holder: QueueSongViewHolder, position: Int) =
throw IllegalStateException()
override fun onBindViewHolder( override fun onBindViewHolder(
viewHolder: QueueSongViewHolder, viewHolder: QueueSongViewHolder,
@ -45,13 +50,21 @@ class QueueAdapter(listener: QueueItemListener) :
payload: List<Any> payload: List<Any>
) { ) {
if (payload.isEmpty()) { if (payload.isEmpty()) {
super.onBindViewHolder(viewHolder, position, payload) viewHolder.bind(differ.currentList[position], listener)
} }
viewHolder.isEnabled = position > currentIndex viewHolder.isEnabled = position > currentIndex
viewHolder.isActivated = position == currentIndex viewHolder.isActivated = position == currentIndex
} }
fun submitList(newList: List<Song>) {
differ.submitList(newList)
}
fun replaceList(newList: List<Song>) {
differ.replaceList(newList)
}
fun updateIndex(index: Int) { fun updateIndex(index: Int) {
when { when {
index < currentIndex -> { index < currentIndex -> {
@ -79,7 +92,7 @@ interface QueueItemListener {
class QueueSongViewHolder class QueueSongViewHolder
private constructor( private constructor(
private val binding: ItemQueueSongBinding, private val binding: ItemQueueSongBinding,
) : BindingViewHolder<Song, QueueItemListener>(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
val bodyView: View val bodyView: View
get() = binding.body get() = binding.body
val backgroundView: View val backgroundView: View
@ -92,23 +105,6 @@ private constructor(
alpha = 0 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 { init {
binding.body.background = binding.body.background =
LayerDrawable( LayerDrawable(
@ -121,7 +117,7 @@ private constructor(
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun bind(item: Song, listener: QueueItemListener) { fun bind(item: Song, listener: QueueItemListener) {
binding.songAlbumCover.bind(item) binding.songAlbumCover.bind(item)
binding.songName.text = item.resolveName(binding.context) binding.songName.text = item.resolveName(binding.context)
binding.songInfo.text = item.resolveIndividualArtistName(binding.context) binding.songInfo.text = item.resolveIndividualArtistName(binding.context)
@ -140,15 +136,26 @@ private constructor(
} }
} }
companion object { var isEnabled: Boolean
val CREATOR = get() = binding.songAlbumCover.isEnabled
object : Creator<QueueSongViewHolder> { set(value) {
override val viewType: Int // Don't want to disable clicking, just indicate the body and handle is disabled
get() = IntegerTable.ITEM_TYPE_QUEUE_SONG binding.songAlbumCover.isEnabled = value
binding.songName.isEnabled = value
binding.songInfo.isEnabled = value
binding.songDragHandle.isEnabled = value
}
override fun create(context: Context): QueueSongViewHolder = var isActivated: Boolean
QueueSongViewHolder(ItemQueueSongBinding.inflate(context.inflater)) 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 val DIFFER = SongViewHolder.DIFFER
} }

View file

@ -85,10 +85,10 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
val replaceQueue = queueModel.replaceQueue val replaceQueue = queueModel.replaceQueue
if (replaceQueue == true) { if (replaceQueue == true) {
logD("Replacing queue") logD("Replacing queue")
queueAdapter.data.replaceList(queue) queueAdapter.replaceList(queue)
} else { } else {
logD("Diffing queue") logD("Diffing queue")
queueAdapter.data.submitList(queue) queueAdapter.submitList(queue)
} }
binding.queueDivider.isInvisible = binding.queueDivider.isInvisible =

View file

@ -200,6 +200,7 @@ class PlaybackService :
playbackManager.isPlaying = false playbackManager.isPlaying = false
playbackManager.unregisterInternalPlayer(this) playbackManager.unregisterInternalPlayer(this)
musicStore.addCallback(this)
settings.release() settings.release()
unregisterReceiver(systemReceiver) unregisterReceiver(systemReceiver)
serviceJob.cancel() serviceJob.cancel()

View file

@ -17,6 +17,8 @@
package org.oxycblt.auxio.search package org.oxycblt.auxio.search
import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist 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.music.Song
import org.oxycblt.auxio.ui.recycler.AlbumViewHolder import org.oxycblt.auxio.ui.recycler.AlbumViewHolder
import org.oxycblt.auxio.ui.recycler.ArtistViewHolder 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.GenreViewHolder
import org.oxycblt.auxio.ui.recycler.Header import org.oxycblt.auxio.ui.recycler.Header
import org.oxycblt.auxio.ui.recycler.HeaderViewHolder import org.oxycblt.auxio.ui.recycler.HeaderViewHolder
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener 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.SimpleItemCallback
import org.oxycblt.auxio.ui.recycler.SongViewHolder import org.oxycblt.auxio.ui.recycler.SongViewHolder
class SearchAdapter(listener: MenuItemListener) : MultiAdapter<MenuItemListener>(listener) { class SearchAdapter(private val listener: MenuItemListener) :
override val data = AsyncBackingData(this, DIFFER) RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val differ = AsyncListDiffer(this, DIFFER)
override fun getCreatorFromItem(item: Item) = override fun getItemCount() = differ.currentList.size
when (item) {
is Song -> SongViewHolder.CREATOR override fun getItemViewType(position: Int) =
is Album -> AlbumViewHolder.CREATOR when (differ.currentList[position]) {
is Artist -> ArtistViewHolder.CREATOR is Song -> SongViewHolder.VIEW_TYPE
is Genre -> GenreViewHolder.CREATOR is Album -> AlbumViewHolder.VIEW_TYPE
is Header -> HeaderViewHolder.CREATOR is Artist -> ArtistViewHolder.VIEW_TYPE
else -> null 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) { when (viewType) {
SongViewHolder.CREATOR.viewType -> SongViewHolder.CREATOR SongViewHolder.VIEW_TYPE -> SongViewHolder.new(parent)
AlbumViewHolder.CREATOR.viewType -> AlbumViewHolder.CREATOR AlbumViewHolder.VIEW_TYPE -> AlbumViewHolder.new(parent)
ArtistViewHolder.CREATOR.viewType -> ArtistViewHolder.CREATOR ArtistViewHolder.VIEW_TYPE -> ArtistViewHolder.new(parent)
GenreViewHolder.CREATOR.viewType -> GenreViewHolder.CREATOR GenreViewHolder.VIEW_TYPE -> GenreViewHolder.new(parent)
HeaderViewHolder.CREATOR.viewType -> HeaderViewHolder.CREATOR HeaderViewHolder.VIEW_TYPE -> HeaderViewHolder.new(parent)
else -> null else -> error("Invalid item type $viewType")
} }
override fun onBind( override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
viewHolder: RecyclerView.ViewHolder, when (val item = differ.currentList[position]) {
item: Item, is Song -> (holder as SongViewHolder).bind(item, listener)
listener: MenuItemListener, is Album -> (holder as AlbumViewHolder).bind(item, listener)
payload: List<Any> is Artist -> (holder as ArtistViewHolder).bind(item, listener)
) { is Genre -> (holder as GenreViewHolder).bind(item, listener)
when (item) { is Header -> (holder as HeaderViewHolder).bind(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 -> {}
} }
} }
fun submitList(list: List<Item>, callback: () -> Unit) = differ.submitList(list, callback)
companion object { companion object {
private val DIFFER = private val DIFFER =
object : SimpleItemCallback<Item>() { object : SimpleItemCallback<Item>() {

View file

@ -106,7 +106,7 @@ class SearchFragment :
binding.searchRecycler.apply { binding.searchRecycler.apply {
adapter = searchAdapter adapter = searchAdapter
setSpanSizeLookup { pos -> searchAdapter.data.getItem(pos) is Header } setSpanSizeLookup { pos -> searchModel.searchResults.value[pos] is Header }
} }
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
@ -154,7 +154,7 @@ class SearchFragment :
private fun updateResults(results: List<Item>) { private fun updateResults(results: List<Item>) {
val binding = requireBinding() 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 // 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 // the query actually changes instead of once every re-creation event, but sadly
// that doesn't seem possible. // that doesn't seem possible.

View file

@ -17,13 +17,12 @@
package org.oxycblt.auxio.ui.accent 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.appcompat.widget.TooltipCompat
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemAccentBinding 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.getAttrColorCompat
import org.oxycblt.auxio.util.getColorCompat import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
@ -32,25 +31,29 @@ import org.oxycblt.auxio.util.inflater
* An adapter that displays the accent palette. * An adapter that displays the accent palette.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class AccentAdapter(listener: Listener) : class AccentAdapter(private val listener: Listener) : RecyclerView.Adapter<AccentViewHolder>() {
MonoAdapter<Accent, AccentAdapter.Listener, AccentViewHolder>(listener) {
var selectedAccent: Accent? = null var selectedAccent: Accent? = null
private set private set
override val data = AccentData() override fun getItemCount() = Accent.MAX
override val creator = AccentViewHolder.CREATOR
override fun onBind( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = AccentViewHolder.new(parent)
viewHolder: AccentViewHolder,
item: Accent, override fun onBindViewHolder(holder: AccentViewHolder, position: Int) =
listener: Listener, throw IllegalStateException()
payload: List<Any>
override fun onBindViewHolder(
holder: AccentViewHolder,
position: Int,
payloads: MutableList<Any>
) { ) {
if (payload.isEmpty()) { val item = Accent.from(position)
super.onBind(viewHolder, item, listener, payload)
if (payloads.isEmpty()) {
holder.bind(item, listener)
} }
viewHolder.setSelected(item == selectedAccent) holder.setSelected(item == selectedAccent)
} }
fun setSelectedAccent(accent: Accent) { fun setSelectedAccent(accent: Accent) {
@ -64,20 +67,15 @@ class AccentAdapter(listener: Listener) :
fun onAccentSelected(accent: Accent) fun onAccentSelected(accent: Accent)
} }
class AccentData : BackingData<Accent>() {
override fun getItem(position: Int) = Accent.from(position)
override fun getItemCount() = Accent.MAX
}
companion object { companion object {
val PAYLOAD_SELECTION_CHANGED = Any() val PAYLOAD_SELECTION_CHANGED = Any()
} }
} }
class AccentViewHolder private constructor(private val binding: ItemAccentBinding) : class AccentViewHolder private constructor(private val binding: ItemAccentBinding) :
BindingViewHolder<Accent, AccentAdapter.Listener>(binding.root) { RecyclerView.ViewHolder(binding.root) {
override fun bind(item: Accent, listener: AccentAdapter.Listener) { fun bind(item: Accent, listener: AccentAdapter.Listener) {
setSelected(false) setSelected(false)
binding.accent.apply { binding.accent.apply {
@ -101,13 +99,6 @@ class AccentViewHolder private constructor(private val binding: ItemAccentBindin
} }
companion object { companion object {
val CREATOR = fun new(parent: View) = AccentViewHolder(ItemAccentBinding.inflate(parent.context.inflater))
object : Creator<AccentViewHolder> {
override val viewType: Int
get() = throw UnsupportedOperationException()
override fun create(context: Context) =
AccentViewHolder(ItemAccentBinding.inflate(context.inflater))
}
} }
} }

View file

@ -38,6 +38,10 @@ import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getDimenSize import org.oxycblt.auxio.util.getDimenSize
import org.oxycblt.auxio.util.isRtl import org.oxycblt.auxio.util.isRtl
/**
* Internal view responsible for the fast scroller popup.
* @author OxygenCobalt, Hai Zhang
*/
class FastScrollPopupView class FastScrollPopupView
@JvmOverloads @JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0) : constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0) :

View file

@ -17,145 +17,13 @@
package org.oxycblt.auxio.ui.recycler package org.oxycblt.auxio.ui.recycler
import android.content.Context
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.recyclerview.widget.AdapterListUpdateCallback import androidx.recyclerview.widget.AdapterListUpdateCallback
import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView 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<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>
/**
* 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<Any>) {
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<Any>) {
onBind(viewHolder, data.getItem(position), listener, payload)
}
}
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".
* @author OxygenCobalt
*/
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,
payload: List<Any>
)
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<Any>
) {
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<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 * 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. * provided for free by the normal adapter implementations, such as certain types of diffing.
@ -174,85 +42,49 @@ data class Header(
get() = string.toLong() get() = string.toLong()
} }
/** /** An interface for detecting if an item has been clicked once. */
* Represents data that backs a [MonoAdapter] or [MultiAdapter]. This can be implemented by any interface ItemClickListener {
* datatype to customize the organization or editing of data in a way that works best for the /** Called when an item is clicked once. */
* specific adapter. fun onItemClick(item: Item)
*/
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
} }
/** /** An interface for detecting if an item has had it's menu opened. */
* A list-backed [BackingData] that is modified synchronously. This is generally the recommended interface MenuItemListener : ItemClickListener {
* option for most adapters. /** Called when an item desires to open a menu relating to it. */
* @author OxygenCobalt fun onOpenMenu(item: Item, anchor: View)
*/
class SyncBackingData<T>(adapter: RecyclerView.Adapter<*>, diffCallback: DiffUtil.ItemCallback<T>) :
BackingData<T>() {
private var differ = SyncListDiffer(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 normally, doing a diff synchronously. Only use this for trivial changes. */
fun submitList(newList: List<T>) {
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<T>) {
if (newList == differ.currentList) {
return
}
differ.currentList = emptyList()
differ.currentList = newList
}
} }
/** /**
* Like [AsyncListDiffer], but synchronous. This may seem like it would be inefficient, but in * 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 * 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<T>( class SyncListDiffer<T>(
adapter: RecyclerView.Adapter<*>, adapter: RecyclerView.Adapter<*>,
private val diffCallback: DiffUtil.ItemCallback<T> private val diffCallback: DiffUtil.ItemCallback<T>
) { ) {
private val updateCallback = AdapterListUpdateCallback(adapter) private val updateCallback = AdapterListUpdateCallback(adapter)
private var _currentList: List<T> = emptyList() var currentList: List<T> = emptyList()
var currentList: List<T> private set(newList) {
get() = _currentList if (newList === currentList || newList.isEmpty() && currentList.isEmpty()) {
set(newList) {
if (newList === _currentList || newList.isEmpty() && _currentList.isEmpty()) {
return return
} }
if (newList.isEmpty()) { if (newList.isEmpty()) {
val oldListSize = _currentList.size val oldListSize = currentList.size
_currentList = emptyList() field = emptyList()
updateCallback.onRemoved(0, oldListSize) updateCallback.onRemoved(0, oldListSize)
return return
} }
if (_currentList.isEmpty()) { if (currentList.isEmpty()) {
_currentList = newList field = newList
updateCallback.onInserted(0, newList.size) updateCallback.onInserted(0, newList.size)
return return
} }
val oldList = _currentList val oldList = currentList
val result = val result =
DiffUtil.calculateDiff( DiffUtil.calculateDiff(
object : DiffUtil.Callback() { object : DiffUtil.Callback() {
@ -306,35 +138,26 @@ private class SyncListDiffer<T>(
} }
}) })
_currentList = newList field = newList
result.dispatchUpdatesTo(updateCallback) result.dispatchUpdatesTo(updateCallback)
} }
}
/** /** Submit a list normally, doing a diff synchronously. Only use this for trivial changes. */
* A list-backed [BackingData] that is modified with [AsyncListDiffer]. This is useful in cases fun submitList(newList: List<T>) {
* where data updates are rapid-fire and unpredictable, and where the benefits of asynchronously currentList = newList
* diffing the adapter outweigh the shortcomings. }
* @author OxygenCobalt
*/
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 * Replace this list with a new list. This is useful for very large list diffs that would
* dropped. * generally be too chaotic and slow to provide a good UX.
*/ */
fun submitList(newList: List<T>, onDone: () -> Unit = {}) { fun replaceList(newList: List<T>) {
differ.submitList(newList, onDone) if (newList == currentList) {
return
}
currentList = emptyList()
currentList = newList
} }
} }

View file

@ -17,7 +17,8 @@
package org.oxycblt.auxio.ui.recycler 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.IntegerTable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemHeaderBinding import org.oxycblt.auxio.databinding.ItemHeaderBinding
@ -36,8 +37,8 @@ import org.oxycblt.auxio.util.inflater
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class SongViewHolder private constructor(private val binding: ItemSongBinding) : class SongViewHolder private constructor(private val binding: ItemSongBinding) :
BindingViewHolder<Song, MenuItemListener>(binding.root) { RecyclerView.ViewHolder(binding.root) {
override fun bind(item: Song, listener: MenuItemListener) { fun bind(item: Song, listener: MenuItemListener) {
binding.songAlbumCover.bind(item) binding.songAlbumCover.bind(item)
binding.songName.text = item.resolveName(binding.context) binding.songName.text = item.resolveName(binding.context)
binding.songInfo.text = item.resolveIndividualArtistName(binding.context) binding.songInfo.text = item.resolveIndividualArtistName(binding.context)
@ -50,14 +51,9 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
} }
companion object { companion object {
val CREATOR = const val VIEW_TYPE = IntegerTable.VIEW_TYPE_SONG
object : Creator<SongViewHolder> {
override val viewType: Int
get() = IntegerTable.ITEM_TYPE_SONG
override fun create(context: Context) = fun new(parent: View) = SongViewHolder(ItemSongBinding.inflate(parent.context.inflater))
SongViewHolder(ItemSongBinding.inflate(context.inflater))
}
val DIFFER = val DIFFER =
object : SimpleItemCallback<Song>() { object : SimpleItemCallback<Song>() {
@ -75,9 +71,9 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
class AlbumViewHolder class AlbumViewHolder
private constructor( private constructor(
private val binding: ItemParentBinding, private val binding: ItemParentBinding,
) : BindingViewHolder<Album, MenuItemListener>(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
override fun bind(item: Album, listener: MenuItemListener) { fun bind(item: Album, listener: MenuItemListener) {
binding.parentImage.bind(item) binding.parentImage.bind(item)
binding.parentName.text = item.resolveName(binding.context) binding.parentName.text = item.resolveName(binding.context)
binding.parentInfo.text = item.artist.resolveName(binding.context) binding.parentInfo.text = item.artist.resolveName(binding.context)
@ -90,14 +86,9 @@ private constructor(
} }
companion object { companion object {
val CREATOR = const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ALBUM
object : Creator<AlbumViewHolder> {
override val viewType: Int
get() = IntegerTable.ITEM_TYPE_ALBUM
override fun create(context: Context) = fun new(parent: View) = AlbumViewHolder(ItemParentBinding.inflate(parent.context.inflater))
AlbumViewHolder(ItemParentBinding.inflate(context.inflater))
}
val DIFFER = val DIFFER =
object : SimpleItemCallback<Album>() { object : SimpleItemCallback<Album>() {
@ -114,9 +105,9 @@ private constructor(
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class ArtistViewHolder private constructor(private val binding: ItemParentBinding) : class ArtistViewHolder private constructor(private val binding: ItemParentBinding) :
BindingViewHolder<Artist, MenuItemListener>(binding.root) { RecyclerView.ViewHolder(binding.root) {
override fun bind(item: Artist, listener: MenuItemListener) { fun bind(item: Artist, listener: MenuItemListener) {
binding.parentImage.bind(item) binding.parentImage.bind(item)
binding.parentName.text = item.resolveName(binding.context) binding.parentName.text = item.resolveName(binding.context)
binding.parentInfo.text = binding.parentInfo.text =
@ -133,14 +124,9 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
} }
companion object { companion object {
val CREATOR = const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST
object : Creator<ArtistViewHolder> {
override val viewType: Int
get() = IntegerTable.ITEM_TYPE_ARTIST
override fun create(context: Context) = fun new(parent: View) = ArtistViewHolder(ItemParentBinding.inflate(parent.context.inflater))
ArtistViewHolder(ItemParentBinding.inflate(context.inflater))
}
val DIFFER = val DIFFER =
object : SimpleItemCallback<Artist>() { object : SimpleItemCallback<Artist>() {
@ -159,9 +145,9 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
class GenreViewHolder class GenreViewHolder
private constructor( private constructor(
private val binding: ItemParentBinding, private val binding: ItemParentBinding,
) : BindingViewHolder<Genre, MenuItemListener>(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
override fun bind(item: Genre, listener: MenuItemListener) { fun bind(item: Genre, listener: MenuItemListener) {
binding.parentImage.bind(item) binding.parentImage.bind(item)
binding.parentName.text = item.resolveName(binding.context) binding.parentName.text = item.resolveName(binding.context)
binding.parentInfo.text = binding.parentInfo.text =
@ -175,14 +161,9 @@ private constructor(
} }
companion object { companion object {
val CREATOR = const val VIEW_TYPE = IntegerTable.VIEW_TYPE_GENRE
object : Creator<GenreViewHolder> {
override val viewType: Int
get() = IntegerTable.ITEM_TYPE_GENRE
override fun create(context: Context) = fun new(parent: View) = GenreViewHolder(ItemParentBinding.inflate(parent.context.inflater))
GenreViewHolder(ItemParentBinding.inflate(context.inflater))
}
val DIFFER = val DIFFER =
object : SimpleItemCallback<Genre>() { object : SimpleItemCallback<Genre>() {
@ -197,21 +178,16 @@ private constructor(
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class HeaderViewHolder private constructor(private val binding: ItemHeaderBinding) : class HeaderViewHolder private constructor(private val binding: ItemHeaderBinding) :
BindingViewHolder<Header, Unit>(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) binding.title.text = binding.context.getString(item.string)
} }
companion object { companion object {
val CREATOR = const val VIEW_TYPE = IntegerTable.VIEW_TYPE_HEADER
object : Creator<HeaderViewHolder> {
override val viewType: Int
get() = IntegerTable.ITEM_TYPE_HEADER
override fun create(context: Context) = fun new(parent: View) = HeaderViewHolder(ItemHeaderBinding.inflate(parent.context.inflater))
HeaderViewHolder(ItemHeaderBinding.inflate(context.inflater))
}
val DIFFER = val DIFFER =
object : SimpleItemCallback<Header>() { object : SimpleItemCallback<Header>() {

View file

@ -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 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. view-binding is still used.
Auxio uses `RecyclerView` for all list information. Due to the complexities of Auxio, the way one Auxio uses `RecyclerView` for all list information. To manage some complexity, there are a few
defines an adapter differs quite heavily from the normal library. Generally, start with conventions that are used when creating adapters. These can be seen in the `RecyclerFramework`
`MonoAdapter` for a list with one type of data and `MultiAdapter` for lists with many types of data, file and in adapter implementations.
then follow the documentation to see how to fully implement the class.
#### Object communication #### Object communication
Auxio's codebase is mostly centered around 4 different types of code that communicates with Auxio's codebase is mostly centered around 4 different types of code that communicates with