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

View file

@ -3,8 +3,14 @@ package org.oxycblt.auxio.detail.adapters
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.ListAdapter 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.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.music.Song
import org.oxycblt.auxio.recycler.DiffCallback import org.oxycblt.auxio.recycler.DiffCallback
import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder 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. * An adapter for displaying the [Song]s of a genre.
*/ */
class GenreSongAdapter( class GenreSongAdapter(
private val lifecycleOwner: LifecycleOwner,
private val detailModel: DetailViewModel,
private val doOnClick: (data: Song) -> Unit, private val doOnClick: (data: Song) -> Unit,
private val doOnLongClick: (data: Song, view: View) -> 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 { override fun getItemViewType(position: Int): Int {
return GenreSongViewHolder( return when (getItem(position)) {
ItemGenreSongBinding.inflate(LayoutInflater.from(parent.context)), is Genre -> GENRE_HEADER_ITEM_TYPE
doOnClick, doOnLongClick is Song -> GENRE_SONG_ITEM_TYPE
)
else -> -1
}
} }
override fun onBindViewHolder(holder: GenreSongViewHolder, position: Int) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
holder.bind(getItem(position)) 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( inner class GenreSongViewHolder(
private val binding: ItemGenreSongBinding, private val binding: ItemGenreSongBinding,
doOnClick: (data: Song) -> Unit,
doOnLongClick: (data: Song, view: View) -> Unit
) : BaseViewHolder<Song>(binding, doOnClick, doOnLongClick) { ) : BaseViewHolder<Song>(binding, doOnClick, doOnLongClick) {
override fun onBind(data: Song) { override fun onBind(data: Song) {
@ -41,4 +76,9 @@ class GenreSongAdapter(
binding.songInfo.requestLayout() 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.net.Uri
import android.provider.MediaStore import android.provider.MediaStore
import android.text.format.DateUtils import android.text.format.DateUtils
import android.widget.ImageButton
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.logD
import org.oxycblt.auxio.recycler.SortMode
/** /**
* 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.
@ -166,3 +169,12 @@ fun TextView.bindAlbumInfo(album: Album) {
fun TextView.bindAlbumYear(album: Album) { fun TextView.bindAlbumYear(album: Album) {
text = album.year.toYear(context) 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 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 true
} }
R.id.action_play_all_songs -> {
playbackModel.playSong(song, PlaybackMode.ALL_SONGS)
true
}
else -> false 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:id="@+id/genre_detail_fragment"
android:name="org.oxycblt.auxio.detail.GenreDetailFragment" android:name="org.oxycblt.auxio.detail.GenreDetailFragment"
android:label="GenreDetailFragment" android:label="GenreDetailFragment"
tools:layout="@layout/fragment_genre_detail"> tools:layout="@layout/fragment_detail">
<action <action
android:id="@+id/action_show_artist" android:id="@+id/action_show_artist"
app:destination="@id/artist_detail_fragment" app:destination="@id/artist_detail_fragment"