Change search to show all music items

Change the library search to show artists, albums, and songs.
This commit is contained in:
OxygenCobalt 2020-10-03 18:19:51 -06:00
parent b814ac613d
commit 45f411fdd7
13 changed files with 332 additions and 74 deletions

View file

@ -26,8 +26,7 @@ TODOs surrounded with !s are things I tried to do, but failed for reasons includ
- Exit functionality - Exit functionality
- ? Remove gap from where I removed the overflow menu ? - ? Remove gap from where I removed the overflow menu ?
- ? Add icons to overflow menu items ? - ? Add icons to overflow menu items ?
- ? Show Artists, Albums, and Songs in search ? - ? Implement filtering for search [Will resolve gap issue] ?
- ? Implement filtering for above ^^^ [Will resolve gap issue] ?
- ? Move into ViewPager ? - ? Move into ViewPager ?
- ! Move Adapter functionality to ListAdapter [RecyclerView scrolls to middle/bottom when data is re-sorted] ! - ! Move Adapter functionality to ListAdapter [RecyclerView scrolls to middle/bottom when data is re-sorted] !

View file

@ -3,6 +3,7 @@ package org.oxycblt.auxio.library
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
@ -16,6 +17,8 @@ import androidx.transition.TransitionManager
import org.oxycblt.auxio.MainFragmentDirections import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentLibraryBinding import org.oxycblt.auxio.databinding.FragmentLibraryBinding
import org.oxycblt.auxio.library.recycler.LibraryAdapter
import org.oxycblt.auxio.library.recycler.SearchAdapter
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.BaseModel import org.oxycblt.auxio.music.BaseModel
@ -34,8 +37,6 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
private val musicModel: MusicViewModel by activityViewModels() private val musicModel: MusicViewModel by activityViewModels()
private val libraryModel: LibraryViewModel by activityViewModels() private val libraryModel: LibraryViewModel by activityViewModels()
private lateinit var libraryAdapter: LibraryAdapter
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -43,6 +44,15 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
): View? { ): View? {
val binding = FragmentLibraryBinding.inflate(inflater) val binding = FragmentLibraryBinding.inflate(inflater)
val libraryAdapter = LibraryAdapter(
libraryModel.showMode.value!!,
ClickListener { navToItem(it) }
)
val searchAdapter = SearchAdapter(
ClickListener { navToItem(it) }
)
// Toolbar setup // Toolbar setup
binding.libraryToolbar.overflowIcon = ContextCompat.getDrawable( binding.libraryToolbar.overflowIcon = ContextCompat.getDrawable(
requireContext(), R.drawable.ic_sort_none requireContext(), R.drawable.ic_sort_none
@ -52,18 +62,35 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
val item = findItem(R.id.action_search) val item = findItem(R.id.action_search)
val searchView = item.actionView as SearchView val searchView = item.actionView as SearchView
// Set up the SearchView itself
searchView.queryHint = getString(R.string.hint_search_library) searchView.queryHint = getString(R.string.hint_search_library)
searchView.setOnQueryTextListener(this@LibraryFragment) searchView.setOnQueryTextListener(this@LibraryFragment)
searchView.setOnQueryTextFocusChangeListener { _, hasFocus -> searchView.setOnQueryTextFocusChangeListener { _, hasFocus ->
this.setGroupVisible(R.id.group_sorting, !hasFocus) libraryModel.updateSearchFocusStatus(hasFocus)
// Make sure the search item will still be visible, and then do an animation
item.isVisible = !hasFocus item.isVisible = !hasFocus
TransitionManager.beginDelayedTransition(
binding.libraryToolbar, Fade()
)
item.collapseActionView()
} }
item.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
// When opened, update the adapter to the SearchAdapter
// And remove the sorting group
binding.libraryRecycler.adapter = searchAdapter
setGroupVisible(R.id.group_sorting, false)
libraryModel.resetQuery()
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
// When closed, switch back to LibraryAdapter, make the sorting
// visible again, and reset the query so that the old results wont show
// up if the search is opened again.
binding.libraryRecycler.adapter = libraryAdapter
setGroupVisible(R.id.group_sorting, true)
return true
}
})
} }
binding.libraryToolbar.setOnMenuItemClickListener { binding.libraryToolbar.setOnMenuItemClickListener {
@ -82,11 +109,6 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
true true
} }
libraryAdapter = LibraryAdapter(
libraryModel.showMode.value!!,
ClickListener { navToItem(it) }
)
// RecyclerView setup // RecyclerView setup
binding.libraryRecycler.adapter = libraryAdapter binding.libraryRecycler.adapter = libraryAdapter
binding.libraryRecycler.applyDivider() binding.libraryRecycler.applyDivider()
@ -116,17 +138,10 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
} }
} }
libraryModel.searchQuery.observe(viewLifecycleOwner) { query -> libraryModel.searchResults.observe(viewLifecycleOwner) {
// Update the adapter with the new data if (libraryModel.searchHasFocus) {
libraryAdapter.updateData( searchAdapter.submitList(it)
when (libraryModel.showMode.value) { }
SHOW_GENRES -> musicModel.genres.value!!
SHOW_ARTISTS -> musicModel.artists.value!!
SHOW_ALBUMS -> musicModel.albums.value!!
else -> musicModel.artists.value!!
}.filter { it.name.contains(query, true) }
)
} }
Log.d(this::class.simpleName, "Fragment created.") Log.d(this::class.simpleName, "Fragment created.")
@ -143,7 +158,7 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean = false override fun onQueryTextSubmit(query: String): Boolean = false
override fun onQueryTextChange(query: String): Boolean { override fun onQueryTextChange(query: String): Boolean {
libraryModel.updateSearchQuery(query) libraryModel.updateSearchQuery(query, musicModel)
return false return false
} }

