Update genre detail layout

Make GenreDetailFragment a full RecyclerView in order to fix issues with how NestedScrollView has to generate all ViewHolders in order on creation.
This commit is contained in:
OxygenCobalt 2020-12-28 10:48:50 -07:00
parent c1ae32325f
commit 804db8b0d3
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
12 changed files with 485 additions and 302 deletions

View file

@ -8,14 +8,16 @@ import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.GridLayoutManager
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentGenreDetailBinding
import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.adapters.GenreSongAdapter
import org.oxycblt.auxio.logD
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.ui.disable
import org.oxycblt.auxio.ui.isLandscape
import org.oxycblt.auxio.ui.setupGenreSongActions
/**
@ -32,7 +34,7 @@ class GenreDetailFragment : DetailFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = FragmentGenreDetailBinding.inflate(inflater)
val binding = FragmentDetailBinding.inflate(inflater)
// If DetailViewModel isn't already storing the genre, get it from MusicStore
// using the ID given by the navigation arguments
@ -47,6 +49,7 @@ class GenreDetailFragment : DetailFragment() {
}
val songAdapter = GenreSongAdapter(
viewLifecycleOwner, detailModel,
doOnClick = {
playbackModel.playSong(it, PlaybackMode.IN_GENRE)
},
@ -60,10 +63,9 @@ class GenreDetailFragment : DetailFragment() {
// --- UI SETUP ---
binding.lifecycleOwner = this
binding.detailModel = detailModel
binding.genre = detailModel.currentGenre.value
binding.genreToolbar.apply {
binding.detailToolbar.apply {
inflateMenu(R.menu.menu_songs)
setNavigationOnClickListener {
findNavController().navigateUp()
}
@ -84,13 +86,19 @@ class GenreDetailFragment : DetailFragment() {
}
}
if (detailModel.currentGenre.value!!.songs.size < 2) {
binding.genreSortButton.disable(requireContext())
}
binding.genreSongRecycler.apply {
binding.detailRecycler.apply {
adapter = songAdapter
setHasFixedSize(true)
if (isLandscape(resources)) {
layoutManager = GridLayoutManager(requireContext(), 2).also {
it.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return if (position == 0) 2 else 1
}
}
}
}
}
// --- VIEWMODEL SETUP ---
@ -98,13 +106,11 @@ class GenreDetailFragment : DetailFragment() {
detailModel.genreSortMode.observe(viewLifecycleOwner) { mode ->
logD("Updating sort mode to $mode")
// Update the current sort icon
binding.genreSortButton.setImageResource(mode.iconRes)
val data = mutableListOf<BaseModel>(detailModel.currentGenre.value!!).also {
it.addAll(mode.getSortedSongList(detailModel.currentGenre.value!!.songs))
}
// Then update the sort mode of the artist adapter.
songAdapter.submitList(
mode.getSortedSongList(detailModel.currentGenre.value!!.songs)
)
songAdapter.submitList(data)
}
logD("Fragment created.")

View file

@ -3,8 +3,14 @@ package org.oxycblt.auxio.detail.adapters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemGenreHeaderBinding
import org.oxycblt.auxio.databinding.ItemGenreSongBinding
import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.recycler.DiffCallback
import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder
@ -13,25 +19,54 @@ import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder
* An adapter for displaying the [Song]s of a genre.
*/
class GenreSongAdapter(
private val lifecycleOwner: LifecycleOwner,
private val detailModel: DetailViewModel,
private val doOnClick: (data: Song) -> Unit,
private val doOnLongClick: (data: Song, view: View) -> Unit
) : ListAdapter<Song, GenreSongAdapter.GenreSongViewHolder>(DiffCallback()) {
) : ListAdapter<BaseModel, RecyclerView.ViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenreSongViewHolder {
return GenreSongViewHolder(
ItemGenreSongBinding.inflate(LayoutInflater.from(parent.context)),
doOnClick, doOnLongClick
)
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is Genre -> GENRE_HEADER_ITEM_TYPE
is Song -> GENRE_SONG_ITEM_TYPE
else -> -1
}
}
override fun onBindViewHolder(holder: GenreSongViewHolder, position: Int) {
holder.bind(getItem(position))
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
GENRE_HEADER_ITEM_TYPE -> GenreHeaderViewHolder(
ItemGenreHeaderBinding.inflate(LayoutInflater.from(parent.context))
)
GENRE_SONG_ITEM_TYPE -> GenreSongViewHolder(
ItemGenreSongBinding.inflate(LayoutInflater.from(parent.context)),
)
else -> error("Bad viewholder item type $viewType")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = getItem(position)) {
is Genre -> (holder as GenreHeaderViewHolder).bind(item)
is Song -> (holder as GenreSongViewHolder).bind(item)
}
}
inner class GenreHeaderViewHolder(
private val binding: ItemGenreHeaderBinding
) : BaseViewHolder<Genre>(binding, null, null) {
override fun onBind(data: Genre) {
binding.genre = data
binding.detailModel = detailModel
binding.lifecycleOwner = lifecycleOwner
}
}
inner class GenreSongViewHolder(
private val binding: ItemGenreSongBinding,
doOnClick: (data: Song) -> Unit,
doOnLongClick: (data: Song, view: View) -> Unit
) : BaseViewHolder<Song>(binding, doOnClick, doOnLongClick) {
override fun onBind(data: Song) {
@ -41,4 +76,9 @@ class GenreSongAdapter(
binding.songInfo.requestLayout()
}
}
companion object {
const val GENRE_HEADER_ITEM_TYPE = 0xA020
const val GENRE_SONG_ITEM_TYPE = 0xA021
}
}

