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:
parent
c1ae32325f
commit
804db8b0d3
12 changed files with 485 additions and 302 deletions
|
@ -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.")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
97
app/src/main/res/layout-land/item_genre_header.xml
Normal file
97
app/src/main/res/layout-land/item_genre_header.xml
Normal 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>
|
32
app/src/main/res/layout/fragment_detail.xml
Normal file
32
app/src/main/res/layout/fragment_detail.xml
Normal 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>
|
|
@ -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>
|
94
app/src/main/res/layout/item_genre_header.xml
Normal file
94
app/src/main/res/layout/item_genre_header.xml
Normal 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>
|
9
app/src/main/res/menu/menu_genre_actions.xml
Normal file
9
app/src/main/res/menu/menu_genre_actions.xml
Normal 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>
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue