Update action header structure

Create a seperate ActionHeader object for headers with actions attached alongside a new viewholder to accompany it.
This allows action headers to be created easier without creating your own viewholder.
This commit is contained in:
OxygenCobalt 2021-04-25 17:15:20 -06:00
parent 029a4b1ff6
commit cc72ebc251
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
8 changed files with 76 additions and 62 deletions

View file

@ -1,6 +1,8 @@
package org.oxycblt.auxio.music package org.oxycblt.auxio.music
import android.net.Uri import android.net.Uri
import android.widget.ImageButton
import androidx.annotation.DrawableRes
// --- MUSIC MODELS --- // --- MUSIC MODELS ---
@ -189,10 +191,20 @@ data class Genre(
/** /**
* A data object used solely for the "Header" UI element. Inherits [BaseModel]. * A data object used solely for the "Header" UI element. Inherits [BaseModel].
* @property isAction Value that marks whether this header should have an action attached to it.
*/ */
data class Header( data class Header(
override val id: Long = -1, override val id: Long = -1,
override val name: String = "", override val name: String = "",
val isAction: Boolean = false ) : BaseModel()
/**
* A data object for a header with an action button. Inherits [BaseModel].
* @property icon The icon ot apply for this header
* @property action The callback that will be called when the action button is clicked.
*/
data class ActionHeader(
override val id: Long = -1,
override val name: String = "",
@DrawableRes val icon: Int,
val action: (button: ImageButton) -> Unit,
) : BaseModel() ) : BaseModel()

View file

@ -16,9 +16,10 @@ import org.oxycblt.auxio.logD
* @author OxygenCobalt * @author OxygenCobalt
* *
* FIXME: Here's a catalog of problems that I already know about with this abomination * FIXME: Here's a catalog of problems that I already know about with this abomination
* - Does not support the album artist tag
* - All loading is done at startup [Not efficent for large libraries, would require massive arch retooling to fix] * - All loading is done at startup [Not efficent for large libraries, would require massive arch retooling to fix]
* - Genre system is a bottleneck [Nothing I can do about it, MediaStore is garbage] * - Does not support the album artist tag [Nothing I can do that doesn't involve rolling my own loader]
* - Genre system is a bottleneck [See Above]
* Blame MediaStore, loading anything on this platform is a nightmare.
*/ */
class MusicLoader(private val context: Context) { class MusicLoader(private val context: Context) {
var genres = mutableListOf<Genre>() var genres = mutableListOf<Genre>()
@ -100,7 +101,7 @@ class MusicLoader(private val context: Context) {
Albums.ARTIST, // 2 Albums.ARTIST, // 2
Albums.FIRST_YEAR, // 4 Albums.FIRST_YEAR, // 4
), ),
"", null, null, null,
Albums.DEFAULT_SORT_ORDER Albums.DEFAULT_SORT_ORDER
) )

View file

@ -3,19 +3,18 @@ package org.oxycblt.auxio.playback.queue
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.view.MotionEvent import android.view.MotionEvent
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.TooltipCompat
import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemActionHeaderBinding import org.oxycblt.auxio.databinding.ItemActionHeaderBinding
import org.oxycblt.auxio.databinding.ItemQueueSongBinding import org.oxycblt.auxio.databinding.ItemQueueSongBinding
import org.oxycblt.auxio.logE import org.oxycblt.auxio.logE
import org.oxycblt.auxio.music.ActionHeader
import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Header import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.recycler.DiffCallback import org.oxycblt.auxio.recycler.DiffCallback
import org.oxycblt.auxio.recycler.viewholders.ActionHeaderViewHolder
import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder
import org.oxycblt.auxio.recycler.viewholders.HeaderViewHolder import org.oxycblt.auxio.recycler.viewholders.HeaderViewHolder
import org.oxycblt.auxio.ui.inflater import org.oxycblt.auxio.ui.inflater
@ -23,12 +22,10 @@ import org.oxycblt.auxio.ui.inflater
/** /**
* The single adapter for both the Next Queue and the User Queue. * The single adapter for both the Next Queue and the User Queue.
* @param touchHelper The [ItemTouchHelper] ***containing*** [QueueDragCallback] to be used * @param touchHelper The [ItemTouchHelper] ***containing*** [QueueDragCallback] to be used
* @param playbackModel The [PlaybackViewModel] to dispatch updates to.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class QueueAdapter( class QueueAdapter(
private val touchHelper: ItemTouchHelper, private val touchHelper: ItemTouchHelper
private val playbackModel: PlaybackViewModel,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var data = mutableListOf<BaseModel>() private var data = mutableListOf<BaseModel>()
private var listDiffer = AsyncListDiffer(this, DiffCallback()) private var listDiffer = AsyncListDiffer(this, DiffCallback())
@ -38,14 +35,11 @@ class QueueAdapter(
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
val item = data[position] val item = data[position]
return if (item is Header) { return when (item) {
if (item.isAction) { is Header -> HeaderViewHolder.ITEM_TYPE
USER_QUEUE_HEADER_ITEM_TYPE is ActionHeader -> ActionHeaderViewHolder.ITEM_TYPE
} else { is Song -> QUEUE_SONG_ITEM_TYPE
HeaderViewHolder.ITEM_TYPE else -> -1
}
} else {
QUEUE_SONG_ITEM_TYPE
} }
} }
@ -53,7 +47,7 @@ class QueueAdapter(
return when (viewType) { return when (viewType) {
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context) HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
USER_QUEUE_HEADER_ITEM_TYPE -> UserQueueHeaderViewHolder( ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder(
ItemActionHeaderBinding.inflate(parent.context.inflater) ItemActionHeaderBinding.inflate(parent.context.inflater)
) )
@ -68,12 +62,8 @@ class QueueAdapter(
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = data[position]) { when (val item = data[position]) {
is Song -> (holder as QueueSongViewHolder).bind(item) is Song -> (holder as QueueSongViewHolder).bind(item)
is Header -> (holder as HeaderViewHolder).bind(item)
is Header -> if (item.isAction) { is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item)
(holder as UserQueueHeaderViewHolder).bind(item)
} else {
(holder as HeaderViewHolder).bind(item)
}
else -> logE("Bad data given to QueueAdapter.") else -> logE("Bad data given to QueueAdapter.")
} }
@ -158,34 +148,7 @@ class QueueAdapter(
} }
} }
/**
* ViewHolder for the **user queue header**. Has the clear queue button.
*/
inner class UserQueueHeaderViewHolder(
private val binding: ItemActionHeaderBinding
) : BaseViewHolder<Header>(binding) {
init {
binding.headerButton.apply {
contentDescription = context.getString(R.string.description_clear_user_queue)
TooltipCompat.setTooltipText(this, contentDescription)
}
}
override fun onBind(data: Header) {
binding.header = data
binding.headerButton.apply {
setImageResource(R.drawable.ic_clear)
setOnClickListener {
playbackModel.clearUserQueue()
}
}
}
}
companion object { companion object {
const val QUEUE_SONG_ITEM_TYPE = 0xA005 const val QUEUE_SONG_ITEM_TYPE = 0xA005
const val USER_QUEUE_HEADER_ITEM_TYPE = 0xA006
} }
} }

