ui: migrate esoteric adapters to framework

Migrate all esoteric adapters to the new RecyclerView framework.

One of the shortcomings with the previous RecyclerView utilities was
how more esoteric adapters with data that does not implement Item
could not use the utlities. The new system, by comparison, is capable
of taking any type of data, so we can no migrate all of the esoteric
adapters to the new system.
This commit is contained in:
OxygenCobalt 2022-03-27 09:59:35 -06:00
parent 2406c371db
commit 05a5ef5c3f
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
22 changed files with 409 additions and 363 deletions

View file

@ -43,6 +43,8 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
* TODO: Custom language support * TODO: Custom language support
* *
* TODO: Rework menus [perhaps add multi-select] * TODO: Rework menus [perhaps add multi-select]
*
* TODO: Rework navigation to be based on a viewmodel
*/ */
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private val playbackModel: PlaybackViewModel by viewModels() private val playbackModel: PlaybackViewModel by viewModels()

View file

@ -17,71 +17,93 @@
package org.oxycblt.auxio.accent package org.oxycblt.auxio.accent
import android.view.ViewGroup import android.content.Context
import androidx.appcompat.widget.TooltipCompat import androidx.appcompat.widget.TooltipCompat
import androidx.recyclerview.widget.RecyclerView 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.BackingData
import org.oxycblt.auxio.ui.BindingViewHolder
import org.oxycblt.auxio.ui.MonoAdapter
import org.oxycblt.auxio.util.getAttrColorSafe import org.oxycblt.auxio.util.getAttrColorSafe
import org.oxycblt.auxio.util.getColorSafe import org.oxycblt.auxio.util.getColorSafe
import org.oxycblt.auxio.util.getViewHolderAt
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.stateList import org.oxycblt.auxio.util.stateList
/** /** An adapter that displays the accent palette. */
* An adapter that displays the list of all possible accents, and highlights the current one. class AccentAdapter(listener: Listener) :
* @author OxygenCobalt MonoAdapter<Accent, AccentAdapter.Listener, NewAccentViewHolder>(listener) {
* @param onSelect What to do when an accent is selected. var selectedAccent: Accent? = null
*/ private set
class AccentAdapter(private var curAccent: Accent, private val onSelect: (accent: Accent) -> Unit) : private var selectedViewHolder: NewAccentViewHolder? = null
RecyclerView.Adapter<AccentAdapter.ViewHolder>() {
private var selectedViewHolder: ViewHolder? = null
override fun getItemCount(): Int = ACCENT_COUNT override val data = AccentData()
override val creator = NewAccentViewHolder.CREATOR
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onBindViewHolder(viewHolder: NewAccentViewHolder, position: Int) {
return ViewHolder(ItemAccentBinding.inflate(parent.context.inflater)) super.onBindViewHolder(viewHolder, position)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) { if (data.getItem(position) == selectedAccent) {
holder.bind(Accent(position)) selectedViewHolder?.setSelected(false)
} selectedViewHolder = viewHolder
viewHolder.setSelected(true)
private fun setAccent(accent: Accent) {
curAccent = accent
onSelect(accent)
}
inner class ViewHolder(private val binding: ItemAccentBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(accent: Accent) {
setSelected(accent == curAccent)
binding.accent.apply {
backgroundTintList = context.getColorSafe(accent.primary).stateList
contentDescription = context.getString(accent.name)
TooltipCompat.setTooltipText(this, contentDescription)
}
binding.accent.setOnClickListener {
setAccent(accent)
setSelected(true)
}
} }
}
private fun setSelected(isSelected: Boolean) { fun setSelectedAccent(accent: Accent, recycler: RecyclerView) {
val context = binding.accent.context if (accent == selectedAccent) return
selectedAccent = accent
selectedViewHolder?.setSelected(false)
selectedViewHolder = recycler.getViewHolderAt(accent.index) as NewAccentViewHolder?
selectedViewHolder?.setSelected(true)
}
binding.accent.isEnabled = !isSelected interface Listener {
binding.accent.imageTintList = fun onAccentSelected(accent: Accent)
if (isSelected) { }
// Switch out the currently selected ViewHolder with this one.
selectedViewHolder?.setSelected(false) class AccentData : BackingData<Accent>() {
selectedViewHolder = this override fun getItem(position: Int) = Accent(position)
context.getAttrColorSafe(R.attr.colorSurface).stateList override fun getItemCount() = ACCENT_COUNT
} else { }
context.getColorSafe(android.R.color.transparent).stateList }
}
} class NewAccentViewHolder private constructor(private val binding: ItemAccentBinding) :
BindingViewHolder<Accent, AccentAdapter.Listener>(binding.root) {
override fun bind(item: Accent, listener: AccentAdapter.Listener) {
setSelected(false)
binding.accent.apply {
backgroundTintList = context.getColorSafe(item.primary).stateList
contentDescription = context.getString(item.name)
TooltipCompat.setTooltipText(this, contentDescription)
}
binding.accent.setOnClickListener { listener.onAccentSelected(item) }
}
fun setSelected(isSelected: Boolean) {
val context = binding.accent.context
binding.accent.isEnabled = !isSelected
binding.accent.imageTintList =
if (isSelected) {
context.getAttrColorSafe(R.attr.colorSurface).stateList
} else {
context.getColorSafe(android.R.color.transparent).stateList
}
}
companion object {
val CREATOR =
object : Creator<NewAccentViewHolder> {
override val viewType: Int
get() = throw UnsupportedOperationException()
override fun create(context: Context) =
NewAccentViewHolder(ItemAccentBinding.inflate(context.inflater))
}
} }
} }

View file

