Unify Queue

Make QueueFragment contain both the user queue and the next queue, instead of having viewpager between the two.
This commit is contained in:
OxygenCobalt 2020-11-09 15:39:13 -07:00
parent b5552411b6
commit 67d10009d4
17 changed files with 189 additions and 302 deletions

View file

@ -120,8 +120,6 @@ class AlbumDetailFragment : Fragment() {
binding.albumArtist.setBackgroundResource(R.drawable.ui_ripple)
}
// TODO: Make DetailFragment scroll to song if navigated from CompactPlaybackFragment
Log.d(this::class.simpleName, "Fragment created.")
return binding.root

View file

@ -85,7 +85,6 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
true
}
// TODO: Add icons to overflow menu items?
menu.apply {
val item = findItem(R.id.action_search)
val searchView = item.actionView as SearchView
@ -100,11 +99,6 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
item.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
// When opened, update the adapter to the SearchAdapter, and make the
// sorting group invisible. The query is also reset, as if the Auxio process
// is killed in the background while still on the search adapter, then the
// search query will stick around if its opened again
// TODO: Couldn't you just try to restore the search state on restart?
binding.libraryRecycler.adapter = searchAdapter
setGroupVisible(R.id.group_sorting, false)
libraryModel.resetQuery()
@ -113,8 +107,6 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
// When closed, make the sorting icon visible again, change back to
// LibraryAdapter, and reset the query.
binding.libraryRecycler.adapter = libraryAdapter
setGroupVisible(R.id.group_sorting, true)
libraryModel.resetQuery()
@ -188,14 +180,12 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean = false
override fun onQueryTextChange(query: String): Boolean {
libraryModel.updateSearchQuery(query)
libraryModel.updateSearchQuery(query, requireContext())
return false
}
private fun navToItem(baseModel: BaseModel) {
// TODO: Implement shared element transitions to the DetailFragments [If possible]
// If the item is a song [That was selected through search], then update the playback
// to that song instead of doing any navigation
if (baseModel is Song) {

View file

@ -1,5 +1,6 @@
package org.oxycblt.auxio.library
import android.content.Context
import android.view.MenuItem
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
@ -44,7 +45,7 @@ class LibraryViewModel : ViewModel() {
}
}
fun updateSearchQuery(query: String) {
fun updateSearchQuery(query: String, context: Context) {
// Don't bother if the query is blank.
if (query == "") {
resetQuery()
@ -65,7 +66,7 @@ class LibraryViewModel : ViewModel() {
val genres = musicStore.genres.filter { it.name.contains(query, true) }
if (genres.isNotEmpty()) {
combined.add(Header(id = ShowMode.SHOW_GENRES.constant))
combined.add(Header(name = context.getString(R.string.label_genres)))
combined.addAll(genres)
}
}
@ -74,7 +75,7 @@ class LibraryViewModel : ViewModel() {
val artists = musicStore.artists.filter { it.name.contains(query, true) }
if (artists.isNotEmpty()) {
combined.add(Header(id = ShowMode.SHOW_ARTISTS.constant))
combined.add(Header(name = context.getString(R.string.label_artists)))
combined.addAll(artists)
}
}
@ -83,14 +84,14 @@ class LibraryViewModel : ViewModel() {
val albums = musicStore.albums.filter { it.name.contains(query, true) }
if (albums.isNotEmpty()) {
combined.add(Header(id = ShowMode.SHOW_ALBUMS.constant))
combined.add(Header(name = context.getString(R.string.label_albums)))
combined.addAll(albums)
}
val songs = musicStore.songs.filter { it.name.contains(query, true) }
if (songs.isNotEmpty()) {
combined.add(Header(id = ShowMode.SHOW_SONGS.constant))
combined.add(Header(name = context.getString(R.string.label_songs)))
combined.addAll(songs)
}

View file

@ -8,7 +8,6 @@ import android.text.format.DateUtils
import android.widget.TextView
import androidx.databinding.BindingAdapter
import org.oxycblt.auxio.R
import org.oxycblt.auxio.recycler.ShowMode
// List of ID3 genres + Winamp extensions, each index corresponds to their int value.
// There are a lot more int-genre extensions as far as Im aware, but this works for most cases.
@ -158,18 +157,3 @@ fun TextView.bindAlbumInfo(album: Album) {
fun TextView.bindAlbumYear(album: Album) {
text = album.year.toYear(context)
}
// Bind the text used by the header item
@BindingAdapter("headerText")
fun TextView.bindHeaderText(header: Header) {
text = context.getString(
when (header.id) {
ShowMode.SHOW_GENRES.constant -> R.string.label_genres
ShowMode.SHOW_ARTISTS.constant -> R.string.label_artists
ShowMode.SHOW_ALBUMS.constant -> R.string.label_albums
ShowMode.SHOW_SONGS.constant -> R.string.label_songs
else -> R.string.label_artists
}
)
}

View file

@ -9,7 +9,6 @@ import android.content.Intent
import android.content.IntentFilter
import android.content.pm.ServiceInfo
import android.media.AudioManager
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.os.Parcelable
@ -71,7 +70,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
return START_NOT_STICKY
}
override fun onBind(intent: Intent): IBinder? = LocalBinder()
override fun onBind(intent: Intent): IBinder? = null
override fun onCreate() {
super.onCreate()
@ -295,9 +294,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
}
}
// Awful Hack to get position polling to work, as exoplayer does not provide any
// onPositionChanged callback for some inane reason.
// TODO: MediaSession might have a callback for positions. Idk.
private fun pollCurrentPosition() = flow {
while (player.isPlaying) {
emit(player.currentPosition)
@ -435,10 +431,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
}
}
inner class LocalBinder : Binder() {
fun getService() = this@PlaybackService
}
companion object {
private const val DISCONNECTED = 0
private const val CONNECTED = 1

View file

@ -153,39 +153,61 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
playbackManager.prev()
}
// Remove a queue item, given a QueueAdapter index.
// Remove a queue OR user queue item, given a QueueAdapter index.
fun removeQueueItem(adapterIndex: Int) {
// Translate the adapter indices into the correct queue indices
val delta = mQueue.value!!.size - nextItemsInQueue.value!!.size
var index = adapterIndex.dec()
val index = adapterIndex + delta
// If the item is in the user queue, then remove it from there after accounting for the header.
if (index < mUserQueue.value!!.size) {
playbackManager.removeUserQueueItem(index)
} else {
// Translate the indices into proper queue indices if removing an item from there.
index += (mQueue.value!!.size - nextItemsInQueue.value!!.size)
playbackManager.removeQueueItem(index)
if (userQueue.value!!.isNotEmpty()) {
index -= mUserQueue.value!!.size.inc()
}
playbackManager.removeQueueItem(index)
}
}
// Move queue items, given QueueAdapter indices.
fun moveQueueItems(adapterFrom: Int, adapterTo: Int) {
// Translate the adapter indices into the correct queue indices
val delta = mQueue.value!!.size - nextItemsInQueue.value!!.size
// Move queue OR user queue items, given QueueAdapter indices.
// I have no idea what is going on in this function, but it works, so
fun moveQueueItems(adapterFrom: Int, adapterTo: Int): Boolean {
var from = adapterFrom.dec()
var to = adapterTo.dec()
val from = adapterFrom + delta
val to = adapterTo + delta
if (from < mUserQueue.value!!.size) {
playbackManager.moveQueueItems(from, to)
if (to >= mUserQueue.value!!.size || to < 0) return false
playbackManager.moveUserQueueItems(from, to)
} else {
if (to < 0) return false
val delta = mQueue.value!!.size - nextItemsInQueue.value!!.size
from += delta
to += delta
if (userQueue.value!!.isNotEmpty()) {
if (to <= mUserQueue.value!!.size.inc()) return false
from -= mUserQueue.value!!.size.inc()
to -= mUserQueue.value!!.size.inc()
}
playbackManager.moveQueueItems(from, to)
}
return true
}
fun addToUserQueue(song: Song) {
playbackManager.addToUserQueue(song)
}
fun moveUserQueueItems(from: Int, to: Int) {
playbackManager.moveUserQueueItems(from, to)
}
fun removeUserQueueItem(index: Int) {
playbackManager.removeUserQueueItem(index)
}
// --- STATUS FUNCTIONS ---
// Flip the playing status.

View file

@ -1,25 +1,53 @@
package org.oxycblt.auxio.playback.queue
import android.annotation.SuppressLint
import android.util.Log
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.ViewGroup
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemQueueSongBinding
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.recycler.DiffCallback
import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder
import org.oxycblt.auxio.recycler.viewholders.HeaderViewHolder
class QueueAdapter(
val touchHelper: ItemTouchHelper
) : ListAdapter<Song, QueueAdapter.ViewHolder>(DiffCallback<Song>()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(ItemQueueSongBinding.inflate(LayoutInflater.from(parent.context)))
) : ListAdapter<BaseModel, RecyclerView.ViewHolder>(DiffCallback<BaseModel>()) {
override fun getItemViewType(position: Int): Int {
val item = getItem(position)
if (item is Header) {
return HeaderViewHolder.ITEM_TYPE
} else {
return QUEUE_ITEM_VIEW_TYPE
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
QUEUE_ITEM_VIEW_TYPE -> ViewHolder(
ItemQueueSongBinding.inflate(LayoutInflater.from(parent.context))
)
else -> error("Someone messed with the ViewHolder item types. Tell OxygenCobalt.")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = getItem(position)) {
is Song -> (holder as ViewHolder).bind(item)
is Header -> (holder as HeaderViewHolder).bind(item)
else -> {
Log.d(this::class.simpleName, "Bad data fed to QueueAdapter.")
}
}
}
// Generic ViewHolder for a queue item
@ -46,4 +74,8 @@ class QueueAdapter(
}
}
}
companion object {
const val QUEUE_ITEM_VIEW_TYPE = 0xA030
}
}

View file

@ -9,13 +9,22 @@ import kotlin.math.min
import kotlin.math.sign
// The drag callback used for the Queue RecyclerView.
class QueueDragCallback(
private val playbackModel: PlaybackViewModel,
private val isUserQueue: Boolean
) : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
ItemTouchHelper.START
) {
class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
// Make header objects unswipable by only returning the swipe flags if the ViewHolder
// is for a queue item.
return if (viewHolder is QueueAdapter.ViewHolder) {
makeFlag(
ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN
) or makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START)
} else {
0
}
}
override fun interpolateOutOfBoundsScroll(
recyclerView: RecyclerView,
viewSize: Int,
@ -45,21 +54,11 @@ class QueueDragCallback(
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
if (isUserQueue) {
playbackModel.moveUserQueueItems(viewHolder.adapterPosition, target.adapterPosition)
} else {
playbackModel.moveQueueItems(viewHolder.adapterPosition, target.adapterPosition)
}
return true
return playbackModel.moveQueueItems(viewHolder.adapterPosition, target.adapterPosition)
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
if (isUserQueue) {
playbackModel.removeUserQueueItem(viewHolder.adapterPosition)
} else {
playbackModel.removeQueueItem(viewHolder.adapterPosition)
}
playbackModel.removeQueueItem(viewHolder.adapterPosition)
}
companion object {

View file

@ -7,11 +7,16 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentQueueBinding
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.ui.applyDivider
// TODO: Make this better
class QueueFragment : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels()
@ -22,28 +27,69 @@ class QueueFragment : Fragment() {
): View? {
val binding = FragmentQueueBinding.inflate(inflater)
val helper = ItemTouchHelper(QueueDragCallback(playbackModel))
val queueAdapter = QueueAdapter(helper)
// --- UI SETUP ---
binding.queueToolbar.setNavigationOnClickListener {
findNavController().navigateUp()
}
binding.queueViewpager.adapter = PagerAdapter()
binding.queueRecycler.apply {
setHasFixedSize(true)
applyDivider()
adapter = queueAdapter
helper.attachToRecyclerView(this)
}
// TODO: Add option for default queue screen
if (playbackModel.userQueue.value!!.isNotEmpty()) {
binding.queueViewpager.setCurrentItem(0, false)
} else {
binding.queueViewpager.setCurrentItem(1, false)
playbackModel.userQueue.observe(viewLifecycleOwner) {
queueAdapter.submitList(createQueueDisplay()) {
binding.queueRecycler.scrollToPosition(0)
scrollRecyclerIfNeeded(binding)
}
}
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) {
queueAdapter.submitList(createQueueDisplay()) {
scrollRecyclerIfNeeded(binding)
}
}
return binding.root
}
private inner class PagerAdapter :
FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) {
override fun getItemCount(): Int = 2
private fun createQueueDisplay(): MutableList<BaseModel> {
val queue = mutableListOf<BaseModel>()
override fun createFragment(position: Int): Fragment {
return QueueListFragment(position)
if (playbackModel.userQueue.value!!.isNotEmpty()) {
queue.add(Header(name = getString(R.string.label_next_user_queue)))
queue.addAll(playbackModel.userQueue.value!!)
}
if (playbackModel.nextItemsInQueue.value!!.isNotEmpty()) {
queue.add(
Header(
name = getString(
R.string.format_next_from,
if (playbackModel.mode.value == PlaybackMode.ALL_SONGS)
getString(R.string.title_all_songs)
else
playbackModel.parent.value!!.name
)
)
)
queue.addAll(playbackModel.nextItemsInQueue.value!!)
}
return queue
}
private fun scrollRecyclerIfNeeded(binding: FragmentQueueBinding) {
if ((binding.queueRecycler.layoutManager as LinearLayoutManager)
.findFirstVisibleItemPosition() < 1
) {
binding.queueRecycler.scrollToPosition(0)
}
}
}

View file

@ -1,145 +0,0 @@
package org.oxycblt.auxio.playback.queue
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentQueueListBinding
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.ui.applyDivider
// TODO: Unify the user/next queues into a single fragment
class QueueListFragment(private val type: Int) : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentQueueListBinding.inflate(inflater)
// --- UI SETUP ---
binding.queueRecycler.apply {
itemAnimator = DefaultItemAnimator()
applyDivider()
setHasFixedSize(true)
}
// Continue setup with different values depending on the type
when (type) {
TYPE_NEXT_QUEUE -> setupForNextQueue(binding)
TYPE_USER_QUEUE -> setupForUserQueue(binding)
}
return binding.root
}
private fun setupForNextQueue(binding: FragmentQueueListBinding) {
val helper = ItemTouchHelper(QueueDragCallback(playbackModel, false))
val queueNextAdapter = QueueAdapter(helper)
binding.queueRecycler.apply {
adapter = queueNextAdapter
helper.attachToRecyclerView(this)
}
playbackModel.mode.observe(viewLifecycleOwner) {
binding.queueHeader.text = getString(
R.string.format_next_from,
if (it == PlaybackMode.ALL_SONGS) getString(R.string.title_all_songs)
else playbackModel.parent.value!!.name
)
}
playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) {
if (it.isEmpty()) {
if (playbackModel.userQueue.value!!.isEmpty()) {
findNavController().navigateUp()
} else {
binding.queueNothingIndicator.visibility = View.VISIBLE
binding.queueRecycler.visibility = View.GONE
}
return@observe
}
binding.queueNothingIndicator.visibility = View.GONE
binding.queueRecycler.visibility = View.VISIBLE
// If the first item is being moved, then scroll to the top position on completion
// to prevent ListAdapter from scrolling uncontrollably.
if (queueNextAdapter.currentList.isNotEmpty() &&
it[0].id != queueNextAdapter.currentList[0].id
) {
queueNextAdapter.submitList(it.toMutableList()) {
scrollRecyclerIfNeeded(binding)
}
} else {
queueNextAdapter.submitList(it.toMutableList())
}
}
}
private fun setupForUserQueue(binding: FragmentQueueListBinding) {
val helper = ItemTouchHelper(QueueDragCallback(playbackModel, true))
val userQueueAdapter = QueueAdapter(helper)
binding.queueHeader.setText(R.string.label_next_user_queue)
binding.queueRecycler.apply {
adapter = userQueueAdapter
helper.attachToRecyclerView(this)
}
playbackModel.userQueue.observe(viewLifecycleOwner) {
if (it.isEmpty()) {
if (playbackModel.queue.value!!.isEmpty()) {
findNavController().navigateUp()
} else {
binding.queueNothingIndicator.visibility = View.VISIBLE
binding.queueRecycler.visibility = View.GONE
}
return@observe
}
binding.queueNothingIndicator.visibility = View.GONE
binding.queueRecycler.visibility = View.VISIBLE
// If the first item is being moved, then scroll to the top position on completion
// to prevent ListAdapter from scrolling uncontrollably.
if (userQueueAdapter.currentList.isNotEmpty() &&
it[0].id != userQueueAdapter.currentList[0].id
) {
userQueueAdapter.submitList(it.toMutableList()) {
scrollRecyclerIfNeeded(binding)
}
} else {
userQueueAdapter.submitList(it.toMutableList())
}
}
}
private fun scrollRecyclerIfNeeded(binding: FragmentQueueListBinding) {
if ((binding.queueRecycler.layoutManager as LinearLayoutManager)
.findFirstVisibleItemPosition() < 1
) {
binding.queueRecycler.scrollToPosition(0)
}
}
companion object {
const val TYPE_USER_QUEUE = 0
const val TYPE_NEXT_QUEUE = 1
}
}

View file

@ -42,7 +42,6 @@ class PlaybackStateManager private constructor() {
}
private var mUserQueue = mutableListOf<Song>()
set(value) {
Log.d(this::class.simpleName, "retard.")
field = value
callbacks.forEach { it.onUserQueueUpdate(value) }
}
@ -174,7 +173,8 @@ class PlaybackStateManager private constructor() {
mMode = PlaybackMode.IN_GENRE
}
else -> error("what")
else -> {
}
}
resetLoopMode()