View file

@ -13,6 +13,7 @@ import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentQueueBinding import org.oxycblt.auxio.databinding.FragmentQueueBinding
import org.oxycblt.auxio.music.ActionHeader
import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Header import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
@ -36,7 +37,7 @@ class QueueFragment : Fragment() {
val callback = QueueDragCallback(playbackModel) val callback = QueueDragCallback(playbackModel)
val helper = ItemTouchHelper(callback) val helper = ItemTouchHelper(callback)
val queueAdapter = QueueAdapter(helper, playbackModel) val queueAdapter = QueueAdapter(helper)
var lastShuffle = playbackModel.isShuffling.value var lastShuffle = playbackModel.isShuffling.value
callback.addQueueAdapter(queueAdapter) callback.addQueueAdapter(queueAdapter)
@ -136,10 +137,13 @@ class QueueFragment : Fragment() {
val nextQueue = playbackModel.nextItemsInQueue.value!! val nextQueue = playbackModel.nextItemsInQueue.value!!
if (userQueue.isNotEmpty()) { if (userQueue.isNotEmpty()) {
queue += Header( queue += ActionHeader(
id = -2, id = -2,
name = getString(R.string.label_next_user_queue), name = getString(R.string.label_next_user_queue),
isAction = true icon = R.drawable.ic_clear,
action = {
playbackModel.clearUserQueue()
}
) )
queue += userQueue queue += userQueue
@ -147,9 +151,7 @@ class QueueFragment : Fragment() {
if (nextQueue.isNotEmpty()) { if (nextQueue.isNotEmpty()) {
queue += Header( queue += Header(
id = -3, id = -3, name = getString(R.string.format_next_from, getParentName()),
name = getString(R.string.format_next_from, getParentName()),
isAction = false
) )
queue += nextQueue queue += nextQueue

View file

@ -2,11 +2,13 @@ package org.oxycblt.auxio.recycler.viewholders
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import org.oxycblt.auxio.databinding.ItemActionHeaderBinding
import org.oxycblt.auxio.databinding.ItemAlbumBinding import org.oxycblt.auxio.databinding.ItemAlbumBinding
import org.oxycblt.auxio.databinding.ItemArtistBinding import org.oxycblt.auxio.databinding.ItemArtistBinding
import org.oxycblt.auxio.databinding.ItemGenreBinding import org.oxycblt.auxio.databinding.ItemGenreBinding
import org.oxycblt.auxio.databinding.ItemHeaderBinding import org.oxycblt.auxio.databinding.ItemHeaderBinding
import org.oxycblt.auxio.databinding.ItemSongBinding import org.oxycblt.auxio.databinding.ItemSongBinding
import org.oxycblt.auxio.music.ActionHeader
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
@ -170,3 +172,35 @@ class HeaderViewHolder(private val binding: ItemHeaderBinding) : BaseViewHolder<
} }
} }
} }
/**
* The Shared ViewHolder for a [ActionHeader]. Instantiation should be done with [from]
*/
class ActionHeaderViewHolder(
private val binding: ItemActionHeaderBinding
) : BaseViewHolder<ActionHeader>(binding) {
override fun onBind(data: ActionHeader) {
binding.header = data
binding.headerButton.apply {
setImageResource(data.icon)
setOnClickListener {
data.action(binding.headerButton)
}
}
}
companion object {
const val ITEM_TYPE = 0xA006
/**
* Create an instance of [HeaderViewHolder]
*/
fun from(context: Context): HeaderViewHolder {
return HeaderViewHolder(
ItemHeaderBinding.inflate(context.inflater)
)
}
}
}

View file

@ -33,6 +33,8 @@ class SearchAdapter(
is Album -> AlbumViewHolder.ITEM_TYPE is Album -> AlbumViewHolder.ITEM_TYPE
is Song -> SongViewHolder.ITEM_TYPE is Song -> SongViewHolder.ITEM_TYPE
is Header -> HeaderViewHolder.ITEM_TYPE is Header -> HeaderViewHolder.ITEM_TYPE
else -> -1
} }
} }

