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. */
object IntegerTable {
/** SongViewHolder */
const val ITEM_TYPE_SONG = 0xA000
const val VIEW_TYPE_SONG = 0xA000
/** AlbumViewHolder */
const val ITEM_TYPE_ALBUM = 0xA001
const val VIEW_TYPE_ALBUM = 0xA001
/** ArtistViewHolder */
const val ITEM_TYPE_ARTIST = 0xA002
const val VIEW_TYPE_ARTIST = 0xA002
/** GenreViewHolder */
const val ITEM_TYPE_GENRE = 0xA003
const val VIEW_TYPE_GENRE = 0xA003
/** HeaderViewHolder */
const val ITEM_TYPE_HEADER = 0xA004
const val VIEW_TYPE_HEADER = 0xA004
/** SortHeaderViewHolder */
const val ITEM_TYPE_SORT_HEADER = 0xA005
const val VIEW_TYPE_SORT_HEADER = 0xA005
/** AlbumDetailViewHolder */
const val ITEM_TYPE_ALBUM_DETAIL = 0xA006
const val VIEW_TYPE_ALBUM_DETAIL = 0xA006
/** AlbumSongViewHolder */
const val ITEM_TYPE_ALBUM_SONG = 0xA007
const val VIEW_TYPE_ALBUM_SONG = 0xA007
/** ArtistDetailViewHolder */
const val ITEM_TYPE_ARTIST_DETAIL = 0xA008
const val VIEW_TYPE_ARTIST_DETAIL = 0xA008
/** ArtistAlbumViewHolder */
const val ITEM_TYPE_ARTIST_ALBUM = 0xA009
const val VIEW_TYPE_ARTIST_ALBUM = 0xA009
/** ArtistSongViewHolder */
const val ITEM_TYPE_ARTIST_SONG = 0xA00A
const val VIEW_TYPE_ARTIST_SONG = 0xA00A
/** GenreDetailViewHolder */
const val ITEM_TYPE_GENRE_DETAIL = 0xA00B
const val VIEW_TYPE_GENRE_DETAIL = 0xA00B
/** DiscHeaderViewHolder */
const val ITEM_TYPE_DISC_HEADER = 0xA00C
/** QueueSongViewHolder */
const val ITEM_TYPE_QUEUE_SONG = 0xA00D
const val VIEW_TYPE_DISC_HEADER = 0xA00C
/** "Music playback" notification code */
const val PLAYBACK_NOTIFICATION_CODE = 0xA0A0

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,42 +18,35 @@
package org.oxycblt.auxio.home.tabs
import android.annotation.SuppressLint
import android.content.Context
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemTabBinding
import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.recycler.BackingData
import org.oxycblt.auxio.ui.recycler.BindingViewHolder
import org.oxycblt.auxio.ui.recycler.MonoAdapter
import org.oxycblt.auxio.util.inflater
class TabAdapter(listener: Listener) :
MonoAdapter<Tab, TabAdapter.Listener, TabViewHolder>(listener) {
override val data = TabData(this)
override val creator = TabViewHolder.CREATOR
interface Listener {
fun onVisibilityToggled(displayMode: DisplayMode)
fun onPickUpTab(viewHolder: RecyclerView.ViewHolder)
}
class TabData(private val adapter: RecyclerView.Adapter<*>) : BackingData<Tab>() {
class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewHolder>() {
var tabs = arrayOf<Tab>()
private set
override fun getItem(position: Int) = tabs[position]
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
adapter.notifyDataSetChanged()
notifyDataSetChanged()
}
fun setTab(at: Int, tab: Tab) {
tabs[at] = tab
adapter.notifyItemChanged(at, PAYLOAD_TAB_CHANGED)
notifyItemChanged(at, PAYLOAD_TAB_CHANGED)
}
fun moveItems(from: Int, to: Int) {
@ -61,8 +54,12 @@ class TabAdapter(listener: Listener) :
val f = tabs[from]
tabs[from] = t
tabs[to] = f
adapter.notifyItemMoved(from, to)
notifyItemMoved(from, to)
}
interface Listener {
fun onVisibilityToggled(displayMode: DisplayMode)
fun onPickUpTab(viewHolder: RecyclerView.ViewHolder)
}
companion object {
@ -71,10 +68,15 @@ class TabAdapter(listener: Listener) :
}
class TabViewHolder private constructor(private val binding: ItemTabBinding) :
BindingViewHolder<Tab, TabAdapter.Listener>(binding.root) {
RecyclerView.ViewHolder(binding.root) {
@SuppressLint("ClickableViewAccessibility")
override fun bind(item: Tab, listener: TabAdapter.Listener) {
binding.root.apply { setOnClickListener { listener.onVisibilityToggled(item.mode) } }
fun bind(item: Tab, listener: TabAdapter.Listener) {
// Actually make the item full-width, which it won't be in dialogs
binding.root.layoutParams =
RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
binding.root.setOnClickListener { listener.onVisibilityToggled(item.mode) }
binding.tabIcon.apply {
setText(item.mode.string)
@ -92,13 +94,6 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) :
}
companion object {
val CREATOR =
object : Creator<TabViewHolder> {
override val viewType: Int
get() = throw UnsupportedOperationException()
override fun create(context: Context) =
TabViewHolder(ItemTabBinding.inflate(context.inflater))
}
fun new(parent: View) = TabViewHolder(ItemTabBinding.inflate(parent.context.inflater))
}
}

View file

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

View file

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

View file

@ -17,74 +17,71 @@
package org.oxycblt.auxio.music.dirs
import android.content.Context
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemMusicDirBinding
import org.oxycblt.auxio.music.Directory
import org.oxycblt.auxio.ui.recycler.BackingData
import org.oxycblt.auxio.ui.recycler.BindingViewHolder
import org.oxycblt.auxio.ui.recycler.MonoAdapter
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater
/**
* Adapter that shows the excluded directories and their "Clear" button.
* Adapter that shows the list of music folder and their "Clear" button.
* @author OxygenCobalt
*/
class MusicDirAdapter(listener: Listener) :
MonoAdapter<Directory, MusicDirAdapter.Listener, MusicDirViewHolder>(listener) {
override val data = ExcludedBackingData(this)
override val creator = MusicDirViewHolder.CREATOR
class MusicDirAdapter(private val listener: Listener) : RecyclerView.Adapter<MusicDirViewHolder>() {
private val _dirs = mutableListOf<Directory>()
val dirs: List<Directory> = _dirs
interface Listener {
fun onRemoveDirectory(dir: Directory)
}
override fun getItemCount() = dirs.size
class ExcludedBackingData(private val adapter: MusicDirAdapter) : BackingData<Directory>() {
private val _currentList = mutableListOf<Directory>()
val currentList: List<Directory> = _currentList
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
MusicDirViewHolder.new(parent)
override fun getItemCount(): Int = _currentList.size
override fun getItem(position: Int): Directory = _currentList[position]
override fun onBindViewHolder(holder: MusicDirViewHolder, position: Int) =
holder.bind(dirs[position], listener)
fun add(dir: Directory) {
if (_currentList.contains(dir)) {
if (_dirs.contains(dir)) {
return
}
_currentList.add(dir)
adapter.notifyItemInserted(_currentList.lastIndex)
_dirs.add(dir)
notifyItemInserted(_dirs.lastIndex)
}
fun addAll(dirs: List<Directory>) {
val oldLastIndex = dirs.lastIndex
_currentList.addAll(dirs)
adapter.notifyItemRangeInserted(oldLastIndex, dirs.size)
_dirs.addAll(dirs)
notifyItemRangeInserted(oldLastIndex, dirs.size)
}
fun remove(dir: Directory) {
val idx = _currentList.indexOf(dir)
_currentList.removeAt(idx)
adapter.notifyItemRemoved(idx)
val idx = _dirs.indexOf(dir)
_dirs.removeAt(idx)
notifyItemRemoved(idx)
}
interface Listener {
fun onRemoveDirectory(dir: Directory)
}
}
/** The viewholder for [MusicDirAdapter]. Not intended for use in other adapters. */
class MusicDirViewHolder private constructor(private val binding: ItemMusicDirBinding) :
BindingViewHolder<Directory, MusicDirAdapter.Listener>(binding.root) {
override fun bind(item: Directory, listener: MusicDirAdapter.Listener) {
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Directory, listener: MusicDirAdapter.Listener) {
// Actually make the item full-width, which it won't be in dialogs
binding.root.layoutParams =
RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
binding.dirPath.text = item.resolveName(binding.context)
binding.dirDelete.setOnClickListener { listener.onRemoveDirectory(item) }
}
companion object {
val CREATOR =
object : Creator<MusicDirViewHolder> {
override val viewType: Int
get() = throw UnsupportedOperationException()
override fun create(context: Context) =
MusicDirViewHolder(ItemMusicDirBinding.inflate(context.inflater))
}
fun new(parent: View) =
MusicDirViewHolder(ItemMusicDirBinding.inflate(parent.context.inflater))
}
}

View file

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

View file

@ -18,26 +18,31 @@
package org.oxycblt.auxio.playback.queue
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.LayerDrawable
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isInvisible
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemQueueSongBinding
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.recycler.*
import org.oxycblt.auxio.util.*
class QueueAdapter(listener: QueueItemListener) :
MonoAdapter<Song, QueueItemListener, QueueSongViewHolder>(listener) {
class QueueAdapter(private val listener: QueueItemListener) :
RecyclerView.Adapter<QueueSongViewHolder>() {
private var differ = SyncListDiffer(this, QueueSongViewHolder.DIFFER)
private var currentIndex = 0
override val data = SyncBackingData(this, QueueSongViewHolder.DIFFER)
override val creator = QueueSongViewHolder.CREATOR
override fun getItemCount() = differ.currentList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
QueueSongViewHolder.new(parent)
override fun onBindViewHolder(holder: QueueSongViewHolder, position: Int) =
throw IllegalStateException()
override fun onBindViewHolder(
viewHolder: QueueSongViewHolder,
@ -45,13 +50,21 @@ class QueueAdapter(listener: QueueItemListener) :
payload: List<Any>
) {
if (payload.isEmpty()) {
super.onBindViewHolder(viewHolder, position, payload)
viewHolder.bind(differ.currentList[position], listener)
}
viewHolder.isEnabled = position > currentIndex
viewHolder.isActivated = position == currentIndex
}
fun submitList(newList: List<Song>) {
differ.submitList(newList)
}
fun replaceList(newList: List<Song>) {
differ.replaceList(newList)
}
fun updateIndex(index: Int) {
when {
index < currentIndex -> {
@ -79,7 +92,7 @@ interface QueueItemListener {
class QueueSongViewHolder
private constructor(
private val binding: ItemQueueSongBinding,
) : BindingViewHolder<Song, QueueItemListener>(binding.root) {
) : RecyclerView.ViewHolder(binding.root) {
val bodyView: View
get() = binding.body
val backgroundView: View
@ -92,6 +105,37 @@ private constructor(
alpha = 0
}
init {
binding.body.background =
LayerDrawable(
arrayOf(
MaterialShapeDrawable.createWithElevationOverlay(binding.context).apply {
fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface)
elevation = binding.context.getDimen(R.dimen.elevation_normal)
},
backgroundDrawable))
}
@SuppressLint("ClickableViewAccessibility")
fun bind(item: Song, listener: QueueItemListener) {
binding.songAlbumCover.bind(item)
binding.songName.text = item.resolveName(binding.context)
binding.songInfo.text = item.resolveIndividualArtistName(binding.context)
binding.background.isInvisible = true
binding.body.setOnClickListener { listener.onClick(this) }
// Roll our own drag handlers as the default ones suck
binding.songDragHandle.setOnTouchListener { _, motionEvent ->
binding.songDragHandle.performClick()
if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
listener.onPickUp(this)
true
} else false
}
}
var isEnabled: Boolean
get() = binding.songAlbumCover.isEnabled
set(value) {
@ -109,46 +153,9 @@ private constructor(
binding.interactBody.isActivated = value
}
init {
binding.body.background =
LayerDrawable(
arrayOf(
MaterialShapeDrawable.createWithElevationOverlay(binding.context).apply {
fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface)
elevation = binding.context.getDimen(R.dimen.elevation_normal)
},
backgroundDrawable))
}
@SuppressLint("ClickableViewAccessibility")
override fun bind(item: Song, listener: QueueItemListener) {
binding.songAlbumCover.bind(item)
binding.songName.text = item.resolveName(binding.context)
binding.songInfo.text = item.resolveIndividualArtistName(binding.context)
binding.background.isInvisible = true
binding.body.setOnClickListener { listener.onClick(this) }
// Roll our own drag handlers as the default ones suck
binding.songDragHandle.setOnTouchListener { _, motionEvent ->
binding.songDragHandle.performClick()
if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
listener.onPickUp(this)
true
} else false
}
}
companion object {
val CREATOR =
object : Creator<QueueSongViewHolder> {
override val viewType: Int
get() = IntegerTable.ITEM_TYPE_QUEUE_SONG
override fun create(context: Context): QueueSongViewHolder =
QueueSongViewHolder(ItemQueueSongBinding.inflate(context.inflater))
}
fun new(parent: View) =
QueueSongViewHolder(ItemQueueSongBinding.inflate(parent.context.inflater))
val DIFFER = SongViewHolder.DIFFER
}

View file

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

View file

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

View file

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

View file

@ -106,7 +106,7 @@ class SearchFragment :
binding.searchRecycler.apply {
adapter = searchAdapter
setSpanSizeLookup { pos -> searchAdapter.data.getItem(pos) is Header }
setSpanSizeLookup { pos -> searchModel.searchResults.value[pos] is Header }
}
// --- VIEWMODEL SETUP ---
@ -154,7 +154,7 @@ class SearchFragment :
private fun updateResults(results: List<Item>) {
val binding = requireBinding()
searchAdapter.data.submitList(results.toMutableList()) {
searchAdapter.submitList(results.toMutableList()) {
// I would make it so that the position is only scrolled back to the top when
// the query actually changes instead of once every re-creation event, but sadly
// that doesn't seem possible.

View file

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

View file

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

View file

@ -17,145 +17,13 @@
package org.oxycblt.auxio.ui.recycler
import android.content.Context
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.recyclerview.widget.AdapterListUpdateCallback
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
// TODO: Unify music updates and sorts under replace
/**
* An adapter for one viewholder tied to one type of data. All functionality is derived from the
* overridden values.
* @author OxygenCobalt
*/
abstract class MonoAdapter<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
* provided for free by the normal adapter implementations, such as certain types of diffing.
@ -174,85 +42,49 @@ data class Header(
get() = string.toLong()
}
/**
* Represents data that backs a [MonoAdapter] or [MultiAdapter]. This can be implemented by any
* datatype to customize the organization or editing of data in a way that works best for the
* specific adapter.
*/
abstract class BackingData<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 been clicked once. */
interface ItemClickListener {
/** Called when an item is clicked once. */
fun onItemClick(item: Item)
}
/**
* A list-backed [BackingData] that is modified synchronously. This is generally the recommended
* option for most adapters.
* @author OxygenCobalt
*/
class SyncBackingData<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
}
/** An interface for detecting if an item has had it's menu opened. */
interface MenuItemListener : ItemClickListener {
/** Called when an item desires to open a menu relating to it. */
fun onOpenMenu(item: Item, anchor: View)
}
/**
* Like [AsyncListDiffer], but synchronous. This may seem like it would be inefficient, but in
* practice Auxio's lists tend to be small enough to the point where this does not matter, and
* situations that would be inefficient are ruled out with [SyncBackingData.replaceList].
* situations that would be inefficient are ruled out with [replaceList].
*/
private class SyncListDiffer<T>(
class SyncListDiffer<T>(
adapter: RecyclerView.Adapter<*>,
private val diffCallback: DiffUtil.ItemCallback<T>
) {
private val updateCallback = AdapterListUpdateCallback(adapter)
private var _currentList: List<T> = emptyList()
var currentList: List<T>
get() = _currentList
set(newList) {
if (newList === _currentList || newList.isEmpty() && _currentList.isEmpty()) {
var currentList: List<T> = emptyList()
private set(newList) {
if (newList === currentList || newList.isEmpty() && currentList.isEmpty()) {
return
}
if (newList.isEmpty()) {
val oldListSize = _currentList.size
_currentList = emptyList()
val oldListSize = currentList.size
field = emptyList()
updateCallback.onRemoved(0, oldListSize)
return
}
if (_currentList.isEmpty()) {
_currentList = newList
if (currentList.isEmpty()) {
field = newList
updateCallback.onInserted(0, newList.size)
return
}
val oldList = _currentList
val oldList = currentList
val result =
DiffUtil.calculateDiff(
object : DiffUtil.Callback() {
@ -306,35 +138,26 @@ private class SyncListDiffer<T>(
}
})
_currentList = newList
field = newList
result.dispatchUpdatesTo(updateCallback)
}
/** Submit a list normally, doing a diff synchronously. Only use this for trivial changes. */
fun submitList(newList: List<T>) {
currentList = newList
}
/**
* A list-backed [BackingData] that is modified with [AsyncListDiffer]. This is useful in cases
* where data updates are rapid-fire and unpredictable, and where the benefits of asynchronously
* diffing the adapter outweigh the shortcomings.
* @author OxygenCobalt
* 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.
*/
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
fun replaceList(newList: List<T>) {
if (newList == currentList) {
return
}
override fun getItem(position: Int): T = differ.currentList[position]
override fun getItemCount(): Int = differ.currentList.size
/**
* Submit a list for [AsyncListDiffer] to calculate. Any previous calls of [submitList] will be
* dropped.
*/
fun submitList(newList: List<T>, onDone: () -> Unit = {}) {
differ.submitList(newList, onDone)
currentList = emptyList()
currentList = newList
}
}

View file

@ -17,7 +17,8 @@
package org.oxycblt.auxio.ui.recycler
import android.content.Context
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemHeaderBinding
@ -36,8 +37,8 @@ import org.oxycblt.auxio.util.inflater
* @author OxygenCobalt
*/
class SongViewHolder private constructor(private val binding: ItemSongBinding) :
BindingViewHolder<Song, MenuItemListener>(binding.root) {
override fun bind(item: Song, listener: MenuItemListener) {
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Song, listener: MenuItemListener) {
binding.songAlbumCover.bind(item)
binding.songName.text = item.resolveName(binding.context)
binding.songInfo.text = item.resolveIndividualArtistName(binding.context)
@ -50,14 +51,9 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
}
companion object {
val CREATOR =
object : Creator<SongViewHolder> {
override val viewType: Int
get() = IntegerTable.ITEM_TYPE_SONG
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_SONG
override fun create(context: Context) =
SongViewHolder(ItemSongBinding.inflate(context.inflater))
}
fun new(parent: View) = SongViewHolder(ItemSongBinding.inflate(parent.context.inflater))
val DIFFER =
object : SimpleItemCallback<Song>() {
@ -75,9 +71,9 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
class AlbumViewHolder
private constructor(
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.parentName.text = item.resolveName(binding.context)
binding.parentInfo.text = item.artist.resolveName(binding.context)
@ -90,14 +86,9 @@ private constructor(
}
companion object {
val CREATOR =
object : Creator<AlbumViewHolder> {
override val viewType: Int
get() = IntegerTable.ITEM_TYPE_ALBUM
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ALBUM
override fun create(context: Context) =
AlbumViewHolder(ItemParentBinding.inflate(context.inflater))
}
fun new(parent: View) = AlbumViewHolder(ItemParentBinding.inflate(parent.context.inflater))
val DIFFER =
object : SimpleItemCallback<Album>() {
@ -114,9 +105,9 @@ private constructor(
* @author OxygenCobalt
*/
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.parentName.text = item.resolveName(binding.context)
binding.parentInfo.text =
@ -133,14 +124,9 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
}
companion object {
val CREATOR =
object : Creator<ArtistViewHolder> {
override val viewType: Int
get() = IntegerTable.ITEM_TYPE_ARTIST
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST
override fun create(context: Context) =
ArtistViewHolder(ItemParentBinding.inflate(context.inflater))
}
fun new(parent: View) = ArtistViewHolder(ItemParentBinding.inflate(parent.context.inflater))
val DIFFER =
object : SimpleItemCallback<Artist>() {
@ -159,9 +145,9 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
class GenreViewHolder
private constructor(
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.parentName.text = item.resolveName(binding.context)
binding.parentInfo.text =
@ -175,14 +161,9 @@ private constructor(
}
companion object {
val CREATOR =
object : Creator<GenreViewHolder> {
override val viewType: Int
get() = IntegerTable.ITEM_TYPE_GENRE
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_GENRE
override fun create(context: Context) =
GenreViewHolder(ItemParentBinding.inflate(context.inflater))
}
fun new(parent: View) = GenreViewHolder(ItemParentBinding.inflate(parent.context.inflater))
val DIFFER =
object : SimpleItemCallback<Genre>() {
@ -197,21 +178,16 @@ private constructor(
* @author OxygenCobalt
*/
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)
}
companion object {
val CREATOR =
object : Creator<HeaderViewHolder> {
override val viewType: Int
get() = IntegerTable.ITEM_TYPE_HEADER
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_HEADER
override fun create(context: Context) =
HeaderViewHolder(ItemHeaderBinding.inflate(context.inflater))
}
fun new(parent: View) = HeaderViewHolder(ItemHeaderBinding.inflate(parent.context.inflater))
val DIFFER =
object : SimpleItemCallback<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
view-binding is still used.
Auxio uses `RecyclerView` for all list information. Due to the complexities of Auxio, the way one
defines an adapter differs quite heavily from the normal library. Generally, start with
`MonoAdapter` for a list with one type of data and `MultiAdapter` for lists with many types of data,
then follow the documentation to see how to fully implement the class.
Auxio uses `RecyclerView` for all list information. To manage some complexity, there are a few
conventions that are used when creating adapters. These can be seen in the `RecyclerFramework`
file and in adapter implementations.
#### Object communication
Auxio's codebase is mostly centered around 4 different types of code that communicates with