View file

@ -4,14 +4,24 @@ import android.view.MenuItem
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.recycler.SortMode import org.oxycblt.auxio.recycler.SortMode
import org.oxycblt.auxio.theme.SHOW_ALBUMS
import org.oxycblt.auxio.theme.SHOW_ARTISTS import org.oxycblt.auxio.theme.SHOW_ARTISTS
import org.oxycblt.auxio.theme.SHOW_SONGS
class LibraryViewModel : ViewModel() { class LibraryViewModel : ViewModel() {
private var mIsNavigating = false private var mIsNavigating = false
val isNavigating: Boolean get() = mIsNavigating val isNavigating: Boolean get() = mIsNavigating
private var mSearchHasFocus = false
val searchHasFocus: Boolean get() = mSearchHasFocus
// TODO: Move these to prefs when they're added // TODO: Move these to prefs when they're added
private val mShowMode = MutableLiveData(SHOW_ARTISTS) private val mShowMode = MutableLiveData(SHOW_ARTISTS)
val showMode: LiveData<Int> get() = mShowMode val showMode: LiveData<Int> get() = mShowMode
@ -19,8 +29,8 @@ class LibraryViewModel : ViewModel() {
private val mSortMode = MutableLiveData(SortMode.ALPHA_DOWN) private val mSortMode = MutableLiveData(SortMode.ALPHA_DOWN)
val sortMode: LiveData<SortMode> get() = mSortMode val sortMode: LiveData<SortMode> get() = mSortMode
private val mSearchQuery = MutableLiveData("") private val mSearchResults = MutableLiveData(listOf<BaseModel>())
val searchQuery: LiveData<String> get() = mSearchQuery val searchResults: LiveData<List<BaseModel>> get() = mSearchResults
fun updateSortMode(item: MenuItem) { fun updateSortMode(item: MenuItem) {
val mode = when (item.itemId) { val mode = when (item.itemId) {
@ -36,11 +46,53 @@ class LibraryViewModel : ViewModel() {
} }
} }
fun updateSearchQuery(query: String) { fun updateSearchQuery(query: String, musicModel: MusicViewModel) {
mSearchQuery.value = query if (query == "") {
resetQuery()
return
}
// Search MusicViewModel for all the items [Artists, Albums, Songs] that contain
// the query, and update the LiveData with those items. This is done on a seperate
// thread as it can be a very intensive operation for large music libraries.
viewModelScope.launch {
val combined = mutableListOf<BaseModel>()
val artists = musicModel.artists.value!!.filter { it.name.contains(query, true) }
if (artists.isNotEmpty()) {
combined.add(Header(id = SHOW_ARTISTS.toLong()))
combined.addAll(artists)
}
val albums = musicModel.albums.value!!.filter { it.name.contains(query, true) }
if (albums.isNotEmpty()) {
combined.add(Header(id = SHOW_ALBUMS.toLong()))
combined.addAll(albums)
}
val songs = musicModel.songs.value!!.filter { it.name.contains(query, true) }
if (songs.isNotEmpty()) {
combined.add(Header(id = SHOW_SONGS.toLong()))
combined.addAll(songs)
}
mSearchResults.value = combined
}
}
fun resetQuery() {
mSearchResults.value = listOf()
} }
fun updateNavigationStatus(value: Boolean) { fun updateNavigationStatus(value: Boolean) {
mIsNavigating = value mIsNavigating = value
} }
fun updateSearchFocusStatus(value: Boolean) {
mSearchHasFocus = value
}
} }

View file

@ -1,4 +1,4 @@
package org.oxycblt.auxio.library package org.oxycblt.auxio.library.recycler
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
@ -10,7 +10,6 @@ import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.recycler.BaseViewHolder
import org.oxycblt.auxio.recycler.ClickListener import org.oxycblt.auxio.recycler.ClickListener
import org.oxycblt.auxio.theme.SHOW_ALBUMS import org.oxycblt.auxio.theme.SHOW_ALBUMS
import org.oxycblt.auxio.theme.SHOW_ARTISTS import org.oxycblt.auxio.theme.SHOW_ARTISTS
@ -22,7 +21,7 @@ class LibraryAdapter(
val listener: ClickListener<BaseModel> val listener: ClickListener<BaseModel>
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var data: List<BaseModel> private var data: List<BaseModel>
init { init {
// Assign the data on startup depending on the type // Assign the data on startup depending on the type
@ -41,18 +40,22 @@ class LibraryAdapter(
// Return a different View Holder depending on the show type // Return a different View Holder depending on the show type
return when (showMode) { return when (showMode) {
SHOW_GENRES -> GenreViewHolder( SHOW_GENRES -> GenreViewHolder(
listener,
ItemGenreBinding.inflate(LayoutInflater.from(parent.context)) ItemGenreBinding.inflate(LayoutInflater.from(parent.context))
) )
SHOW_ARTISTS -> ArtistViewHolder( SHOW_ARTISTS -> ArtistViewHolder(
listener,
ItemArtistBinding.inflate(LayoutInflater.from(parent.context)) ItemArtistBinding.inflate(LayoutInflater.from(parent.context))
) )
SHOW_ALBUMS -> AlbumViewHolder( SHOW_ALBUMS -> AlbumViewHolder(
listener,
ItemAlbumBinding.inflate(LayoutInflater.from(parent.context)) ItemAlbumBinding.inflate(LayoutInflater.from(parent.context))
) )
else -> ArtistViewHolder( else -> ArtistViewHolder(
listener,
ItemArtistBinding.inflate(LayoutInflater.from(parent.context)) ItemArtistBinding.inflate(LayoutInflater.from(parent.context))
) )
} }
@ -69,42 +72,9 @@ class LibraryAdapter(
} }
// Update the data, as its an internal value. // Update the data, as its an internal value.
// TODO: Call this from a coroutine.
fun updateData(newData: List<BaseModel>) { fun updateData(newData: List<BaseModel>) {
data = newData data = newData
notifyDataSetChanged() notifyDataSetChanged()
} }
// --- VIEWHOLDERS ---
inner class GenreViewHolder(
private val binding: ItemGenreBinding
) : BaseViewHolder<BaseModel>(binding, listener) {
override fun onBind(model: BaseModel) {
binding.genre = model as Genre
binding.genreName.requestLayout()
}
}
inner class ArtistViewHolder(
private val binding: ItemArtistBinding
) : BaseViewHolder<BaseModel>(binding, listener) {
override fun onBind(model: BaseModel) {
binding.artist = model as Artist
binding.artistName.requestLayout()
}
}
inner class AlbumViewHolder(
private val binding: ItemAlbumBinding
) : BaseViewHolder<BaseModel>(binding, listener) {
override fun onBind(model: BaseModel) {
binding.album = model as Album
binding.albumName.requestLayout()
}
}
} }

View file

@ -0,0 +1,90 @@
package org.oxycblt.auxio.library.recycler
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemAlbumBinding
import org.oxycblt.auxio.databinding.ItemArtistBinding
import org.oxycblt.auxio.databinding.ItemGenreBinding
import org.oxycblt.auxio.databinding.ItemHeaderBinding
import org.oxycblt.auxio.databinding.ItemSongBinding
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.recycler.ClickListener
import org.oxycblt.auxio.recycler.DiffCallback
class SearchAdapter(
private val listener: ClickListener<BaseModel>
) : ListAdapter<BaseModel, RecyclerView.ViewHolder>(DiffCallback<BaseModel>()) {
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is Genre -> ITEM_TYPE_GENRE
is Artist -> ITEM_TYPE_ARTIST
is Album -> ITEM_TYPE_ALBUM
is Song -> ITEM_TYPE_SONG
is Header -> ITEM_TYPE_HEADER
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ITEM_TYPE_GENRE -> GenreViewHolder(
listener,
ItemGenreBinding.inflate(
LayoutInflater.from(parent.context)
)
)
ITEM_TYPE_ARTIST -> ArtistViewHolder(
listener,
ItemArtistBinding.inflate(
LayoutInflater.from(parent.context)
)
)
ITEM_TYPE_ALBUM -> AlbumViewHolder(
listener,
ItemAlbumBinding.inflate(
LayoutInflater.from(parent.context)
)
)
ITEM_TYPE_SONG -> SongViewHolder(
listener,
ItemSongBinding.inflate(
LayoutInflater.from(parent.context)
)
)
ITEM_TYPE_HEADER -> HeaderViewHolder(
ItemHeaderBinding.inflate(
LayoutInflater.from(parent.context)
)
)
else -> ArtistViewHolder(
listener,
ItemArtistBinding.inflate(
LayoutInflater.from(parent.context)
)
)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is GenreViewHolder -> holder
is ArtistViewHolder -> holder
is AlbumViewHolder -> holder
is SongViewHolder -> holder
is HeaderViewHolder -> holder
else -> return
}.onBind(getItem(position))
}
}

View file

@ -0,0 +1,75 @@
package org.oxycblt.auxio.library.recycler
import org.oxycblt.auxio.databinding.ItemAlbumBinding
import org.oxycblt.auxio.databinding.ItemArtistBinding
import org.oxycblt.auxio.databinding.ItemGenreBinding
import org.oxycblt.auxio.databinding.ItemHeaderBinding
import org.oxycblt.auxio.databinding.ItemSongBinding
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.recycler.BaseViewHolder
import org.oxycblt.auxio.recycler.ClickListener
const val ITEM_TYPE_GENRE = 10
const val ITEM_TYPE_ARTIST = 11
const val ITEM_TYPE_ALBUM = 12
const val ITEM_TYPE_SONG = 13
const val ITEM_TYPE_HEADER = 14
class GenreViewHolder(
listener: ClickListener<BaseModel>,
private val binding: ItemGenreBinding
) : BaseViewHolder<BaseModel>(binding, listener) {
override fun onBind(model: BaseModel) {
binding.genre = model as Genre
binding.genreName.requestLayout()
}
}
class ArtistViewHolder(
listener: ClickListener<BaseModel>,
private val binding: ItemArtistBinding
) : BaseViewHolder<BaseModel>(binding, listener) {
override fun onBind(model: BaseModel) {
binding.artist = model as Artist
binding.artistName.requestLayout()
}
}
class AlbumViewHolder(
listener: ClickListener<BaseModel>,
private val binding: ItemAlbumBinding
) : BaseViewHolder<BaseModel>(binding, listener) {
override fun onBind(model: BaseModel) {
binding.album = model as Album
binding.albumName.requestLayout()
}
}
class SongViewHolder(
listener: ClickListener<BaseModel>,
private val binding: ItemSongBinding
) : BaseViewHolder<BaseModel>(binding, listener) {
override fun onBind(model: BaseModel) {
binding.song = model as Song
binding.songName.requestLayout()
binding.songInfo.requestLayout()
}
}
class HeaderViewHolder(
private val binding: ItemHeaderBinding
) : BaseViewHolder<BaseModel>(binding, null) {
override fun onBind(model: BaseModel) {
binding.header = model as Header
}
}

View file

@ -90,3 +90,9 @@ data class Genre(
return num return num
} }
} }
// Header [Used for search, nothing else]
data class Header(
override val id: Long = -1,
override var name: String = ""
) : BaseModel()

