detail: make album view use collapsing toolbar

This commit is contained in:
Alexander Capehart 2024-07-20 11:19:18 -06:00
parent 82a015c1e1
commit 86e2fd7a89
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
5 changed files with 474 additions and 151 deletions

View file

@ -23,14 +23,16 @@ import android.view.LayoutInflater
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.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.LinearSmoothScroller
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetail2Binding
import org.oxycblt.auxio.detail.header.AlbumDetailHeaderAdapter
import org.oxycblt.auxio.detail.list.AlbumDetailListAdapter import org.oxycblt.auxio.detail.list.AlbumDetailListAdapter
import org.oxycblt.auxio.detail.list.DetailListAdapter import org.oxycblt.auxio.detail.list.DetailListAdapter
import org.oxycblt.auxio.list.Divider import org.oxycblt.auxio.list.Divider
@ -46,12 +48,14 @@ import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.PlaylistDecision import org.oxycblt.auxio.music.PlaylistDecision
import org.oxycblt.auxio.music.PlaylistMessage import org.oxycblt.auxio.music.PlaylistMessage
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.resolveNames
import org.oxycblt.auxio.playback.PlaybackDecision import org.oxycblt.auxio.playback.PlaybackDecision
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.canScroll import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.getDimenPixels
import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.navigateSafe import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.overrideOnOverflowMenuClick import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
@ -66,9 +70,9 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class AlbumDetailFragment : class AlbumDetailFragment :
ListFragment<Song, FragmentDetailBinding>(), ListFragment<Song, FragmentDetail2Binding>(),
AlbumDetailHeaderAdapter.Listener, DetailListAdapter.Listener<Song>,
DetailListAdapter.Listener<Song> { AppBarLayout.OnOffsetChangedListener {
private val detailModel: DetailViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels()
override val listModel: ListViewModel by activityViewModels() override val listModel: ListViewModel by activityViewModels()
override val musicModel: MusicViewModel by activityViewModels() override val musicModel: MusicViewModel by activityViewModels()
@ -77,9 +81,10 @@ class AlbumDetailFragment :
// Information about what album to display is initially within the navigation arguments // Information about what album to display is initially within the navigation arguments
// as a UID, as that is the only safe way to parcel an album. // as a UID, as that is the only safe way to parcel an album.
private val args: AlbumDetailFragmentArgs by navArgs() private val args: AlbumDetailFragmentArgs by navArgs()
private val albumHeaderAdapter = AlbumDetailHeaderAdapter(this)
private val albumListAdapter = AlbumDetailListAdapter(this) private val albumListAdapter = AlbumDetailListAdapter(this)
private var spacingSmall = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// Detail transitions are always on the X axis. Shared element transitions are more // Detail transitions are always on the X axis. Shared element transitions are more
@ -90,15 +95,18 @@ class AlbumDetailFragment :
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
} }
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater) override fun onCreateBinding(inflater: LayoutInflater) =
FragmentDetail2Binding.inflate(inflater)
override fun getSelectionToolbar(binding: FragmentDetailBinding) = override fun getSelectionToolbar(binding: FragmentDetail2Binding) =
binding.detailSelectionToolbar binding.detailSelectionToolbar
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentDetail2Binding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
// --- UI SETUP -- // --- UI SETUP --
binding.detailAppbar.addOnOffsetChangedListener(this)
binding.detailNormalToolbar.apply { binding.detailNormalToolbar.apply {
setNavigationOnClickListener { findNavController().navigateUp() } setNavigationOnClickListener { findNavController().navigateUp() }
overrideOnOverflowMenuClick { overrideOnOverflowMenuClick {
@ -108,17 +116,23 @@ class AlbumDetailFragment :
} }
binding.detailRecycler.apply { binding.detailRecycler.apply {
adapter = ConcatAdapter(albumHeaderAdapter, albumListAdapter) adapter = albumListAdapter
(layoutManager as GridLayoutManager).setFullWidthLookup { (layoutManager as GridLayoutManager).setFullWidthLookup {
if (it != 0) { if (it != 0) {
val item = detailModel.albumSongList.value[it - 1] val item =
item is Divider || item is Header || item is Disc detailModel.genreSongList.value.getOrElse(it - 1) {
return@setFullWidthLookup false
}
item is Divider || item is Header
} else { } else {
true true
} }
} }
} }
spacingSmall = requireContext().getDimenPixels(R.dimen.spacing_small)
// -- VIEWMODEL SETUP --- // -- VIEWMODEL SETUP ---
// DetailViewModel handles most initialization from the navigation argument. // DetailViewModel handles most initialization from the navigation argument.
detailModel.setAlbum(args.albumUid) detailModel.setAlbum(args.albumUid)
@ -134,15 +148,31 @@ class AlbumDetailFragment :
collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision) collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision)
} }
override fun onDestroyBinding(binding: FragmentDetailBinding) { override fun onDestroyBinding(binding: FragmentDetail2Binding) {
super.onDestroyBinding(binding) super.onDestroyBinding(binding)
binding.detailNormalToolbar.setOnMenuItemClickListener(null)
binding.detailRecycler.adapter = null binding.detailRecycler.adapter = null
// Avoid possible race conditions that could cause a bad replace instruction to be consumed // Avoid possible race conditions that could cause a bad replace instruction to be consumed
// during list initialization and crash the app. Could happen if the user is fast enough. // during list initialization and crash the app. Could happen if the user is fast enough.
detailModel.albumSongInstructions.consume() detailModel.albumSongInstructions.consume()
} }
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
val binding = requireBinding()
val range = appBarLayout.totalScrollRange
val ratio = abs(verticalOffset.toFloat()) / range.toFloat()
val outRatio = min(ratio * 2, 1f)
val detailHeader = binding.detailHeader
detailHeader.scaleX = 1 - 0.05f * outRatio
detailHeader.scaleY = 1 - 0.05f * outRatio
detailHeader.alpha = 1 - outRatio
val inRatio = max(ratio - 0.5f, 0f) * 2
val detailContent = binding.detailToolbarContent
detailContent.alpha = inRatio
detailContent.translationY = spacingSmall * (1 - inRatio)
}
override fun onRealClick(item: Song) { override fun onRealClick(item: Song) {
playbackModel.play(item, detailModel.playInAlbumWith) playbackModel.play(item, detailModel.playInAlbumWith)
} }
@ -151,30 +181,50 @@ class AlbumDetailFragment :
listModel.openMenu(R.menu.album_song, item, detailModel.playInAlbumWith) listModel.openMenu(R.menu.album_song, item, detailModel.playInAlbumWith)
} }
override fun onPlay() {
playbackModel.play(unlikelyToBeNull(detailModel.currentAlbum.value))
}
override fun onShuffle() {
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentAlbum.value))
}
override fun onOpenSortMenu() { override fun onOpenSortMenu() {
findNavController().navigateSafe(AlbumDetailFragmentDirections.sort()) findNavController().navigateSafe(AlbumDetailFragmentDirections.sort())
} }
override fun onNavigateToParentArtist() {
detailModel.showArtist(unlikelyToBeNull(detailModel.currentAlbum.value))
}
private fun updateAlbum(album: Album?) { private fun updateAlbum(album: Album?) {
if (album == null) { if (album == null) {
logD("No album to show, navigating away") logD("No album to show, navigating away")
findNavController().navigateUp() findNavController().navigateUp()
return return
} }
requireBinding().detailNormalToolbar.title = album.name.resolve(requireContext())
albumHeaderAdapter.setParent(album) val binding = requireBinding()
binding.detailToolbarTitle.text = album.name.resolve(requireContext())
binding.detailCover.bind(album)
// The type text depends on the release type (Album, EP, Single, etc.)
binding.detailType.text = getString(album.releaseType.stringRes)
binding.detailName.text = album.name.resolve(requireContext())
// Artist name maps to the subhead text
binding.detailSubhead.apply {
text = album.artists.resolveNames(context)
// Add a QoL behavior where navigation to the artist will occur if the artist
// name is pressed.
setOnClickListener {
detailModel.showArtist(unlikelyToBeNull(detailModel.currentAlbum.value))
}
}
// Date, song count, and duration map to the info text
binding.detailInfo.apply {
// Fall back to a friendlier "No date" text if the album doesn't have date information
val date = album.dates?.resolveDate(context) ?: context.getString(R.string.def_date)
val songCount = context.getPlural(R.plurals.fmt_song_count, album.songs.size)
val duration = album.durationMs.formatDurationMs(true)
text = context.getString(R.string.fmt_three, date, songCount, duration)
}
binding.detailPlayButton.setOnClickListener {
playbackModel.play(unlikelyToBeNull(detailModel.currentAlbum.value))
}
binding.detailShuffleButton.setOnClickListener {
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentAlbum.value))
}
} }
private fun updateList(list: List<Item>) { private fun updateList(list: List<Item>) {
@ -318,6 +368,9 @@ class AlbumDetailFragment :
if (pos != -1) { if (pos != -1) {
// Only scroll if the song is within this album. // Only scroll if the song is within this album.
val binding = requireBinding() val binding = requireBinding()
// RecyclerView will scroll assuming it has the total height of the screen (i.e a
// collapsed appbar), so we need to collapse the appbar if that's the case.
binding.detailAppbar.setExpanded(false)
binding.detailRecycler.post { binding.detailRecycler.post {
// Use a custom smooth scroller that will settle the item in the middle of // Use a custom smooth scroller that will settle the item in the middle of
// the screen rather than the end. // the screen rather than the end.
@ -340,11 +393,6 @@ class AlbumDetailFragment :
// Make sure to increment the position to make up for the detail header // Make sure to increment the position to make up for the detail header
binding.detailRecycler.layoutManager?.startSmoothScroll(centerSmoothScroller) binding.detailRecycler.layoutManager?.startSmoothScroll(centerSmoothScroller)
// If the recyclerview can scroll, its certain that it will have to scroll to
// correctly center the playing item, so make sure that the Toolbar is lifted in
// that case.
binding.detailAppbar.isLifted = binding.detailRecycler.canScroll()
} }
} }
} }