View file

@ -5,9 +5,12 @@ import android.content.Context
import android.net.Uri
import android.provider.MediaStore
import android.text.format.DateUtils
import android.widget.ImageButton
import android.widget.TextView
import androidx.databinding.BindingAdapter
import org.oxycblt.auxio.R
import org.oxycblt.auxio.logD
import org.oxycblt.auxio.recycler.SortMode
/**
* List of ID3 genres + Winamp extensions, each index corresponds to their int value.
@ -166,3 +169,12 @@ fun TextView.bindAlbumInfo(album: Album) {
fun TextView.bindAlbumYear(album: Album) {
text = album.year.toYear(context)
}
/**
* Bind the [SortMode] icon for an ImageButton.
*/
@BindingAdapter("sortIcon")
fun ImageButton.bindSortIcon(data: SortMode) {
logD("YOU STUPID FUCKING RETARD JUST FUNCITON")
setImageResource(data.iconRes)
}

View file

@ -0,0 +1,166 @@
package org.oxycblt.auxio.recycler
import android.graphics.PointF
import android.view.View
import android.view.animation.Interpolator
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.BuildConfig
import kotlin.math.exp
/**
* A custom [RecyclerView.SmoothScroller] partially copied from [androidx.recyclerview.widget.LinearSmoothScroller] that has a scroll effect similar
* to [androidx.core.widget.NestedScrollView].
*
* I don't know what half of this code does but it works and looks better than the default scroller so I use it
*/
class LinearCenterScroller(target: Int) : RecyclerView.SmoothScroller() {
private val viscousInterpolator = ViscousFluidInterpolator()
private var targetVec: PointF? = null
// Temporary variables to keep track of the interim scroll target. These values do not
// point to a real item position, rather point to an estimated location pixels.
private var interimTargetDx = 0
private var interimTargetDy = 0
init {
targetPosition = target
}
// Not used
override fun onStart() {}
override fun onTargetFound(targetView: View, state: RecyclerView.State, action: Action) {
val dx = calcDxToMakeVisible(targetView)
val dy = calcDyToMakeVisible(targetView)
action.update(-dx, -dy, DEFAULT_TIME, viscousInterpolator)
}
override fun onSeekTargetStep(dx: Int, dy: Int, state: RecyclerView.State, action: Action) {
if (childCount == 0) {
stop()
return
}
if (BuildConfig.DEBUG && targetVec != null && ((targetVec!!.x * dx < 0 || targetVec!!.y * dy < 0))) {
error("Scroll happened in the opposite direction of the target. Some calculations are wrong")
}
interimTargetDx = clampApplyScroll(interimTargetDx, dx)
interimTargetDy = clampApplyScroll(interimTargetDy, dy)
if (interimTargetDx == 0 && interimTargetDy == 0) {
updateActionForInterimTarget(action)
}
}
override fun onStop() {
interimTargetDx = 0
interimTargetDy = 0
targetVec = null
}
private fun calcDxToMakeVisible(view: View): Int {
val manager = layoutManager ?: return 0
if (!manager.canScrollHorizontally()) return 0
val params = view.layoutParams as RecyclerView.LayoutParams
val top = manager.getDecoratedTop(view) - params.topMargin
val bottom = manager.getDecoratedBottom(view) + params.bottomMargin
val start = manager.paddingTop
val end = manager.height - manager.paddingBottom
return calculateDeltaToFit(top, bottom, start, end)
}
private fun calcDyToMakeVisible(view: View): Int {
val manager = layoutManager ?: return 0
if (!manager.canScrollVertically()) return 0
val params = view.layoutParams as RecyclerView.LayoutParams
val top = manager.getDecoratedTop(view) - params.topMargin
val bottom = manager.getDecoratedBottom(view) + params.bottomMargin
val start = manager.paddingTop
val end = manager.height - manager.paddingBottom
return calculateDeltaToFit(top, bottom, start, end)
}
private fun calculateDeltaToFit(viewStart: Int, viewEnd: Int, boxStart: Int, boxEnd: Int): Int {
// Center the view instead of making it sit at the top or bottom.
return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2)
}
private fun clampApplyScroll(argTmpDt: Int, dt: Int): Int {
var tmpDt = argTmpDt
tmpDt -= dt
if (argTmpDt * tmpDt <= 0) { // changed sign, reached 0 or was 0, reset
return 0
}
return tmpDt
}
private fun updateActionForInterimTarget(action: Action) {
val scrollVector = computeScrollVectorForPosition(targetPosition)
if (scrollVector == null || (scrollVector.x == 0.0f && scrollVector.y == 0.0f)) {
val target = targetPosition
action.jumpTo(target)
stop()
return
}
normalize(scrollVector)
targetVec = scrollVector
interimTargetDx = (TARGET_SEEK_SCROLL_DIST * scrollVector.x).toInt()
interimTargetDy = (TARGET_SEEK_SCROLL_DIST * scrollVector.y).toInt()
// To avoid UI hiccups, trigger a smooth scroll to a distance little further than the
// interim target. Since we track the distance travelled in onSeekTargetStep callback, it
// won't actually scroll more than what we need.
action.update(
(interimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO).toInt(),
(interimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO).toInt(),
DEFAULT_TIME, viscousInterpolator
)
}
/**
* A nice-looking interpolator that is similar to the [androidx.core.widget.NestedScrollView] interpolator.
*/
private inner class ViscousFluidInterpolator : Interpolator {
private val viscousNormalize = 1.0f / viscousFluid(1.0f)
private val viscousOffset = 1.0f - viscousNormalize * viscousFluid(1.0f)
fun viscousFluid(argX: Float): Float {
var x = argX
x *= VISCOUS_FLUID_SCALE
if (x < 1.0f) {
x -= (1.0f - exp(-x))
} else {
val start = 0.36787944117f; // 1/e == exp(-1)
x = 1.0f - exp(1.0f - x)
x = start + x * (1.0f - start)
}
return x
}
override fun getInterpolation(input: Float): Float {
val interpolated = viscousNormalize * viscousFluid(input)
if (interpolated > 0) {
return interpolated + viscousOffset
}
return interpolated
}
}
companion object {
private const val VISCOUS_FLUID_SCALE = 12.0f
private const val TARGET_SEEK_SCROLL_DIST = 10000
private const val TARGET_SEEK_EXTRA_SCROLL_RATIO = 1.2f
private const val DEFAULT_TIME = 500
}
}