View file

@ -8,6 +8,9 @@ import android.text.format.DateUtils
import android.widget.TextView import android.widget.TextView
import androidx.databinding.BindingAdapter import androidx.databinding.BindingAdapter
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.theme.SHOW_ALBUMS
import org.oxycblt.auxio.theme.SHOW_ARTISTS
import org.oxycblt.auxio.theme.SHOW_SONGS
// List of ID3 genres + Winamp extensions, each index corresponds to their int value. // 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. // There are a lot more int-genre extensions as far as Im aware, but this works for most cases.
@ -152,3 +155,16 @@ fun TextView.bindAlbumSongs(album: Album) {
R.plurals.format_song_count, album.numSongs, album.numSongs R.plurals.format_song_count, album.numSongs, album.numSongs
) )
} }
@BindingAdapter("headerText")
fun TextView.bindHeaderText(header: Header) {
text = context.getString(
when (header.id.toInt()) {
SHOW_ARTISTS -> R.string.label_artists
SHOW_ALBUMS -> R.string.label_albums
SHOW_SONGS -> R.string.label_songs
else -> R.string.label_artists
}
)
}

View file

@ -17,7 +17,7 @@ import org.oxycblt.auxio.music.processing.MusicLoaderResponse
import org.oxycblt.auxio.music.processing.MusicSorter import org.oxycblt.auxio.music.processing.MusicSorter
// ViewModel for music storage. // ViewModel for music storage.
// FIXME: This class can be improved in multiple ways // FIXME: This system can be improved in multiple ways
// - Remove lists/parents from models so that they can be parcelable // - Remove lists/parents from models so that they can be parcelable
// - Move genre usage to songs [If there's a way to find songs without a genre] // - Move genre usage to songs [If there's a way to find songs without a genre]
class MusicViewModel(private val app: Application) : ViewModel() { class MusicViewModel(private val app: Application) : ViewModel() {

View file

@ -7,7 +7,7 @@ import org.oxycblt.auxio.music.BaseModel
// ViewHolder abstraction that automates some of the things that are common for all ViewHolders. // ViewHolder abstraction that automates some of the things that are common for all ViewHolders.
abstract class BaseViewHolder<T : BaseModel>( abstract class BaseViewHolder<T : BaseModel>(
private val baseBinding: ViewDataBinding, private val baseBinding: ViewDataBinding,
protected val listener: ClickListener<T> protected val listener: ClickListener<T>?
) : RecyclerView.ViewHolder(baseBinding.root) { ) : RecyclerView.ViewHolder(baseBinding.root) {
init { init {
baseBinding.root.layoutParams = RecyclerView.LayoutParams( baseBinding.root.layoutParams = RecyclerView.LayoutParams(
@ -16,8 +16,10 @@ abstract class BaseViewHolder<T : BaseModel>(
} }
fun bind(model: T) { fun bind(model: T) {
baseBinding.root.setOnClickListener { if (listener != null) {
listener.onClick(model) baseBinding.root.setOnClickListener {
listener.onClick(model)
}
} }
onBind(model) onBind(model)

View file

@ -3,8 +3,7 @@ package org.oxycblt.auxio.recycler
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.BaseModel
// A RecyclerView click listener that can only be called once. // RecyclerView click listener
// Primarily used for navigation to prevent bugs when multiple items are selected.
class ClickListener<T>(val onClick: (T) -> Unit) class ClickListener<T>(val onClick: (T) -> Unit)
// Base Diff callback // Base Diff callback

View file

@ -4,3 +4,4 @@ package org.oxycblt.auxio.theme
const val SHOW_ARTISTS = 0 const val SHOW_ARTISTS = 0
const val SHOW_ALBUMS = 1 const val SHOW_ALBUMS = 1
const val SHOW_GENRES = 2 const val SHOW_GENRES = 2
const val SHOW_SONGS = 3

View file

@ -0,0 +1,33 @@
<?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">
<data>
<variable
name="header"
type="org.oxycblt.auxio.music.Header" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/header_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/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"
app:headerText="@{header}"
tools:text="Songs" />
</FrameLayout>
</layout>