View file

@ -556,7 +556,6 @@ constructor(
logD("Refreshing album list") logD("Refreshing album list")
val list = mutableListOf<Item>() val list = mutableListOf<Item>()
val header = SortHeader(R.string.lbl_songs) val header = SortHeader(R.string.lbl_songs)
list.add(Divider(header))
list.add(header) list.add(header)
val instructions = val instructions =
if (replace) { if (replace) {

View file

@ -1,114 +0,0 @@
/*
* Copyright (c) 2023 Auxio Project
* AlbumDetailHeaderAdapter.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.detail.header
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemDetailHeaderBinding
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.resolveNames
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater
/**
* A [DetailHeaderAdapter] that shows [Album] information.
*
* @param listener [DetailHeaderAdapter.Listener] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt)
*/
class AlbumDetailHeaderAdapter(private val listener: Listener) :
DetailHeaderAdapter<Album, AlbumDetailHeaderViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
AlbumDetailHeaderViewHolder.from(parent)
override fun onBindHeader(holder: AlbumDetailHeaderViewHolder, parent: Album) =
holder.bind(parent, listener)
/** An extended listener for [DetailHeaderAdapter] implementations. */
interface Listener : DetailHeaderAdapter.Listener {
/**
* Called when the artist name in the [Album] header was clicked, requesting navigation to
* it's parent artist.
*/
fun onNavigateToParentArtist()
}
}
/**
* A [RecyclerView.ViewHolder] that displays the [Album] header in the detail view. Use [from] to
* create an instance.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class AlbumDetailHeaderViewHolder
private constructor(private val binding: ItemDetailHeaderBinding) :
RecyclerView.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param album The new [Album] to bind.
* @param listener A [AlbumDetailHeaderAdapter.Listener] to bind interactions to.
*/
fun bind(album: Album, listener: AlbumDetailHeaderAdapter.Listener) {
binding.detailCover.bind(album)
// The type text depends on the release type (Album, EP, Single, etc.)
binding.detailType.text = binding.context.getString(album.releaseType.stringRes)
binding.detailName.text = album.name.resolve(binding.context)
// Artist name maps to the subhead text
binding.detailSubhead.apply {
text = album.artists.resolveNames(context)
// Add a QoL behavior where navigation to the artist will occur if the artist
// name is pressed.
setOnClickListener { listener.onNavigateToParentArtist() }
}
// Date, song count, and duration map to the info text
binding.detailInfo.apply {
// Fall back to a friendlier "No date" text if the album doesn't have date information
val date = album.dates?.resolveDate(context) ?: context.getString(R.string.def_date)
val songCount = context.getPlural(R.plurals.fmt_song_count, album.songs.size)
val duration = album.durationMs.formatDurationMs(true)
text = context.getString(R.string.fmt_three, date, songCount, duration)
}
binding.detailPlayButton.setOnClickListener { listener.onPlay() }
binding.detailShuffleButton.setOnClickListener { listener.onShuffle() }
}
companion object {
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/
fun from(parent: View) =
AlbumDetailHeaderViewHolder(ItemDetailHeaderBinding.inflate(parent.context.inflater))
}
}

View file

@ -0,0 +1,203 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
android:id="@+id/detail_coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
android:transitionGroup="true">
<org.oxycblt.auxio.ui.CoordinatorAppBarLayout
android:id="@+id/detail_appbar"
style="@style/Widget.Auxio.AppBarLayout"
app:liftOnScroll="true"
app:liftOnScrollTargetViewId="@id/detail_recycler">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/detail_collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:titleEnabled="false"
app:toolbarId="@+id/detail_toolbar">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/detail_header"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:paddingStart="@dimen/spacing_medium"
android:paddingTop="86dp"
android:paddingEnd="@dimen/spacing_medium"
android:paddingBottom="@dimen/spacing_mid_medium"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.85">
<org.oxycblt.auxio.image.CoverView
android:id="@+id/detail_cover"
style="@style/Widget.Auxio.Image.Large"
app:enablePlaybackIndicator="false"
app:enableSelectionBadge="false"
app:layout_constraintBottom_toTopOf="@+id/detail_play_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/detail_type"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:textAppearance="@style/TextAppearance.Auxio.LabelMedium"
android:textColor="?attr/colorSecondary"
app:layout_constraintBottom_toTopOf="@+id/detail_name"
app:layout_constraintStart_toEndOf="@+id/detail_cover"
app:layout_constraintTop_toTopOf="@+id/detail_cover"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Type" />
<TextView
android:id="@+id/detail_name"
style="@style/Widget.Auxio.TextView.Detail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:ellipsize="end"
android:maxLines="1"
app:layout_constraintBottom_toTopOf="@+id/detail_subhead"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/detail_cover"
app:layout_constraintTop_toBottomOf="@+id/detail_type"
tools:text="Name" />
<TextView
android:id="@+id/detail_subhead"
style="@style/Widget.Auxio.TextView.Secondary.Ellipsize"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:clickable="true"
android:focusable="true"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toTopOf="@+id/detail_info"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/detail_cover"
app:layout_constraintTop_toBottomOf="@+id/detail_name"
tools:text="Info A" />
<TextView
android:id="@+id/detail_info"
style="@style/Widget.Auxio.TextView.Secondary.Base"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toBottomOf="@+id/detail_cover"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/detail_cover"
app:layout_constraintTop_toBottomOf="@+id/detail_subhead"
tools:text="Info B" />
<org.oxycblt.auxio.ui.RippleFixMaterialButton
android:id="@+id/detail_play_button"
style="@style/Widget.Auxio.Button.Secondary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_mid_medium"
android:layout_marginEnd="@dimen/spacing_small"
android:text="@string/lbl_play"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/detail_shuffle_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/detail_cover" />
<org.oxycblt.auxio.ui.RippleFixMaterialButton
android:id="@+id/detail_shuffle_button"
style="@style/Widget.Auxio.Button.Primary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_small"
android:text="@string/lbl_shuffle"
app:layout_constraintBottom_toBottomOf="@+id/detail_play_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/detail_play_button"
app:layout_constraintTop_toTopOf="@+id/detail_play_button" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:layout_collapseMode="pin" />
<org.oxycblt.auxio.ui.MultiToolbar
android:id="@+id/detail_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_collapseMode="pin">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/detail_normal_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
app:menu="@menu/toolbar_detail"
app:navigationIcon="@drawable/ic_back_24">
<LinearLayout
android:id="@+id/detail_toolbar_content"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/detail_toolbar_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_tiny"
android:textAppearance="@style/TextAppearance.Auxio.TitleLarge"
android:textColor="?attr/colorOnSurface"
tools:text="Name" />
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/detail_selection_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
app:menu="@menu/toolbar_selection"
app:navigationIcon="@drawable/ic_close_24" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/detail_edit_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
app:menu="@menu/toolbar_edit"
app:navigationIcon="@drawable/ic_close_24" />
</org.oxycblt.auxio.ui.MultiToolbar>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</org.oxycblt.auxio.ui.CoordinatorAppBarLayout>
<org.oxycblt.auxio.list.recycler.AuxioRecyclerView
android:id="@+id/detail_recycler"
style="@style/Widget.Auxio.RecyclerView.Grid"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:listitem="@layout/item_song" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,187 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
android:id="@+id/detail_coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
android:transitionGroup="true">
<org.oxycblt.auxio.ui.CoordinatorAppBarLayout
android:id="@+id/detail_appbar"
style="@style/Widget.Auxio.AppBarLayout"
app:liftOnScroll="true"
app:liftOnScrollTargetViewId="@id/detail_recycler">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/detail_collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:titleEnabled="false"
app:toolbarId="@+id/detail_toolbar">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/detail_header"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="@dimen/spacing_medium"
android:paddingTop="86dp"
android:paddingEnd="@dimen/spacing_medium"
android:paddingBottom="@dimen/spacing_mid_medium"
app:layout_collapseMode="parallax"
android:layout_gravity="bottom"
app:layout_collapseParallaxMultiplier="0.85">
<org.oxycblt.auxio.image.CoverView
android:id="@+id/detail_cover"
style="@style/Widget.Auxio.Image.Huge"
app:enablePlaybackIndicator="false"
app:enableSelectionBadge="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/detail_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_medium"
android:textAppearance="@style/TextAppearance.Auxio.LabelMedium"
android:textColor="?attr/colorSecondary"
app:layout_constraintBottom_toTopOf="@+id/detail_name"
app:layout_constraintTop_toBottomOf="@+id/detail_cover"
tools:layout_editor_absoluteX="16dp"
tools:text="Type" />
<TextView
android:id="@+id/detail_name"
style="@style/Widget.Auxio.TextView.Detail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/detail_type"
tools:text="Name" />
<TextView
android:id="@+id/detail_subhead"
style="@style/Widget.Auxio.TextView.Secondary.Ellipsize"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/detail_name"
tools:text="Info A" />
<TextView
android:id="@+id/detail_info"
style="@style/Widget.Auxio.TextView.Secondary.Ellipsize"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/detail_subhead"
tools:text="Info B" />
<org.oxycblt.auxio.ui.RippleFixMaterialButton
android:id="@+id/detail_play_button"
style="@style/Widget.Auxio.Button.Secondary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_mid_medium"
android:layout_marginEnd="@dimen/spacing_small"
android:text="@string/lbl_play"
app:layout_constraintEnd_toStartOf="@+id/detail_shuffle_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/detail_info" />
<org.oxycblt.auxio.ui.RippleFixMaterialButton
android:id="@+id/detail_shuffle_button"
style="@style/Widget.Auxio.Button.Primary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_small"
android:text="@string/lbl_shuffle"
app:layout_constraintBottom_toBottomOf="@+id/detail_play_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/detail_play_button"
app:layout_constraintTop_toTopOf="@+id/detail_play_button" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:layout_collapseMode="pin" />
<org.oxycblt.auxio.ui.MultiToolbar
android:id="@+id/detail_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_collapseMode="pin" >
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/detail_normal_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
app:menu="@menu/toolbar_detail"
app:navigationIcon="@drawable/ic_back_24">
<LinearLayout
android:id="@+id/detail_toolbar_content"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/detail_toolbar_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_tiny"
android:textAppearance="@style/TextAppearance.Auxio.TitleLarge"
android:textColor="?attr/colorOnSurface"
tools:text="Name" />
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/detail_selection_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
app:menu="@menu/toolbar_selection"
app:navigationIcon="@drawable/ic_close_24" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/detail_edit_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
app:menu="@menu/toolbar_edit"
app:navigationIcon="@drawable/ic_close_24" />
</org.oxycblt.auxio.ui.MultiToolbar>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</org.oxycblt.auxio.ui.CoordinatorAppBarLayout>
<org.oxycblt.auxio.list.recycler.AuxioRecyclerView
android:id="@+id/detail_recycler"
style="@style/Widget.Auxio.RecyclerView.Grid"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:listitem="@layout/item_song" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>