Merge pull request #503 from Koitharu/feature/cover_carousel

Cover art carousel on playback fragment
This commit is contained in:
Alexander Capehart 2023-08-21 07:50:31 -06:00 committed by GitHub
commit 83ec0c13da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 434 additions and 158 deletions

View file

@ -29,18 +29,26 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.Toolbar
import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import dagger.hilt.android.AndroidEntryPoint
import java.lang.reflect.Field
import kotlin.math.abs
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.list.ListViewModel
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.resolveNames
import org.oxycblt.auxio.playback.pager.PlaybackPageListener
import org.oxycblt.auxio.playback.pager.PlaybackPagerAdapter
import org.oxycblt.auxio.playback.queue.QueueViewModel
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.playback.ui.StyledSeekBar
import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.lazyReflectedField
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
import org.oxycblt.auxio.util.showToast
@ -58,11 +66,14 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
class PlaybackPanelFragment :
ViewBindingFragment<FragmentPlaybackPanelBinding>(),
Toolbar.OnMenuItemClickListener,
StyledSeekBar.Listener {
StyledSeekBar.Listener,
PlaybackPageListener {
private val playbackModel: PlaybackViewModel by activityViewModels()
private val detailModel: DetailViewModel by activityViewModels()
private val queueModel: QueueViewModel by activityViewModels()
private val listModel: ListViewModel by activityViewModels()
private var equalizerLauncher: ActivityResultLauncher<Intent>? = null
private var coverAdapter: PlaybackPagerAdapter? = null
override fun onCreateBinding(inflater: LayoutInflater) =
FragmentPlaybackPanelBinding.inflate(inflater)
@ -99,11 +110,19 @@ class PlaybackPanelFragment :
}
}
// cover carousel adapter
coverAdapter = PlaybackPagerAdapter(this, viewLifecycleOwner)
binding.playbackCoverPager.apply {
adapter = coverAdapter
registerOnPageChangeCallback(OnCoverChangedCallback(queueModel))
val recycler = VP_RECYCLER_FIELD.get(this@apply) as RecyclerView
recycler.isNestedScrollingEnabled = false
}
// Set up marquee on song information, alongside click handlers that navigate to each
// respective item.
binding.playbackSong.apply {
isSelected = true
setOnClickListener { playbackModel.song.value?.let(detailModel::showAlbum) }
setOnClickListener { navigateToCurrentSong() }
}
binding.playbackArtist.apply {
isSelected = true
@ -131,15 +150,14 @@ class PlaybackPanelFragment :
collectImmediately(playbackModel.repeatMode, ::updateRepeat)
collectImmediately(playbackModel.isPlaying, ::updatePlaying)
collectImmediately(playbackModel.isShuffled, ::updateShuffled)
collectImmediately(queueModel.queue, ::updateQueue)
collectImmediately(queueModel.index, ::updateQueuePosition)
}
override fun onDestroyBinding(binding: FragmentPlaybackPanelBinding) {
equalizerLauncher = null
coverAdapter = null
binding.playbackToolbar.setOnMenuItemClickListener(null)
// Marquee elements leak if they are not disabled when the views are destroyed.
binding.playbackSong.isSelected = false
binding.playbackArtist.isSelected = false
binding.playbackAlbum.isSelected = false
}
override fun onMenuItemClick(item: MenuItem): Boolean {
@ -170,6 +188,18 @@ class PlaybackPanelFragment :
playbackModel.seekTo(positionDs)
}
private fun updateQueue(queue: List<Song>) {
coverAdapter?.update(queue, queueModel.queueInstructions.flow.value)
}
private fun updateQueuePosition(position: Int) {
val pager = requireBinding().playbackCoverPager
val distance = abs(pager.currentItem - position)
if (distance != 0) {
pager.setCurrentItem(position, distance == 1)
}
}
private fun updateSong(song: Song?) {
if (song == null) {
// Nothing to do.
@ -177,12 +207,7 @@ class PlaybackPanelFragment :
}
val binding = requireBinding()
val context = requireContext()
logD("Updating song display: $song")
binding.playbackCover.bind(song)
binding.playbackSong.text = song.name.resolve(context)
binding.playbackArtist.text = song.artists.resolveNames(context)
binding.playbackAlbum.text = song.album.name.resolve(context)
binding.playbackSeekBar.durationDs = song.durationMs.msToDs()
}
@ -212,11 +237,43 @@ class PlaybackPanelFragment :
requireBinding().playbackShuffle.isActivated = isShuffled
}
private fun navigateToCurrentArtist() {
override fun navigateToCurrentSong() {
playbackModel.song.value?.let(detailModel::showAlbum)
}
override fun navigateToCurrentArtist() {
playbackModel.song.value?.let(detailModel::showArtist)
}
private fun navigateToCurrentAlbum() {
override fun navigateToCurrentAlbum() {
playbackModel.song.value?.let { detailModel.showAlbum(it.album) }
}
override fun navigateToMenu() {
binding?.playbackToolbar?.showOverflowMenu()
}
private class OnCoverChangedCallback(private val viewModel: QueueViewModel) :
OnPageChangeCallback() {
private var targetPosition = RecyclerView.NO_POSITION
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
targetPosition = position
}
override fun onPageScrollStateChanged(state: Int) {
super.onPageScrollStateChanged(state)
if (state == ViewPager2.SCROLL_STATE_IDLE &&
targetPosition != RecyclerView.NO_POSITION &&
targetPosition != viewModel.index.value) {
viewModel.goto(targetPosition, playIfPaused = false)
}
}
}
private companion object {
val VP_RECYCLER_FIELD: Field by lazyReflectedField(ViewPager2::class, "mRecyclerView")
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2023 Auxio Project
* PlaybackPageListener.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.playback.pager
interface PlaybackPageListener {
fun navigateToCurrentArtist()
fun navigateToCurrentAlbum()
fun navigateToCurrentSong()
fun navigateToMenu()
}

View file

@ -0,0 +1,142 @@
/*
* Copyright (c) 2023 Auxio Project
* PlaybackPagerAdapter.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.playback.pager
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import kotlin.jvm.internal.Intrinsics
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemPlaybackSongBinding
import org.oxycblt.auxio.list.adapter.FlexibleListAdapter
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.resolveNames
import org.oxycblt.auxio.util.inflater
class PlaybackPagerAdapter(
private val listener: PlaybackPageListener,
private val lifecycleOwner: LifecycleOwner
) : FlexibleListAdapter<Song, CoverViewHolder>(CoverViewHolder.DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CoverViewHolder {
return CoverViewHolder.from(parent, listener).also {
lifecycleOwner.lifecycle.addObserver(it)
}
}
override fun onBindViewHolder(holder: CoverViewHolder, position: Int) {
holder.bind(getItem(position))
}
override fun onViewRecycled(holder: CoverViewHolder) {
holder.recycle()
super.onViewRecycled(holder)
}
}
class CoverViewHolder
private constructor(
private val binding: ItemPlaybackSongBinding,
private val listener: PlaybackPageListener
) : RecyclerView.ViewHolder(binding.root), DefaultLifecycleObserver, View.OnClickListener {
init {
binding.playbackSong.setOnClickListener(this)
binding.playbackArtist.setOnClickListener(this)
binding.playbackAlbum.setOnClickListener(this)
binding.playbackCover.setOnClickListener(this)
}
override fun onClick(v: View) {
when (v.id) {
R.id.playback_album -> listener.navigateToCurrentAlbum()
R.id.playback_artist -> listener.navigateToCurrentArtist()
R.id.playback_song -> listener.navigateToCurrentSong()
R.id.playback_cover -> listener.navigateToMenu()
}
}
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
setSelected(true)
}
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
setSelected(false)
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
owner.lifecycle.removeObserver(this)
}
/**
* Bind new data to this instance.
*
* @param item The new [Song] to bind.
*/
fun bind(item: Song) {
binding.playbackCover.bind(item)
val context = binding.root.context
binding.playbackSong.text = item.name.resolve(context)
binding.playbackArtist.text = item.artists.resolveNames(context)
binding.playbackAlbum.text = item.album.name.resolve(context)
setSelected(true)
}
fun recycle() {
// Marquee elements leak if they are not disabled when the views are destroyed.
setSelected(false)
}
private fun setSelected(value: Boolean) {
binding.playbackSong.isSelected = value
binding.playbackArtist.isSelected = value
binding.playbackAlbum.isSelected = value
}
companion object {
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/
fun from(parent: ViewGroup, listener: PlaybackPageListener) =
CoverViewHolder(
ItemPlaybackSongBinding.inflate(parent.context.inflater, parent, false),
listener
)
/** A comparator that can be used with DiffUtil. */
val DIFF_CALLBACK =
object : DiffUtil.ItemCallback<Song>() {
override fun areItemsTheSame(oldItem: Song, newItem: Song) =
oldItem.uid == newItem.uid
override fun areContentsTheSame(oldItem: Song, newItem: Song): Boolean {
return Intrinsics.areEqual(oldItem, newItem)
}
}
}
}

View file

@ -88,7 +88,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), EditClickList
}
override fun onClick(item: Song, viewHolder: RecyclerView.ViewHolder) {
queueModel.goto(viewHolder.bindingAdapterPosition)
queueModel.goto(viewHolder.bindingAdapterPosition, playIfPaused = true)
}
override fun onPickUp(viewHolder: RecyclerView.ViewHolder) {

View file

@ -106,13 +106,14 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
*
* @param adapterIndex The index of the queue item to play. Does nothing if the index is out of
* range.
* @param playIfPaused Start playing after switching even if it currently is paused
*/
fun goto(adapterIndex: Int) {
fun goto(adapterIndex: Int, playIfPaused: Boolean) {
if (adapterIndex !in queue.value.indices) {
return
}
logD("Going to position $adapterIndex in queue")
playbackManager.goto(adapterIndex)
playbackManager.goto(adapterIndex, playIfPaused || playbackManager.playerState.isPlaying)
}
/**

View file

@ -120,8 +120,9 @@ interface PlaybackStateManager {
* Play a [Song] at the given position in the queue.
*
* @param index The position of the [Song] in the queue to start playing.
* @param play Whether to start playing after switching to target index
*/
fun goto(index: Int)
fun goto(index: Int, play: Boolean)
/**
* Add [Song]s to the top of the queue.
@ -429,12 +430,12 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
}
@Synchronized
override fun goto(index: Int) {
override fun goto(index: Int, play: Boolean) {
val internalPlayer = internalPlayer ?: return
if (queue.goto(index)) {
logD("Moving to $index")
notifyIndexMoved()
internalPlayer.loadSong(queue.currentSong, true)
internalPlayer.loadSong(queue.currentSong, play)
} else {
logW("$index was not in bounds, could not move to it")
}

View file

@ -256,7 +256,7 @@ constructor(
}
override fun onSkipToQueueItem(id: Long) {
playbackManager.goto(id.toInt())
playbackManager.goto(id.toInt(), true)
}
override fun onCustomAction(action: String?, extras: Bundle?) {

View file

@ -16,54 +16,14 @@
app:title="@string/lbl_playback"
tools:subtitle="@string/lbl_all_songs" />
<org.oxycblt.auxio.image.CoverView
android:id="@+id/playback_cover"
style="@style/Widget.Auxio.Image.Full"
android:layout_margin="@dimen/spacing_medium"
app:enablePlaybackIndicator="false"
app:enableSelectionBadge="false"
app:layout_constraintBottom_toTopOf="@+id/playback_song"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" />
<TextView
android:id="@+id/playback_song"
style="@style/Widget.Auxio.TextView.Primary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
app:layout_constraintBottom_toTopOf="@+id/playback_artist"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Song Name" />
<TextView
android:id="@+id/playback_artist"
style="@style/Widget.Auxio.TextView.Secondary.Marquee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
app:layout_constraintBottom_toTopOf="@+id/playback_album"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="Artist Name" />
<TextView
android:id="@+id/playback_album"
style="@style/Widget.Auxio.TextView.Secondary.Marquee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/playback_cover_pager"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="Album Name" />
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" />
<org.oxycblt.auxio.playback.ui.StyledSeekBar
android:id="@+id/playback_seek_bar"

View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
android:layout_height="match_parent">
<org.oxycblt.auxio.image.CoverView
android:id="@+id/playback_cover"
style="@style/Widget.Auxio.Image.Full"
android:layout_margin="@dimen/spacing_medium"
app:enablePlaybackIndicator="false"
app:enableSelectionBadge="false"
app:layout_constraintBottom_toTopOf="@id/playback_song"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/playback_song"
style="@style/Widget.Auxio.TextView.Primary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
app:layout_constraintBottom_toTopOf="@+id/playback_artist"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Song Name" />
<TextView
android:id="@+id/playback_artist"
style="@style/Widget.Auxio.TextView.Secondary.Marquee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
app:layout_constraintBottom_toTopOf="@+id/playback_album"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="Artist Name" />
<TextView
android:id="@+id/playback_album"
style="@style/Widget.Auxio.TextView.Secondary.Marquee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="Album Name" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -16,54 +16,14 @@
app:title="@string/lbl_playback"
tools:subtitle="@string/lbl_all_songs" />
<org.oxycblt.auxio.image.CoverView
android:id="@+id/playback_cover"
style="@style/Widget.Auxio.Image.Full"
android:layout_margin="@dimen/spacing_medium"
app:enablePlaybackIndicator="false"
app:enableSelectionBadge="false"
app:layout_constraintBottom_toTopOf="@+id/playback_song"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" />
<TextView
android:id="@+id/playback_song"
style="@style/Widget.Auxio.TextView.Primary"
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/playback_cover_pager"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
app:layout_constraintBottom_toTopOf="@+id/playback_artist"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Song Name" />
<TextView
android:id="@+id/playback_artist"
style="@style/Widget.Auxio.TextView.Secondary.Marquee"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
app:layout_constraintBottom_toTopOf="@+id/playback_album"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="Artist Name" />
<TextView
android:id="@+id/playback_album"
style="@style/Widget.Auxio.TextView.Secondary.Marquee"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="Album Name" />
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" />
<org.oxycblt.auxio.playback.ui.StyledSeekBar
android:id="@+id/playback_seek_bar"

View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
android:layout_height="match_parent">
<org.oxycblt.auxio.image.CoverView
android:id="@+id/playback_cover"
style="@style/Widget.Auxio.Image.Full"
android:layout_margin="@dimen/spacing_medium"
app:enablePlaybackIndicator="false"
app:enableSelectionBadge="false"
app:layout_constraintBottom_toTopOf="@id/playback_song"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/playback_song"
style="@style/Widget.Auxio.TextView.Primary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
app:layout_constraintBottom_toTopOf="@+id/playback_artist"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Song Name" />
<TextView
android:id="@+id/playback_artist"
style="@style/Widget.Auxio.TextView.Secondary.Marquee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
app:layout_constraintBottom_toTopOf="@+id/playback_album"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="Artist Name" />
<TextView
android:id="@+id/playback_album"
style="@style/Widget.Auxio.TextView.Secondary.Marquee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="Album Name" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -16,64 +16,22 @@
app:title="@string/lbl_playback"
tools:subtitle="@string/lbl_all_songs" />
<org.oxycblt.auxio.image.CoverView
android:id="@+id/playback_cover"
style="@style/Widget.Auxio.Image.Full"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginTop="@dimen/spacing_medium"
app:enablePlaybackIndicator="false"
app:enableSelectionBadge="false"
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/playback_cover_pager"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar"
app:layout_constraintVertical_chainStyle="packed" />
<!-- Playback information is wrapped in a container so that marquee doesn't break -->
<LinearLayout
android:id="@+id/playback_info_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/playback_cover"
app:layout_constraintTop_toTopOf="@+id/playback_cover"
app:layout_constraintVertical_chainStyle="packed">
<TextView
android:id="@+id/playback_song"
style="@style/Widget.Auxio.TextView.Primary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Song Name" />
<TextView
android:id="@+id/playback_artist"
style="@style/Widget.Auxio.TextView.Secondary.Marquee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Artist Name" />
<TextView
android:id="@+id/playback_album"
style="@style/Widget.Auxio.TextView.Secondary.Marquee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Album Name" />
</LinearLayout>
<org.oxycblt.auxio.playback.ui.StyledSeekBar
android:id="@+id/playback_seek_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/playback_controls_container"
app:layout_constraintEnd_toEndOf="@+id/playback_info_container"
app:layout_constraintEnd_toEndOf="@+id/playback_cover_pager"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" />

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
android:layout_height="match_parent">
<org.oxycblt.auxio.image.CoverView
android:id="@+id/playback_cover"
style="@style/Widget.Auxio.Image.Full"
android:layout_margin="@dimen/spacing_medium"
app:enablePlaybackIndicator="false"
app:enableSelectionBadge="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- Playback information is wrapped in a container so that marquee doesn't break -->
<LinearLayout
android:id="@+id/playback_info_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/spacing_medium"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/playback_cover"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed">
<TextView
android:id="@+id/playback_song"
style="@style/Widget.Auxio.TextView.Primary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Song Name" />
<TextView
android:id="@+id/playback_artist"
style="@style/Widget.Auxio.TextView.Secondary.Marquee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Artist Name" />
<TextView
android:id="@+id/playback_album"
style="@style/Widget.Auxio.TextView.Secondary.Marquee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Album Name" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>