View file

@ -240,7 +240,7 @@ fun PopupMenu.setupGenreActions(genre: Genre, playbackModel: PlaybackViewModel)
else -> false
}
}
inflateAndShow(R.menu.menu_artist_detail)
inflateAndShow(R.menu.menu_genre_actions)
}
/**
@ -268,11 +268,6 @@ fun PopupMenu.setupGenreSongActions(context: Context, song: Song, playbackModel:
true
}
R.id.action_play_all_songs -> {
playbackModel.playSong(song, PlaybackMode.ALL_SONGS)
true
}
else -> false
}
}

View file

@ -1,135 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".detail.GenreDetailFragment">
<data>
<variable
name="genre"
type="org.oxycblt.auxio.music.Genre" />
<variable
name="detailModel"
type="org.oxycblt.auxio.detail.DetailViewModel" />
<variable
name="playbackModel"
type="org.oxycblt.auxio.playback.PlaybackViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/genre_toolbar"
style="@style/Toolbar.Style.Icon"
android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_normal"
app:menu="@menu/menu_songs"
app:title="@string/label_library" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:orientation="vertical">
<ImageView
android:id="@+id/genre_image"
android:layout_width="@dimen/size_cover_mid_huge"
android:layout_height="@dimen/size_cover_mid_huge"
android:layout_margin="@dimen/margin_medium"
android:contentDescription="@{@string/description_genre_image(genre.name)}"
app:genreImage="@{genre}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_genre" />
<TextView
android:id="@+id/genre_name"
style="@style/DetailTitleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_medium"
android:layout_marginEnd="@dimen/margin_medium"
android:text="@{genre.displayName}"
app:layout_constraintBottom_toTopOf="@+id/genre_song_count"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/genre_image"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Genre Name" />
<TextView
android:id="@+id/genre_song_count"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_medium"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
android:text="@{@plurals/format_song_count(genre.songs.size(), genre.songs.size())}"
app:layout_constraintBottom_toTopOf="@+id/genre_duration"
app:layout_constraintStart_toEndOf="@+id/genre_image"
app:layout_constraintTop_toBottomOf="@+id/genre_name"
tools:text="2 Artists, 4 Albums" />
<TextView
android:id="@+id/genre_duration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_medium"
android:text="@{genre.totalDuration}"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toTopOf="@+id/genre_song_header"
app:layout_constraintStart_toEndOf="@+id/genre_image"
app:layout_constraintTop_toBottomOf="@+id/genre_song_count"
tools:text="16:16" />
<TextView
android:id="@+id/genre_song_header"
style="@style/HeaderText"
android:layout_marginTop="@dimen/margin_medium"
android:text="@string/label_songs"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_image" />
<ImageButton
android:id="@+id/genre_sort_button"
style="@style/HeaderAction"
android:contentDescription="@string/description_sort_button"
android:onClick="@{() -> detailModel.incrementGenreSortMode()}"
app:layout_constraintBottom_toBottomOf="@+id/genre_song_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/genre_song_header"
tools:src="@drawable/ic_sort_alpha_down" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/genre_song_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_song_header"
app:spanCount="2"
tools:itemCount="4"
tools:listitem="@layout/item_genre_song" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
</layout>

View file

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="genre"
type="org.oxycblt.auxio.music.Genre" />
<variable
name="detailModel"
type="org.oxycblt.auxio.detail.DetailViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:orientation="vertical">
<ImageView
android:id="@+id/genre_image"
android:layout_width="@dimen/size_cover_mid_huge"
android:layout_height="@dimen/size_cover_mid_huge"
android:layout_margin="@dimen/margin_medium"
android:contentDescription="@{@string/description_genre_image(genre.name)}"
app:genreImage="@{genre}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_genre" />
<TextView
android:id="@+id/genre_name"
style="@style/DetailTitleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_medium"
android:layout_marginEnd="@dimen/margin_medium"
android:text="@{genre.displayName}"
app:layout_constraintBottom_toTopOf="@+id/genre_song_count"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/genre_image"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Genre Name" />
<TextView
android:id="@+id/genre_song_count"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_medium"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
android:text="@{@plurals/format_song_count(genre.songs.size(), genre.songs.size())}"
app:layout_constraintBottom_toTopOf="@+id/genre_duration"
app:layout_constraintStart_toEndOf="@+id/genre_image"
app:layout_constraintTop_toBottomOf="@+id/genre_name"
tools:text="2 Artists, 4 Albums" />
<TextView
android:id="@+id/genre_duration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_medium"
android:text="@{genre.totalDuration}"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toTopOf="@+id/genre_song_header"
app:layout_constraintStart_toEndOf="@+id/genre_image"
app:layout_constraintTop_toBottomOf="@+id/genre_song_count"
tools:text="16:16" />
<TextView
android:id="@+id/genre_song_header"
style="@style/HeaderText"
android:layout_marginTop="@dimen/margin_medium"
android:text="@string/label_songs"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_image" />
<ImageButton
android:id="@+id/genre_sort_button"
style="@style/HeaderAction"
android:contentDescription="@string/description_sort_button"
android:onClick="@{() -> detailModel.incrementGenreSortMode()}"
app:layout_constraintBottom_toBottomOf="@+id/genre_song_header"
app:layout_constraintEnd_toEndOf="parent"
app:sortIcon="@{detailModel.genreSortMode}"
app:layout_constraintTop_toTopOf="@+id/genre_song_header"
tools:src="@drawable/ic_sort_alpha_down" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".detail.GenreDetailFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/detail_toolbar"
style="@style/Toolbar.Style.Icon"
android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_normal"
app:title="@string/label_library"
app:contentInsetStartWithNavigation="0dp"
tools:menu="@menu/menu_artist_detail"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/detail_recycler"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_song_header"
tools:listitem="@layout/item_genre_header" />
</LinearLayout>
</layout>

View file

@ -1,133 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".detail.GenreDetailFragment">
<data>
<variable
name="genre"
type="org.oxycblt.auxio.music.Genre" />
<variable
name="detailModel"
type="org.oxycblt.auxio.detail.DetailViewModel" />
<variable
name="playbackModel"
type="org.oxycblt.auxio.playback.PlaybackViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/genre_toolbar"
style="@style/Toolbar.Style.Icon"
android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_normal"
app:menu="@menu/menu_songs"
app:title="@string/label_library" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:orientation="vertical">
<ImageView
android:id="@+id/genre_image"
android:layout_width="@dimen/size_cover_huge"
android:layout_height="@dimen/size_cover_huge"
android:layout_marginTop="@dimen/margin_medium"
android:contentDescription="@{@string/description_genre_image(genre.name)}"
app:genreImage="@{genre}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_genre" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/genre_name"
style="@style/DetailTitleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_medium"
android:layout_marginTop="@dimen/margin_medium"
android:layout_marginEnd="@dimen/margin_medium"
android:text="@{genre.displayName}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_image"
tools:text="Genre Name" />
<TextView
android:id="@+id/genre_song_count"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_medium"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
android:text="@{@plurals/format_song_count(genre.songs.size(), genre.songs.size())}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_name"
tools:text="80 Songs" />
<TextView
android:id="@+id/genre_duration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_medium"
android:text="@{genre.totalDuration}"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_song_count"
tools:text="16:16" />
<TextView
android:id="@+id/genre_song_header"
style="@style/HeaderText"
android:layout_marginTop="@dimen/padding_medium"
android:text="@string/label_songs"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_duration" />
<ImageButton
android:id="@+id/genre_sort_button"
style="@style/HeaderAction"
android:contentDescription="@string/description_sort_button"
android:onClick="@{() -> detailModel.incrementGenreSortMode()}"
app:layout_constraintBottom_toBottomOf="@+id/genre_song_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/genre_song_header"
tools:src="@drawable/ic_sort_alpha_down" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/genre_song_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_song_header"
tools:itemCount="4"
tools:listitem="@layout/item_genre_song" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
</layout>

View file

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="genre"
type="org.oxycblt.auxio.music.Genre" />
<variable
name="detailModel"
type="org.oxycblt.auxio.detail.DetailViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:orientation="vertical">
<ImageView
android:id="@+id/genre_image"
android:layout_width="@dimen/size_cover_huge"
android:layout_height="@dimen/size_cover_huge"
android:layout_marginTop="@dimen/margin_medium"
android:contentDescription="@{@string/description_genre_image(genre.name)}"
app:genreImage="@{genre}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_genre" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/genre_name"
style="@style/DetailTitleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_medium"
android:layout_marginTop="@dimen/margin_medium"
android:layout_marginEnd="@dimen/margin_medium"
android:text="@{genre.displayName}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_image"
tools:text="Genre Name" />
<TextView
android:id="@+id/genre_song_count"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_medium"
android:text="@{@plurals/format_song_count(genre.songs.size(), genre.songs.size())}"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_name"
tools:text="80 Songs" />
<TextView
android:id="@+id/genre_duration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_medium"
android:text="@{genre.totalDuration}"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_song_count"
tools:text="16:16" />
<TextView
android:id="@+id/genre_song_header"
style="@style/HeaderText"
android:layout_marginTop="@dimen/padding_medium"
android:text="@string/label_songs"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/genre_duration" />
<ImageButton
android:id="@+id/genre_sort_button"
style="@style/HeaderAction"
android:contentDescription="@string/description_sort_button"
android:onClick="@{() -> detailModel.incrementGenreSortMode()}"
app:layout_constraintBottom_toBottomOf="@+id/genre_song_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/genre_song_header"
app:sortIcon="@{detailModel.genreSortMode}"
tools:src="@drawable/ic_sort_alpha_down" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_shuffle"
android:icon="@drawable/ic_shuffle"
android:title="@string/label_shuffle"
app:showAsAction="ifRoom" />
</menu>

View file

@ -71,7 +71,7 @@
android:id="@+id/genre_detail_fragment"
android:name="org.oxycblt.auxio.detail.GenreDetailFragment"
android:label="GenreDetailFragment"
tools:layout="@layout/fragment_genre_detail">
tools:layout="@layout/fragment_detail">
<action
android:id="@+id/action_show_artist"
app:destination="@id/artist_detail_fragment"