View file

@ -27,7 +27,7 @@ class SongsFragment : Fragment() {
val musicStore = MusicStore.getInstance()
// TODO: Add option to search songs if LibraryFragment isn't enabled
// TODO: Add option to search songs [Or just make a dedicated tab]
// TODO: Fast scrolling?
// --- UI SETUP ---

View file

@ -26,7 +26,6 @@ fun showActionMenuForSong(
view: View,
playbackModel: PlaybackViewModel
) {
// TODO: Replace this with a BottomSheet dialog?
PopupMenu(context, view).apply {
inflate(R.menu.menu_song_actions)
setOnMenuItemClickListener {

View file

@ -37,6 +37,7 @@
app:title="@string/title_library_fragment" />
<androidx.core.widget.NestedScrollView
android:id="@+id/nested_scroll"
android:layout_width="match_parent"
android:layout_height="match_parent">

View file

@ -6,7 +6,9 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:orientation="vertical"
android:background="@color/background"
android:animateLayoutChanges="true">
<androidx.appcompat.widget.Toolbar
android:id="@+id/queue_toolbar"
@ -22,10 +24,24 @@
app:title="@string/label_queue"
app:titleTextAppearance="@style/TextAppearance.Toolbar.Header" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/queue_viewpager"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/queue_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintTop_toBottomOf="@+id/queue_header"
tools:layout_editor_absoluteX="0dp"
tools:listitem="@layout/item_song" />
<TextView
android:id="@+id/queue_nothing_indicator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/margin_medium"
android:textSize="15sp"
android:text="@string/label_empty_queue"
android:textAlignment="center"
android:visibility="gone" />
</LinearLayout>
</layout>

View file

@ -1,48 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/background"
android:animateLayoutChanges="true">
<TextView
android:id="@+id/queue_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/ui_header_dividers"
android:fontFamily="@font/inter_bold"
android:paddingStart="@dimen/padding_medium"
android:paddingTop="@dimen/padding_small"
android:paddingEnd="@dimen/padding_small"
android:paddingBottom="@dimen/padding_small"
android:textAppearance="@style/TextAppearance.MaterialComponents.Overline"
android:textSize="16sp"
tools:text="Next in Queue"
app:layout_constraintTop_toBottomOf="@+id/album_details" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/queue_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintTop_toBottomOf="@+id/queue_header"
tools:layout_editor_absoluteX="0dp"
tools:listitem="@layout/item_song" />
<TextView
android:id="@+id/queue_nothing_indicator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/margin_medium"
android:textSize="15sp"
android:text="@string/label_empty_queue"
android:textAlignment="center"
android:visibility="gone" />
</LinearLayout>
</layout>

View file

@ -27,7 +27,7 @@
android:paddingBottom="@dimen/padding_small"
android:textAppearance="@style/TextAppearance.MaterialComponents.Overline"
android:textSize="16sp"
app:headerText="@{header}"
android:text="@{header.name}"
tools:text="Songs" />
</FrameLayout>