@ -31,9 +31,10 @@ import org.oxycblt.auxio.util.logD
* Dialog responsible for showing the list of accents to select. * Dialog responsible for showing the list of accents to select.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class AccentCustomizeDialog : ViewBindingDialogFragment<DialogAccentBinding>() { class AccentCustomizeDialog :
ViewBindingDialogFragment<DialogAccentBinding>(), AccentAdapter.Listener {
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
private var pendingAccent = settingsManager.accent private var accentAdapter = AccentAdapter(this)
override fun onCreateBinding(inflater: LayoutInflater) = DialogAccentBinding.inflate(inflater) override fun onCreateBinding(inflater: LayoutInflater) = DialogAccentBinding.inflate(inflater)
@ -41,9 +42,9 @@ class AccentCustomizeDialog : ViewBindingDialogFragment<DialogAccentBinding>() {
builder.setTitle(R.string.set_accent) builder.setTitle(R.string.set_accent)
builder.setPositiveButton(android.R.string.ok) { _, _ -> builder.setPositiveButton(android.R.string.ok) { _, _ ->
if (pendingAccent != settingsManager.accent) { if (accentAdapter.selectedAccent != settingsManager.accent) {
logD("Applying new accent") logD("Applying new accent")
settingsManager.accent = pendingAccent settingsManager.accent = requireNotNull(accentAdapter.selectedAccent)
requireActivity().recreate() requireActivity().recreate()
} }
@ -55,22 +56,30 @@ class AccentCustomizeDialog : ViewBindingDialogFragment<DialogAccentBinding>() {
} }
override fun onBindingCreated(binding: DialogAccentBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: DialogAccentBinding, savedInstanceState: Bundle?) {
savedInstanceState?.getInt(KEY_PENDING_ACCENT)?.let { index -> accentAdapter.setSelectedAccent(
pendingAccent = Accent(index) if (savedInstanceState != null) {
} Accent(savedInstanceState.getInt(KEY_PENDING_ACCENT))
} else {
settingsManager.accent
},
binding.accentRecycler)
// --- UI SETUP --- // --- UI SETUP ---
binding.accentRecycler.adapter = binding.accentRecycler.adapter = accentAdapter
AccentAdapter(pendingAccent) { accent ->
logD("Switching selected accent to $accent")
pendingAccent = accent
}
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putInt(KEY_PENDING_ACCENT, pendingAccent.index) outState.putInt(KEY_PENDING_ACCENT, requireNotNull(accentAdapter.selectedAccent).index)
}
override fun onDestroyBinding(binding: DialogAccentBinding) {
binding.accentRecycler.adapter = null
}
override fun onAccentSelected(accent: Accent) {
accentAdapter.setSelectedAccent(accent, requireBinding().accentRecycler)
} }
companion object { companion object {

View file

@ -22,7 +22,6 @@ import coil.size.Size
import coil.size.pxOrElse import coil.size.pxOrElse
import coil.transform.Transformation import coil.transform.Transformation
import kotlin.math.min import kotlin.math.min
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
/** /**
@ -47,7 +46,6 @@ class SquareFrameTransform : Transformation {
val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize) val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize)
if (dstSize != desiredWidth || dstSize != desiredHeight) { if (dstSize != desiredWidth || dstSize != desiredHeight) {
logD("RETARD YOU STUPID FUCKING IDIOT $desiredWidth $desiredHeight")
try { try {
// Desired size differs from the cropped size, resize the bitmap. // Desired size differs from the cropped size, resize the bitmap.
return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true) return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)

View file

@ -27,7 +27,6 @@ import androidx.recyclerview.widget.LinearSmoothScroller
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
import org.oxycblt.auxio.detail.recycler.AlbumDetailItemListener
import org.oxycblt.auxio.detail.recycler.SortHeader import org.oxycblt.auxio.detail.recycler.SortHeader
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
@ -47,7 +46,7 @@ import org.oxycblt.auxio.util.showToast
* The [DetailFragment] for an album. * The [DetailFragment] for an album.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class AlbumDetailFragment : DetailFragment(), AlbumDetailItemListener { class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
private val args: AlbumDetailFragmentArgs by navArgs() private val args: AlbumDetailFragmentArgs by navArgs()
private val detailAdapter = AlbumDetailAdapter(this) private val detailAdapter = AlbumDetailAdapter(this)

View file

@ -24,7 +24,7 @@ import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
import org.oxycblt.auxio.detail.recycler.DetailItemListener import org.oxycblt.auxio.detail.recycler.DetailAdapter
import org.oxycblt.auxio.detail.recycler.SortHeader import org.oxycblt.auxio.detail.recycler.SortHeader
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
@ -43,7 +43,7 @@ import org.oxycblt.auxio.util.logW
* The [DetailFragment] for an artist. * The [DetailFragment] for an artist.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class ArtistDetailFragment : DetailFragment(), DetailItemListener { class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
private val args: ArtistDetailFragmentArgs by navArgs() private val args: ArtistDetailFragmentArgs by navArgs()
private val detailAdapter = ArtistDetailAdapter(this) private val detailAdapter = ArtistDetailAdapter(this)

View file

@ -22,7 +22,7 @@ import android.view.View
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.DetailItemListener import org.oxycblt.auxio.detail.recycler.DetailAdapter
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
import org.oxycblt.auxio.detail.recycler.SortHeader import org.oxycblt.auxio.detail.recycler.SortHeader
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
@ -42,7 +42,7 @@ import org.oxycblt.auxio.util.logW
* The [DetailFragment] for a genre. * The [DetailFragment] for a genre.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class GenreDetailFragment : DetailFragment(), DetailItemListener { class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
private val args: GenreDetailFragmentArgs by navArgs() private val args: GenreDetailFragmentArgs by navArgs()
private val detailAdapter = GenreDetailAdapter(this) private val detailAdapter = GenreDetailAdapter(this)

View file

@ -40,8 +40,8 @@ import org.oxycblt.auxio.util.textSafe
* 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: AlbumDetailItemListener) : class AlbumDetailAdapter(listener: Listener) :
DetailAdapter<AlbumDetailItemListener>(listener, DIFFER) { DetailAdapter<AlbumDetailAdapter.Listener>(listener, DIFFER) {
private var highlightedSong: Song? = null private var highlightedSong: Song? = null
private var highlightedViewHolder: Highlightable? = null private var highlightedViewHolder: Highlightable? = null
@ -61,11 +61,7 @@ class AlbumDetailAdapter(listener: AlbumDetailItemListener) :
else -> null else -> null
} }
override fun onBind( override fun onBind(viewHolder: RecyclerView.ViewHolder, item: Item, listener: Listener) {
viewHolder: RecyclerView.ViewHolder,
item: Item,
listener: AlbumDetailItemListener
) {
super.onBind(viewHolder, item, listener) super.onBind(viewHolder, item, listener)
when (item) { when (item) {
@ -112,16 +108,16 @@ class AlbumDetailAdapter(listener: AlbumDetailItemListener) :
} }
} }
} }
}
interface AlbumDetailItemListener : DetailItemListener { interface Listener : DetailAdapter.Listener {
fun onNavigateToArtist() fun onNavigateToArtist()
}
} }
private class AlbumDetailViewHolder private constructor(private val binding: ItemDetailBinding) : private class AlbumDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
BindingViewHolder<Album, AlbumDetailItemListener>(binding.root) { BindingViewHolder<Album, AlbumDetailAdapter.Listener>(binding.root) {
override fun bind(item: Album, listener: AlbumDetailItemListener) { override fun bind(item: Album, listener: AlbumDetailAdapter.Listener) {
binding.detailCover.bindAlbumCover(item) binding.detailCover.bindAlbumCover(item)
binding.detailName.textSafe = item.resolvedName binding.detailName.textSafe = item.resolvedName

View file

@ -44,8 +44,8 @@ import org.oxycblt.auxio.util.textSafe
* 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: DetailItemListener) : class ArtistDetailAdapter(listener: Listener) :
DetailAdapter<DetailItemListener>(listener, DIFFER) { DetailAdapter<DetailAdapter.Listener>(listener, DIFFER) {
private var currentAlbum: Album? = null private var currentAlbum: Album? = null
private var currentAlbumHolder: Highlightable? = null private var currentAlbumHolder: Highlightable? = null
@ -70,11 +70,7 @@ class ArtistDetailAdapter(listener: DetailItemListener) :
else -> null else -> null
} }
override fun onBind( override fun onBind(viewHolder: RecyclerView.ViewHolder, item: Item, listener: Listener) {
viewHolder: RecyclerView.ViewHolder,
item: Item,
listener: DetailItemListener
) {
super.onBind(viewHolder, item, listener) super.onBind(viewHolder, item, listener)
when (item) { when (item) {
is Artist -> (viewHolder as ArtistDetailViewHolder).bind(item, listener) is Artist -> (viewHolder as ArtistDetailViewHolder).bind(item, listener)
@ -140,9 +136,9 @@ class ArtistDetailAdapter(listener: DetailItemListener) :
} }
private class ArtistDetailViewHolder private constructor(private val binding: ItemDetailBinding) : private class ArtistDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
BindingViewHolder<Artist, DetailItemListener>(binding.root) { BindingViewHolder<Artist, DetailAdapter.Listener>(binding.root) {
override fun bind(item: Artist, listener: DetailItemListener) { override fun bind(item: Artist, listener: DetailAdapter.Listener) {
binding.detailCover.bindArtistImage(item) binding.detailCover.bindArtistImage(item)
binding.detailName.textSafe = item.resolvedName binding.detailName.textSafe = item.resolvedName

View file

@ -39,7 +39,7 @@ import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.textSafe import org.oxycblt.auxio.util.textSafe
abstract class DetailAdapter<L : DetailItemListener>( abstract class DetailAdapter<L : DetailAdapter.Listener>(
listener: L, listener: L,
diffCallback: DiffUtil.ItemCallback<Item> diffCallback: DiffUtil.ItemCallback<Item>
) : MultiAdapter<L>(listener) { ) : MultiAdapter<L>(listener) {
@ -112,13 +112,19 @@ abstract class DetailAdapter<L : DetailItemListener>(
} }
} }
} }
interface Listener : MenuItemListener {
fun onPlayParent()
fun onShuffleParent()
fun onShowSortMenu(anchor: View)
}
} }
data class SortHeader(override val id: Long, @StringRes val string: Int) : Item() data class SortHeader(override val id: Long, @StringRes val string: Int) : Item()
class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) : class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
BindingViewHolder<SortHeader, DetailItemListener>(binding.root) { BindingViewHolder<SortHeader, DetailAdapter.Listener>(binding.root) {
override fun bind(item: SortHeader, listener: DetailItemListener) { override fun bind(item: SortHeader, listener: DetailAdapter.Listener) {
binding.headerTitle.textSafe = binding.context.getString(item.string) binding.headerTitle.textSafe = binding.context.getString(item.string)
binding.headerButton.apply { binding.headerButton.apply {
TooltipCompat.setTooltipText(this, contentDescription) TooltipCompat.setTooltipText(this, contentDescription)
@ -148,9 +154,3 @@ class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
interface Highlightable { interface Highlightable {
fun setHighlighted(isHighlighted: Boolean) fun setHighlighted(isHighlighted: Boolean)
} }
interface DetailItemListener : MenuItemListener {
fun onPlayParent()
fun onShuffleParent()
fun onShowSortMenu(anchor: View)
}

View file

@ -41,8 +41,8 @@ import org.oxycblt.auxio.util.textSafe
* 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: DetailItemListener) : class GenreDetailAdapter(listener: Listener) :
DetailAdapter<DetailItemListener>(listener, DIFFER) { DetailAdapter<DetailAdapter.Listener>(listener, DIFFER) {
private var currentSong: Song? = null private var currentSong: Song? = null
private var currentHolder: Highlightable? = null private var currentHolder: Highlightable? = null
@ -62,11 +62,7 @@ class GenreDetailAdapter(listener: DetailItemListener) :
else -> null else -> null
} }
override fun onBind( override fun onBind(viewHolder: RecyclerView.ViewHolder, item: Item, listener: Listener) {
viewHolder: RecyclerView.ViewHolder,
item: Item,
listener: DetailItemListener
) {
super.onBind(viewHolder, item, listener) super.onBind(viewHolder, item, listener)
when (item) { when (item) {
is Genre -> (viewHolder as GenreDetailViewHolder).bind(item, listener) is Genre -> (viewHolder as GenreDetailViewHolder).bind(item, listener)
@ -115,8 +111,8 @@ class GenreDetailAdapter(listener: DetailItemListener) :
} }
private class GenreDetailViewHolder private constructor(private val binding: ItemDetailBinding) : private class GenreDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
BindingViewHolder<Genre, DetailItemListener>(binding.root) { BindingViewHolder<Genre, DetailAdapter.Listener>(binding.root) {
override fun bind(item: Genre, listener: DetailItemListener) { override fun bind(item: Genre, listener: DetailAdapter.Listener) {
binding.detailCover.bindGenreImage(item) binding.detailCover.bindGenreImage(item)
binding.detailName.textSafe = item.resolvedName binding.detailName.textSafe = item.resolvedName
binding.detailSubhead.textSafe = binding.detailSubhead.textSafe =

View file

@ -53,6 +53,16 @@ sealed class Tab(open val mode: DisplayMode) {
/** The default tab sequence, represented in integer form */ /** The default tab sequence, represented in integer form */
const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100 const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100
/**
* Maps between the integer code in the tab sequence and the actual [DisplayMode] instance
*/
private val MODE_TABLE =
arrayOf(
DisplayMode.SHOW_SONGS,
DisplayMode.SHOW_ALBUMS,
DisplayMode.SHOW_ARTISTS,
DisplayMode.SHOW_GENRES)
/** Convert an array [tabs] into a sequence of tabs. */ /** Convert an array [tabs] into a sequence of tabs. */
fun toSequence(tabs: Array<Tab>): Int { fun toSequence(tabs: Array<Tab>): Int {
// Like when deserializing, make sure there are no duplicate tabs for whatever reason. // Like when deserializing, make sure there are no duplicate tabs for whatever reason.
@ -64,8 +74,8 @@ sealed class Tab(open val mode: DisplayMode) {
for (tab in distinct) { for (tab in distinct) {
val bin = val bin =
when (tab) { when (tab) {
is Visible -> 1.shl(3) or tab.mode.ordinal is Visible -> 1.shl(3) or MODE_TABLE.indexOf(tab.mode)
is Invisible -> tab.mode.ordinal is Invisible -> MODE_TABLE.indexOf(tab.mode)
} }
sequence = sequence or bin.shl(shift) sequence = sequence or bin.shl(shift)
@ -84,14 +94,7 @@ sealed class Tab(open val mode: DisplayMode) {
for (shift in (0..4 * SEQUENCE_LEN).reversed() step 4) { for (shift in (0..4 * SEQUENCE_LEN).reversed() step 4) {
val chunk = sequence.shr(shift) and 0b1111 val chunk = sequence.shr(shift) and 0b1111
val mode = val mode = MODE_TABLE.getOrNull(chunk and 7) ?: continue
when (chunk and 7) {
0 -> DisplayMode.SHOW_SONGS
1 -> DisplayMode.SHOW_ALBUMS
2 -> DisplayMode.SHOW_ARTISTS
3 -> DisplayMode.SHOW_GENRES
else -> continue
}
// Figure out the visibility // Figure out the visibility
tabs += tabs +=

View file

@ -18,70 +18,92 @@
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.ViewGroup
import androidx.recyclerview.widget.ItemTouchHelper
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.BackingData
import org.oxycblt.auxio.ui.BindingViewHolder
import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.MonoAdapter
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
class TabAdapter( class TabAdapter(listener: Listener) :
private val touchHelper: ItemTouchHelper, MonoAdapter<Tab, TabAdapter.Listener, TabViewHolder>(listener) {
private val getTabs: () -> Array<Tab>, override val data = TabData(this)
private val onTabSwitch: (Tab) -> Unit, override val creator = TabViewHolder.CREATOR
) : RecyclerView.Adapter<TabAdapter.TabViewHolder>() {
private val tabs: Array<Tab>
get() = getTabs()
override fun getItemCount(): Int = Tab.SEQUENCE_LEN interface Listener {
fun onVisibilityToggled(displayMode: DisplayMode)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabViewHolder { fun onPickUpTab(viewHolder: RecyclerView.ViewHolder)
return TabViewHolder(ItemTabBinding.inflate(parent.context.inflater))
} }
override fun onBindViewHolder(holder: TabViewHolder, position: Int) { class TabData(private val adapter: RecyclerView.Adapter<*>) : BackingData<Tab>() {
holder.bind(tabs[position]) var tabs = arrayOf<Tab>()
} private set
inner class TabViewHolder(private val binding: ItemTabBinding) : override fun getItem(position: Int) = tabs[position]
RecyclerView.ViewHolder(binding.root) { override fun getItemCount() = tabs.size
init {
binding.root.layoutParams = @Suppress("NotifyDatasetChanged")
RecyclerView.LayoutParams( fun submitTabs(newTabs: Array<Tab>) {
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) tabs = newTabs
adapter.notifyDataSetChanged()
} }
@SuppressLint("ClickableViewAccessibility") fun setTab(at: Int, tab: Tab) {
fun bind(tab: Tab) { tabs[at] = tab
binding.root.apply { }
setOnClickListener {
// Don't do a typical notifyDataSetChanged call here, because
// A. We don't have a real ViewModel state since this is a dialog
// B. Doing so would cause a relayout and the ripple effect to disappear
// Instead, simply notify a tab change and let TabCustomizeDialog handle it.
binding.tabIcon.isChecked = !binding.tabIcon.isChecked
onTabSwitch(tab)
}
}
binding.tabIcon.apply { fun moveItems(from: Int, to: Int) {
setText(tab.mode.string) val t = tabs[to]
isChecked = tab is Tab.Visible val f = tabs[from]
} tabs[from] = t
tabs[to] = f
// Roll our own drag handlers as the default ones suck adapter.notifyItemMoved(from, to)
binding.tabDragHandle.setOnTouchListener { _, motionEvent ->
binding.tabDragHandle.performClick()
if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
touchHelper.startDrag(this)
true
} else false
}
binding.root.setOnLongClickListener {
touchHelper.startDrag(this)
true
}
} }
} }
} }
class TabViewHolder private constructor(private val binding: ItemTabBinding) :
BindingViewHolder<Tab, TabAdapter.Listener>(binding.root) {
@SuppressLint("ClickableViewAccessibility")
override fun bind(item: Tab, listener: TabAdapter.Listener) {
binding.root.apply {
setOnClickListener {
binding.tabIcon.isChecked = !binding.tabIcon.isChecked
listener.onVisibilityToggled(item.mode)
}
}
binding.tabIcon.apply {
setText(item.mode.string)
isChecked = item is Tab.Visible
}
// Roll our own drag handlers as the default ones suck
binding.tabDragHandle.setOnTouchListener { _, motionEvent ->
binding.tabDragHandle.performClick()
if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
listener.onPickUpTab(this)
true
} else false
}
binding.root.setOnLongClickListener {
listener.onPickUpTab(this)
true
}
}
companion object {
val CREATOR =
object : Creator<TabViewHolder> {
override val viewType: Int
get() = throw UnsupportedOperationException()
override fun create(context: Context) =
TabViewHolder(ItemTabBinding.inflate(context.inflater))
}
}
}

View file

@ -21,21 +21,26 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogTabsBinding import org.oxycblt.auxio.databinding.DialogTabsBinding
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.requireAttached
/** /**
* The dialog for customizing library tabs. This dialog does not rely on any specific ViewModel and * The dialog for customizing library tabs. This dialog does not rely on any specific ViewModel and
* serializes it's state instead of * serializes it's state instead of
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>() { class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAdapter.Listener {
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
private var pendingTabs = settingsManager.libTabs private val tabAdapter = TabAdapter(this)
private var touchHelper: ItemTouchHelper? = null
private var callback: TabDragCallback? = null
override fun onCreateBinding(inflater: LayoutInflater) = DialogTabsBinding.inflate(inflater) override fun onCreateBinding(inflater: LayoutInflater) = DialogTabsBinding.inflate(inflater)
@ -44,7 +49,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>() {
builder.setPositiveButton(android.R.string.ok) { _, _ -> builder.setPositiveButton(android.R.string.ok) { _, _ ->
logD("Committing tab changes") logD("Committing tab changes")
settingsManager.libTabs = pendingTabs settingsManager.libTabs = tabAdapter.data.tabs
} }
// Negative button just dismisses, no need for a listener. // Negative button just dismisses, no need for a listener.
@ -52,49 +57,73 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>() {
} }
override fun onBindingCreated(binding: DialogTabsBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: DialogTabsBinding, savedInstanceState: Bundle?) {
if (savedInstanceState != null) { val savedTabs = findSavedTabState(savedInstanceState)
// Restore any pending tab configurations if (savedTabs != null) {
val tabs = Tab.fromSequence(savedInstanceState.getInt(KEY_TABS)) logD("Found saved tab state")
if (tabs != null) { tabAdapter.data.submitTabs(savedTabs)
pendingTabs = tabs } else {
} tabAdapter.data.submitTabs(settingsManager.libTabs)
} }
// Set up adapter & drag callback
val callback = TabDragCallback { pendingTabs }
val helper = ItemTouchHelper(callback)
val tabAdapter = TabAdapter(helper, getTabs = { pendingTabs }, onTabSwitch = ::moveTabs)
callback.addTabAdapter(tabAdapter)
binding.tabRecycler.apply { binding.tabRecycler.apply {
adapter = tabAdapter adapter = tabAdapter
helper.attachToRecyclerView(this) requireTouchHelper().attachToRecyclerView(this)
} }
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putInt(KEY_TABS, Tab.toSequence(pendingTabs)) outState.putInt(KEY_TABS, Tab.toSequence(tabAdapter.data.tabs))
} }
private fun moveTabs(tab: Tab) { override fun onDestroyBinding(binding: DialogTabsBinding) {
// Don't find the specific tab [Which might be outdated due to the nature super.onDestroyBinding(binding)
// of how ViewHolders are bound], but instead simply look for the mode in binding.tabRecycler.adapter = null
// the list of pending tabs and update that instead. }
val index = pendingTabs.indexOfFirst { it.mode == tab.mode }
if (index != -1) { override fun onVisibilityToggled(displayMode: DisplayMode) {
val curTab = pendingTabs[index] // Tab viewholders bind with the initial tab state, which will drift from the actual
logD("Updating tab $curTab to $tab") // state of the tabs over editing. So, this callback simply provides the displayMode
pendingTabs[index] = // for us to locate within the data and then update.
when (curTab) { val index = tabAdapter.data.tabs.indexOfFirst { it.mode == displayMode }
is Tab.Visible -> Tab.Invisible(curTab.mode) if (index > -1) {
is Tab.Invisible -> Tab.Visible(curTab.mode) val tab = tabAdapter.data.tabs[index]
} tabAdapter.data.setTab(
index,
when (tab) {
is Tab.Visible -> Tab.Invisible(tab.mode)
is Tab.Invisible -> Tab.Visible(tab.mode)
})
} }
(requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = (requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled =
pendingTabs.filterIsInstance<Tab.Visible>().isNotEmpty() tabAdapter.data.tabs.filterIsInstance<Tab.Visible>().isNotEmpty()
}
override fun onPickUpTab(viewHolder: RecyclerView.ViewHolder) {
requireTouchHelper().startDrag(viewHolder)
}
private fun findSavedTabState(savedInstanceState: Bundle?): Array<Tab>? {
if (savedInstanceState != null) {
// Restore any pending tab configurations
return Tab.fromSequence(savedInstanceState.getInt(KEY_TABS))
}
return null
}
private fun requireTouchHelper(): ItemTouchHelper {
requireAttached()
val instance = touchHelper
if (instance != null) {
return instance
}
val newCallback = TabDragCallback(tabAdapter)
val newInstance = ItemTouchHelper(newCallback)
callback = newCallback
touchHelper = newInstance
return newInstance
} }
companion object { companion object {

View file

@ -28,11 +28,7 @@ import androidx.recyclerview.widget.RecyclerView
* TODO: Consider unifying the shared behavior between this and QueueDragCallback into a single * TODO: Consider unifying the shared behavior between this and QueueDragCallback into a single
* class. * class.
*/ */
class TabDragCallback(private val getTabs: () -> Array<Tab>) : ItemTouchHelper.Callback() { class TabDragCallback(private val adapter: TabAdapter) : ItemTouchHelper.Callback() {
private val tabs: Array<Tab>
get() = getTabs()
private lateinit var tabAdapter: TabAdapter
override fun getMovementFlags( override fun getMovementFlags(
recyclerView: RecyclerView, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder viewHolder: RecyclerView.ViewHolder
@ -63,8 +59,7 @@ class TabDragCallback(private val getTabs: () -> Array<Tab>) : ItemTouchHelper.C
viewHolder: RecyclerView.ViewHolder, viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder target: RecyclerView.ViewHolder
): Boolean { ): Boolean {
tabs.swap(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition) adapter.data.moveItems(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
tabAdapter.notifyItemMoved(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
return true return true
} }
@ -72,20 +67,4 @@ class TabDragCallback(private val getTabs: () -> Array<Tab>) : ItemTouchHelper.C
// We use a custom drag handle, so disable the long press action. // We use a custom drag handle, so disable the long press action.
override fun isLongPressDragEnabled(): Boolean = false override fun isLongPressDragEnabled(): Boolean = false
/**
* Add the tab adapter to this callback. Done because there's a circular dependency between the
* two objects
*/
fun addTabAdapter(adapter: TabAdapter) {
tabAdapter = adapter
}
private fun <T : Any> Array<T>.swap(from: Int, to: Int) {
val t = get(to)
val f = get(from)
set(from, t)
set(to, f)
}
} }

View file

@ -40,12 +40,14 @@ import org.oxycblt.auxio.util.showToast
* Dialog that manages the currently excluded directories. * Dialog that manages the currently excluded directories.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class ExcludedDialog : ViewBindingDialogFragment<DialogExcludedBinding>() { class ExcludedDialog :
ViewBindingDialogFragment<DialogExcludedBinding>(), ExcludedAdapter.Listener {
private val excludedModel: ExcludedViewModel by viewModels { private val excludedModel: ExcludedViewModel by viewModels {
ExcludedViewModel.Factory(requireContext()) ExcludedViewModel.Factory(requireContext())
} }
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
private val excludedAdapter = ExcludedAdapter(this)
override fun onCreateBinding(inflater: LayoutInflater) = DialogExcludedBinding.inflate(inflater) override fun onCreateBinding(inflater: LayoutInflater) = DialogExcludedBinding.inflate(inflater)
@ -59,11 +61,10 @@ class ExcludedDialog : ViewBindingDialogFragment<DialogExcludedBinding>() {
} }
override fun onBindingCreated(binding: DialogExcludedBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: DialogExcludedBinding, savedInstanceState: Bundle?) {
val adapter = ExcludedEntryAdapter { path -> excludedModel.removePath(path) }
val launcher = val launcher =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), ::addDocTreePath) registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), ::addDocTreePath)
binding.excludedRecycler.adapter = adapter binding.excludedRecycler.adapter = excludedAdapter
// Now that the dialog exists, we get the view manually when the dialog is shown // Now that the dialog exists, we get the view manually when the dialog is shown
// and override its click listener so that the dialog does not auto-dismiss when we // and override its click listener so that the dialog does not auto-dismiss when we
@ -90,13 +91,22 @@ class ExcludedDialog : ViewBindingDialogFragment<DialogExcludedBinding>() {
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
excludedModel.paths.observe(viewLifecycleOwner) { paths -> updatePaths(paths, adapter) } excludedModel.paths.observe(viewLifecycleOwner, ::updatePaths)
logD("Dialog created") logD("Dialog created")
} }
private fun updatePaths(paths: MutableList<String>, adapter: ExcludedEntryAdapter) { override fun onDestroyBinding(binding: DialogExcludedBinding) {
adapter.submitList(paths) super.onDestroyBinding(binding)
binding.excludedRecycler.adapter = null
}
override fun onRemovePath(path: String) {
excludedModel.removePath(path)
}
private fun updatePaths(paths: MutableList<String>) {
excludedAdapter.data.submitList(paths)
requireBinding().excludedEmpty.isVisible = paths.isEmpty() requireBinding().excludedEmpty.isVisible = paths.isEmpty()
} }

View file

@ -17,10 +17,12 @@
package org.oxycblt.auxio.music.excluded package org.oxycblt.auxio.music.excluded
import android.annotation.SuppressLint import android.content.Context
import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemExcludedDirBinding import org.oxycblt.auxio.databinding.ItemExcludedDirBinding
import org.oxycblt.auxio.ui.BindingViewHolder
import org.oxycblt.auxio.ui.MonoAdapter
import org.oxycblt.auxio.ui.PrimitiveBackingData
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.textSafe import org.oxycblt.auxio.util.textSafe
@ -28,37 +30,34 @@ import org.oxycblt.auxio.util.textSafe
* Adapter that shows the excluded directories and their "Clear" button. * Adapter that shows the excluded directories and their "Clear" button.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class ExcludedEntryAdapter(private val onClear: (String) -> Unit) : class ExcludedAdapter(listener: Listener) :
RecyclerView.Adapter<ExcludedEntryAdapter.ViewHolder>() { MonoAdapter<String, ExcludedAdapter.Listener, ExcludedViewHolder>(listener) {
private var paths = mutableListOf<String>() override val data = PrimitiveBackingData<String>(this)
override val creator = ExcludedViewHolder.CREATOR
override fun getItemCount() = paths.size interface Listener {
fun onRemovePath(path: String)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { }
return ViewHolder(ItemExcludedDirBinding.inflate(parent.context.inflater)) }
}
/**
override fun onBindViewHolder(holder: ViewHolder, position: Int) { * The viewholder for [ExcludedAdapter]. Not intended for use in other adapters.
holder.bind(paths[position]) */
} class ExcludedViewHolder private constructor(private val binding: ItemExcludedDirBinding) :
BindingViewHolder<String, ExcludedAdapter.Listener>(binding.root) {
@SuppressLint("NotifyDataSetChanged") override fun bind(item: String, listener: ExcludedAdapter.Listener) {
fun submitList(newPaths: MutableList<String>) { binding.excludedPath.textSafe = item
paths = newPaths binding.excludedClear.setOnClickListener { listener.onRemovePath(item) }
notifyDataSetChanged() }
}
companion object {
inner class ViewHolder(private val binding: ItemExcludedDirBinding) : val CREATOR =
RecyclerView.ViewHolder(binding.root) { object : Creator<ExcludedViewHolder> {
init { override val viewType: Int
binding.root.layoutParams = get() = throw UnsupportedOperationException()
RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) override fun create(context: Context) =
} ExcludedViewHolder(ItemExcludedDirBinding.inflate(context.inflater))
}
fun bind(path: String) {
binding.excludedPath.textSafe = path
binding.excludedClear.setOnClickListener { onClear(path) }
}
} }
} }

View file

@ -40,11 +40,8 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
val paths: LiveData<MutableList<String>> val paths: LiveData<MutableList<String>>
get() = mPaths get() = mPaths
private var dbPaths = listOf<String>() var isModified: Boolean = false
private set
/** Check if changes have been made to the ViewModel's paths. */
val isModified: Boolean
get() = dbPaths != paths.value
init { init {
loadDatabasePaths() loadDatabasePaths()
@ -58,6 +55,7 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
if (!mPaths.value!!.contains(path)) { if (!mPaths.value!!.contains(path)) {
mPaths.value!!.add(path) mPaths.value!!.add(path)
mPaths.value = mPaths.value mPaths.value = mPaths.value
isModified = true
} }
} }
@ -68,6 +66,7 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
fun removePath(path: String) { fun removePath(path: String) {
mPaths.value!!.remove(path) mPaths.value!!.remove(path)
mPaths.value = mPaths.value mPaths.value = mPaths.value
isModified = true
} }
/** Save the pending paths to the database. [onDone] will be called on completion. */ /** Save the pending paths to the database. [onDone] will be called on completion. */
@ -75,7 +74,7 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
excludedDatabase.writePaths(mPaths.value!!) excludedDatabase.writePaths(mPaths.value!!)
dbPaths = mPaths.value!! isModified = false
onDone() onDone()
this@ExcludedViewModel.logD( this@ExcludedViewModel.logD(
"Path save completed successfully in ${System.currentTimeMillis() - start}ms") "Path save completed successfully in ${System.currentTimeMillis() - start}ms")
@ -86,8 +85,11 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
private fun loadDatabasePaths() { private fun loadDatabasePaths() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
dbPaths = excludedDatabase.readPaths() isModified = false
val dbPaths = excludedDatabase.readPaths()
withContext(Dispatchers.Main) { mPaths.value = dbPaths.toMutableList() } withContext(Dispatchers.Main) { mPaths.value = dbPaths.toMutableList() }
this@ExcludedViewModel.logD( this@ExcludedViewModel.logD(
"Path load completed successfully in ${System.currentTimeMillis() - start}ms") "Path load completed successfully in ${System.currentTimeMillis() - start}ms")
} }

View file

@ -23,14 +23,16 @@ import android.graphics.drawable.ColorDrawable
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
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.IntegerTable
import org.oxycblt.auxio.coil.bindAlbumCover import org.oxycblt.auxio.coil.bindAlbumCover
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.BackingData
import org.oxycblt.auxio.ui.BindingViewHolder import org.oxycblt.auxio.ui.BindingViewHolder
import org.oxycblt.auxio.ui.HybridBackingData
import org.oxycblt.auxio.ui.MonoAdapter import org.oxycblt.auxio.ui.MonoAdapter
import org.oxycblt.auxio.ui.SongViewHolder import org.oxycblt.auxio.ui.SongViewHolder
import org.oxycblt.auxio.util.disableDropShadowCompat import org.oxycblt.auxio.util.disableDropShadowCompat
@ -105,3 +107,67 @@ private constructor(
val DIFFER = SongViewHolder.DIFFER val DIFFER = SongViewHolder.DIFFER
} }
} }
/**
* A list-backed [BackingData] that can be modified with both adapter primitives and
* [AsyncListDiffer]. This is incredibly dangerous and can probably crash the app if you look at it
* the wrong way, so please don't use it outside of the queue module.
*/
class HybridBackingData<T>(
private val adapter: RecyclerView.Adapter<*>,
diffCallback: DiffUtil.ItemCallback<T>
) : BackingData<T>() {
private var mCurrentList = mutableListOf<T>()
val currentList: List<T>
get() = mCurrentList
private val differ = AsyncListDiffer(adapter, diffCallback)
override fun getItem(position: Int): T = mCurrentList[position]
override fun getItemCount(): Int = mCurrentList.size
fun submitList(newData: List<T>, onDone: () -> Unit = {}) {
if (newData != mCurrentList) {
mCurrentList = newData.toMutableList()
differ.submitList(newData, onDone)
}
}
fun moveItems(from: Int, to: Int) {
mCurrentList.add(to, mCurrentList.removeAt(from))
differ.rewriteListUnsafe(mCurrentList)
adapter.notifyItemMoved(from, to)
}
fun removeItem(at: Int) {
mCurrentList.removeAt(at)
differ.rewriteListUnsafe(mCurrentList)
adapter.notifyItemRemoved(at)
}
/**
* Rewrites the AsyncListDiffer's internal list, cancelling any diffs that are currently in
* progress. I cannot describe in words how dangerous this is, but it's also the only thing I
* can do to marry the adapter primitives with DiffUtil.
*/
private fun <T> AsyncListDiffer<T>.rewriteListUnsafe(newList: List<T>) {
differMaxGenerationsField.set(this, (differMaxGenerationsField.get(this) as Int).inc())
differListField.set(this, newList.toMutableList())
differImmutableListField.set(this, newList)
}
companion object {
private val differListField =
AsyncListDiffer::class.java.getDeclaredField("mList").apply { isAccessible = true }
private val differImmutableListField =
AsyncListDiffer::class.java.getDeclaredField("mReadOnlyList").apply {
isAccessible = true
}
private val differMaxGenerationsField =
AsyncListDiffer::class.java.getDeclaredField("mMaxScheduledGeneration").apply {
isAccessible = true
}
}
}

View file

@ -178,15 +178,6 @@ class PrimitiveBackingData<T>(private val adapter: RecyclerView.Adapter<*>) : Ba
mCurrentList = newList.toMutableList() mCurrentList = newList.toMutableList()
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }
/**
* Move an item from [from] to [to]. This calls [RecyclerView.Adapter.notifyItemMoved]
* internally.
*/
fun moveItems(from: Int, to: Int) {
mCurrentList.add(to, mCurrentList.removeAt(from))
adapter.notifyItemMoved(from, to)
}
} }
/** /**
@ -215,80 +206,6 @@ class AsyncBackingData<T>(
} }
} }
/**
* A list-backed [BackingData] that can be modified with both adapter primitives and
* [AsyncListDiffer]. Never use this class unless absolutely necessary, such as when dealing with
* item dragging. This is mostly because the class is a terrible hacky mess that could easily crash
* the app if you are not careful with it. You have been warned.
*/
class HybridBackingData<T>(
private val adapter: RecyclerView.Adapter<*>,
diffCallback: DiffUtil.ItemCallback<T>
) : BackingData<T>() {
private var mCurrentList = mutableListOf<T>()
val currentList: List<T>
get() = mCurrentList
private val differ = AsyncListDiffer(adapter, diffCallback)
override fun getItem(position: Int): T = mCurrentList[position]
override fun getItemCount(): Int = mCurrentList.size
fun submitList(newData: List<T>, onDone: () -> Unit = {}) {
if (newData != mCurrentList) {
mCurrentList = newData.toMutableList()
differ.submitList(newData, onDone)
}
}
// @Suppress("NotifyDatasetChanged")
// fun submitListHard(newList: List<T>) {
// if (newList != mCurrentList) {
// mCurrentList = newList.toMutableList()
// differ.rewriteListUnsafe(mCurrentList)
// adapter.notifyDataSetChanged()
// }
// }
fun moveItems(from: Int, to: Int) {
mCurrentList.add(to, mCurrentList.removeAt(from))
differ.rewriteListUnsafe(mCurrentList)
adapter.notifyItemMoved(from, to)
}
fun removeItem(at: Int) {
mCurrentList.removeAt(at)
differ.rewriteListUnsafe(mCurrentList)
adapter.notifyItemRemoved(at)
}
/**
* Rewrites the AsyncListDiffer's internal list, cancelling any diffs that are currently in
* progress. I cannot describe in words how dangerous this is, but it's also the only thing I
* can do to marry the adapter primitives with DiffUtil.
*/
private fun <T> AsyncListDiffer<T>.rewriteListUnsafe(newList: List<T>) {
differMaxGenerationsField.set(this, (differMaxGenerationsField.get(this) as Int).inc())
differListField.set(this, newList.toMutableList())
differImmutableListField.set(this, newList)
}
companion object {
private val differListField =
AsyncListDiffer::class.java.getDeclaredField("mList").apply { isAccessible = true }
private val differImmutableListField =
AsyncListDiffer::class.java.getDeclaredField("mReadOnlyList").apply {
isAccessible = true
}
private val differMaxGenerationsField =
AsyncListDiffer::class.java.getDeclaredField("mMaxScheduledGeneration").apply {
isAccessible = true
}
}
}
/** /**
* A base [DiffUtil.ItemCallback] that automatically provides an implementation of * A base [DiffUtil.ItemCallback] that automatically provides an implementation of
* [areContentsTheSame] any object that is derived from [Item]. * [areContentsTheSame] any object that is derived from [Item].

View file

@ -122,7 +122,7 @@ class WidgetProvider : AppWidgetProvider() {
.size(min(metrics.widthPixels, metrics.heightPixels)) .size(min(metrics.widthPixels, metrics.heightPixels))
} else { } else {
// Note: Explicitly use the "original" size as without it the scaling logic // Note: Explicitly use the "original" size as without it the scaling logic
// in SquareFrameTransform breaks down and results in an error. // in coil breaks down and results in an error.
coverRequest.transformations(SquareFrameTransform()).size(Size.ORIGINAL) coverRequest.transformations(SquareFrameTransform()).size(Size.ORIGINAL)
} }

View file

@ -58,9 +58,10 @@ the binding being obtained by calling `requireBinding`.
At times it may be more appropriate to use a `View` instead of a full blown fragment. This is okay as long as At times it may be more appropriate to use a `View` instead of a full blown fragment. This is okay as long as
view-binding is still used. view-binding is still used.
When creating a ViewHolder for a `RecyclerView`, one should use `BaseViewHolder` to standardize the binding process Auxio uses `RecyclerView` for all list information. Due to the complexities of Auxio, the way one defines an
and automate some code shared across all ViewHolders. The only exceptions to this case are for ViewHolders that adapter differs quite heavily from the normal library. Generally, start with `MonoAdapter` for a list with one
correspond to non-`BaseModel` data, in which a normal ViewHolder can be used instead. type of data and `MultiAdapter` for lists with many types of data, 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 each-other. Auxio's codebase is mostly centered around 4 different types of code that communicates with each-other.