View file

@ -8,7 +8,7 @@
<variable <variable
name="header" name="header"
type="org.oxycblt.auxio.music.Header" /> type="org.oxycblt.auxio.music.ActionHeader" />
</data> </data>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout

View file

@ -44,7 +44,7 @@ To prevent any strange bugs, all integer representations must be unique. A table
0xA004 | HeaderViewHolder 0xA004 | HeaderViewHolder
0xA005 | QueueSongViewHolder 0xA005 | QueueSongViewHolder
0xA006 | UserQueueHeaderViewHolder 0xA006 | ActionHeaderViewHolder
0xA007 | AlbumHeaderViewHolder 0xA007 | AlbumHeaderViewHolder
0xA008 | AlbumSongViewHolder 0xA008 | AlbumSongViewHolder
@ -89,7 +89,7 @@ org.oxycblt.auxio # Main UI's and logging utilities
├──.coil # Fetchers and utilities for Coil, contains binding adapters than be used in the user interface. ├──.coil # Fetchers and utilities for Coil, contains binding adapters than be used in the user interface.
├──.database # Databases and their items for Auxio ├──.database # Databases and their items for Auxio
├──.detail # UIs for more album/artist/genre details ├──.detail # UIs for more album/artist/genre details
└──.adapters # RecyclerView adapters for the detail UIs, which display the header information and items └──.adapters # RecyclerView adapters for the detail UIs, which display the header information and items
├──.library # Library UI ├──.library # Library UI
├──.loading # Loading UI ├──.loading # Loading UI
├──.music # Music storage and loading ├──.music # Music storage and loading