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.appcompat.widget.Toolbar
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels 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 dagger.hilt.android.AndroidEntryPoint
import java.lang.reflect.Field
import kotlin.math.abs
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.list.ListViewModel import org.oxycblt.auxio.list.ListViewModel
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song 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.state.RepeatMode
import org.oxycblt.auxio.playback.ui.StyledSeekBar import org.oxycblt.auxio.playback.ui.StyledSeekBar
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.lazyReflectedField
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.overrideOnOverflowMenuClick import org.oxycblt.auxio.util.overrideOnOverflowMenuClick
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
@ -58,11 +66,14 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
class PlaybackPanelFragment : class PlaybackPanelFragment :
ViewBindingFragment<FragmentPlaybackPanelBinding>(), ViewBindingFragment<FragmentPlaybackPanelBinding>(),
Toolbar.OnMenuItemClickListener, Toolbar.OnMenuItemClickListener,
StyledSeekBar.Listener { StyledSeekBar.Listener,
PlaybackPageListener {
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
private val detailModel: DetailViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels()
private val queueModel: QueueViewModel by activityViewModels()
private val listModel: ListViewModel by activityViewModels() private val listModel: ListViewModel by activityViewModels()
private var equalizerLauncher: ActivityResultLauncher<Intent>? = null private var equalizerLauncher: ActivityResultLauncher<Intent>? = null
private var coverAdapter: PlaybackPagerAdapter? = null
override fun onCreateBinding(inflater: LayoutInflater) = override fun onCreateBinding(inflater: LayoutInflater) =
FragmentPlaybackPanelBinding.inflate(inflater) 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 // Set up marquee on song information, alongside click handlers that navigate to each
// respective item. // respective item.
binding.playbackSong.apply { binding.playbackSong.apply {
isSelected = true isSelected = true
setOnClickListener { playbackModel.song.value?.let(detailModel::showAlbum) } setOnClickListener { navigateToCurrentSong() }
} }
binding.playbackArtist.apply { binding.playbackArtist.apply {
isSelected = true isSelected = true
@ -131,15 +150,14 @@ class PlaybackPanelFragment :
collectImmediately(playbackModel.repeatMode, ::updateRepeat) collectImmediately(playbackModel.repeatMode, ::updateRepeat)
collectImmediately(playbackModel.isPlaying, ::updatePlaying) collectImmediately(playbackModel.isPlaying, ::updatePlaying)
collectImmediately(playbackModel.isShuffled, ::updateShuffled) collectImmediately(playbackModel.isShuffled, ::updateShuffled)
collectImmediately(queueModel.queue, ::updateQueue)
collectImmediately(queueModel.index, ::updateQueuePosition)
} }
override fun onDestroyBinding(binding: FragmentPlaybackPanelBinding) { override fun onDestroyBinding(binding: FragmentPlaybackPanelBinding) {
equalizerLauncher = null equalizerLauncher = null
coverAdapter = null
binding.playbackToolbar.setOnMenuItemClickListener(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 { override fun onMenuItemClick(item: MenuItem): Boolean {
@ -170,6 +188,18 @@ class PlaybackPanelFragment :
playbackModel.seekTo(positionDs) 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?) { private fun updateSong(song: Song?) {
if (song == null) { if (song == null) {
// Nothing to do. // Nothing to do.
@ -177,12 +207,7 @@ class PlaybackPanelFragment :
} }
val binding = requireBinding() val binding = requireBinding()
val context = requireContext()
logD("Updating song display: $song") 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() binding.playbackSeekBar.durationDs = song.durationMs.msToDs()
} }
@ -212,11 +237,43 @@ class PlaybackPanelFragment :
requireBinding().playbackShuffle.isActivated = isShuffled 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) playbackModel.song.value?.let(detailModel::showArtist)
} }
private fun navigateToCurrentAlbum() { override fun navigateToCurrentAlbum() {
playbackModel.song.value?.let { detailModel.showAlbum(it.album) } 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) { override fun onClick(item: Song, viewHolder: RecyclerView.ViewHolder) {
queueModel.goto(viewHolder.bindingAdapterPosition) queueModel.goto(viewHolder.bindingAdapterPosition, playIfPaused = true)
} }
override fun onPickUp(viewHolder: RecyclerView.ViewHolder) { 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 * @param adapterIndex The index of the queue item to play. Does nothing if the index is out of
* range. * 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) { if (adapterIndex !in queue.value.indices) {
return return
} }
logD("Going to position $adapterIndex in queue") 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. * Play a [Song] at the given position in the queue.
* *
* @param index The position of the [Song] in the queue to start playing. * @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. * Add [Song]s to the top of the queue.
@ -429,12 +430,12 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
} }
@Synchronized @Synchronized
override fun goto(index: Int) { override fun goto(index: Int, play: Boolean) {
val internalPlayer = internalPlayer ?: return val internalPlayer = internalPlayer ?: return
if (queue.goto(index)) { if (queue.goto(index)) {
logD("Moving to $index") logD("Moving to $index")
notifyIndexMoved() notifyIndexMoved()
internalPlayer.loadSong(queue.currentSong, true) internalPlayer.loadSong(queue.currentSong, play)
} else { } else {
logW("$index was not in bounds, could not move to it") logW("$index was not in bounds, could not move to it")
} }

View file

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

View file

@ -16,54 +16,14 @@
app:title="@string/lbl_playback" app:title="@string/lbl_playback"
tools:subtitle="@string/lbl_all_songs" /> tools:subtitle="@string/lbl_all_songs" />
<org.oxycblt.auxio.image.CoverView <androidx.viewpager2.widget.ViewPager2
android:id="@+id/playback_cover" android:id="@+id/playback_cover_pager"
style="@style/Widget.Auxio.Image.Full" android:layout_width="0dp"
android:layout_margin="@dimen/spacing_medium" android:layout_height="0dp"
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"
app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar" app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
tools:text="Album Name" /> app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" />
<org.oxycblt.auxio.playback.ui.StyledSeekBar <org.oxycblt.auxio.playback.ui.StyledSeekBar
android:id="@+id/playback_seek_bar" 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" app:title="@string/lbl_playback"
tools:subtitle="@string/lbl_all_songs" /> tools:subtitle="@string/lbl_all_songs" />
<org.oxycblt.auxio.image.CoverView <androidx.viewpager2.widget.ViewPager2
android:id="@+id/playback_cover" android:id="@+id/playback_cover_pager"
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="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="0dp"
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"
app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar" app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
tools:text="Album Name" /> app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" />
<org.oxycblt.auxio.playback.ui.StyledSeekBar <org.oxycblt.auxio.playback.ui.StyledSeekBar
android:id="@+id/playback_seek_bar" 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" app:title="@string/lbl_playback"
tools:subtitle="@string/lbl_all_songs" /> tools:subtitle="@string/lbl_all_songs" />
<org.oxycblt.auxio.image.CoverView <androidx.viewpager2.widget.ViewPager2
android:id="@+id/playback_cover" android:id="@+id/playback_cover_pager"
style="@style/Widget.Auxio.Image.Full" android:layout_width="0dp"
android:layout_marginStart="@dimen/spacing_medium" android:layout_height="0dp"
android:layout_marginTop="@dimen/spacing_medium"
app:enablePlaybackIndicator="false"
app:enableSelectionBadge="false"
app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar" app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" app:layout_constraintTop_toBottomOf="@+id/playback_toolbar"
app:layout_constraintVertical_chainStyle="packed" /> 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 <org.oxycblt.auxio.playback.ui.StyledSeekBar
android:id="@+id/playback_seek_bar" android:id="@+id/playback_seek_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/playback_controls_container" 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_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" /> 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>