From 7e5cd2acd784ec5ab1efa865fe7c86be7b9a1a97 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 8 Jul 2023 12:11:51 +0300 Subject: [PATCH] Cover art carousel on playback fragment --- .../auxio/playback/PlaybackPanelFragment.kt | 52 +++++++++++++- .../playback/carousel/CoverCarouselAdapter.kt | 72 +++++++++++++++++++ .../layout-h480dp/fragment_playback_panel.xml | 7 +- .../fragment_playback_panel.xml | 7 +- .../res/layout/fragment_playback_panel.xml | 14 ++-- app/src/main/res/layout/item_cover.xml | 10 +++ 6 files changed, 141 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/playback/carousel/CoverCarouselAdapter.kt create mode 100644 app/src/main/res/layout/item_cover.xml diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index af1efd658..74834baf0 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -29,7 +29,11 @@ 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 kotlin.math.abs import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding import org.oxycblt.auxio.detail.DetailViewModel @@ -38,6 +42,8 @@ import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.resolveNames +import org.oxycblt.auxio.playback.carousel.CoverCarouselAdapter +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 @@ -64,7 +70,9 @@ class PlaybackPanelFragment : private val playbackModel: PlaybackViewModel by activityViewModels() private val musicModel: MusicViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels() + private val queueModel: QueueViewModel by activityViewModels() private var equalizerLauncher: ActivityResultLauncher? = null + private var coverAdapter: CoverCarouselAdapter? = null override fun onCreateBinding(inflater: LayoutInflater) = FragmentPlaybackPanelBinding.inflate(inflater) @@ -94,6 +102,13 @@ class PlaybackPanelFragment : setOnMenuItemClickListener(this@PlaybackPanelFragment) } + // cover carousel adapter + coverAdapter = CoverCarouselAdapter() + binding.playbackCoverPager.apply { + adapter = coverAdapter + registerOnPageChangeCallback(OnCoverChangedCallback(queueModel)) + } + // Set up marquee on song information, alongside click handlers that navigate to each // respective item. binding.playbackSong.apply { @@ -131,11 +146,14 @@ class PlaybackPanelFragment : collectImmediately(playbackModel.repeatMode, ::updateRepeat) collectImmediately(playbackModel.isPlaying, ::updatePlaying) collectImmediately(playbackModel.isShuffled, ::updateShuffled) + collectImmediately(queueModel.queue, ::updateQueue) + collectImmediately(queueModel.index, ::updateQueuePosition) collect(detailModel.toShow.flow, ::handleShow) } 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 @@ -194,6 +212,18 @@ class PlaybackPanelFragment : playbackModel.seekTo(positionDs) } + private fun updateQueue(queue: List) { + 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. @@ -203,7 +233,7 @@ class PlaybackPanelFragment : val binding = requireBinding() val context = requireContext() logD("Updating song display: $song") - binding.playbackCover.bind(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) @@ -257,4 +287,24 @@ class PlaybackPanelFragment : private fun navigateToCurrentAlbum() { playbackModel.song.value?.let { detailModel.showAlbum(it.album) } } + + 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) + } + } + } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/carousel/CoverCarouselAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/carousel/CoverCarouselAdapter.kt new file mode 100644 index 000000000..0102d5b50 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/carousel/CoverCarouselAdapter.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 Auxio Project + * CoverCarouselAdapter.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 . + */ + +package org.oxycblt.auxio.playback.carousel + +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import org.oxycblt.auxio.databinding.ItemCoverBinding +import org.oxycblt.auxio.list.adapter.FlexibleListAdapter +import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.util.inflater + +class CoverCarouselAdapter : + FlexibleListAdapter(CoverViewHolder.DIFF_CALLBACK) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CoverViewHolder { + return CoverViewHolder.from(parent) + } + + override fun onBindViewHolder(holder: CoverViewHolder, position: Int) { + holder.bind(getItem(position)) + } +} + +class CoverViewHolder private constructor(private val binding: ItemCoverBinding) : + RecyclerView.ViewHolder(binding.root) { + /** + * Bind new data to this instance. + * + * @param item The new [Song] to bind. + */ + fun bind(item: Song) { + binding.playbackCover.bind(item) + } + + companion object { + /** + * Create a new instance. + * + * @param parent The parent to inflate this instance from. + * @return A new instance. + */ + fun from(parent: ViewGroup) = + CoverViewHolder(ItemCoverBinding.inflate(parent.context.inflater, parent, false)) + + /** A comparator that can be used with DiffUtil. */ + val DIFF_CALLBACK = + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Song, newItem: Song) = + oldItem.uid == newItem.uid + + override fun areContentsTheSame(oldItem: Song, newItem: Song) = + oldItem.album.coverUri == newItem.album.coverUri + } + } +} diff --git a/app/src/main/res/layout-h480dp/fragment_playback_panel.xml b/app/src/main/res/layout-h480dp/fragment_playback_panel.xml index 263bf2a7d..234c0581e 100644 --- a/app/src/main/res/layout-h480dp/fragment_playback_panel.xml +++ b/app/src/main/res/layout-h480dp/fragment_playback_panel.xml @@ -16,12 +16,9 @@ app:title="@string/lbl_playback" tools:subtitle="@string/lbl_all_songs" /> - - - - +