Add play button to AlbumDetailFragment

Add a play button to AlbumDetailFragment that shows the option to play the album, or pause/resume the playback if the album is already playing.
This commit is contained in:
OxygenCobalt 2020-10-14 10:10:57 -06:00
parent 9f05ce6e52
commit a64627c7cf
8 changed files with 185 additions and 12 deletions

View file

@ -5,6 +5,7 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
@ -48,10 +49,14 @@ class AlbumDetailFragment : Fragment() {
playbackModel.update(it, PlaybackMode.IN_ALBUM) playbackModel.update(it, PlaybackMode.IN_ALBUM)
} }
val playIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_play)
val pauseIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_pause)
// --- UI SETUP --- // --- UI SETUP ---
binding.lifecycleOwner = this binding.lifecycleOwner = this
binding.detailModel = detailModel binding.detailModel = detailModel
binding.playbackModel = playbackModel
binding.album = detailModel.currentAlbum.value!! binding.album = detailModel.currentAlbum.value!!
binding.albumSongRecycler.apply { binding.albumSongRecycler.apply {
@ -83,6 +88,17 @@ class AlbumDetailFragment : Fragment() {
) )
} }
// Observe playback model to update the play button
// TODO: Make these icons animated
// TODO: Shuffle button/option, unsure of which one
playbackModel.currentMode.observe(viewLifecycleOwner) {
updatePlayButton(it, binding)
}
playbackModel.isPlaying.observe(viewLifecycleOwner) {
updatePlayButton(playbackModel.currentMode.value!!, binding)
}
// If the album was shown directly from LibraryFragment, Then enable the ability to // If the album was shown directly from LibraryFragment, Then enable the ability to
// navigate upwards to the parent artist // navigate upwards to the parent artist
if (args.enableParentNav) { if (args.enableParentNav) {
@ -107,4 +123,31 @@ class AlbumDetailFragment : Fragment() {
return binding.root return binding.root
} }
// Update the play button depending on the current playback status
// If the shown album is currently playing, set the button to the current isPlaying status and
// its behavior to modify the current playing status
// If the shown album isn't currently playing, set the button to "Play" and its behavior
// to start the playback of the album.
private fun updatePlayButton(mode: PlaybackMode, binding: FragmentAlbumDetailBinding) {
playbackModel.currentSong.value?.let { song ->
if (mode == PlaybackMode.IN_ALBUM && song.album == detailModel.currentAlbum.value) {
if (playbackModel.isPlaying.value!!) {
binding.albumPlay.setImageResource(R.drawable.ic_pause)
} else {
binding.albumPlay.setImageResource(R.drawable.ic_play)
}
binding.albumPlay.setOnClickListener {
playbackModel.invertPlayingStatus()
}
} else {
binding.albumPlay.setImageResource(R.drawable.ic_play)
binding.albumPlay.setOnClickListener {
playbackModel.play(detailModel.currentAlbum.value!!, false)
}
}
}
}
} }

View file

@ -6,5 +6,13 @@ package org.oxycblt.auxio.playback
// IN_ARTIST -> Play from the songs of the artist // IN_ARTIST -> Play from the songs of the artist
// IN_ALBUM -> Play from the songs of the album // IN_ALBUM -> Play from the songs of the album
enum class PlaybackMode { enum class PlaybackMode {
ALL_SONGS, IN_GENRE, IN_ARTIST, IN_ALBUM IN_GENRE, IN_ARTIST, IN_ALBUM, ALL_SONGS;
// Make a slice of all the values that this ShowMode covers.
// ex. SHOW_ARTISTS would return SHOW_ARTISTS, SHOW_ALBUMS, and SHOW_SONGS
fun getChildren(): List<PlaybackMode> {
val vals = values()
return vals.slice(vals.indexOf(this) until vals.size)
}
} }

View file

@ -5,6 +5,9 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.toDuration import org.oxycblt.auxio.music.toDuration
@ -25,6 +28,7 @@ class PlaybackViewModel : ViewModel() {
val currentIndex: LiveData<Int> get() = mCurrentIndex val currentIndex: LiveData<Int> get() = mCurrentIndex
private val mCurrentMode = MutableLiveData(PlaybackMode.ALL_SONGS) private val mCurrentMode = MutableLiveData(PlaybackMode.ALL_SONGS)
val currentMode: LiveData<PlaybackMode> get() = mCurrentMode
private val mCurrentDuration = MutableLiveData(0L) private val mCurrentDuration = MutableLiveData(0L)
@ -63,7 +67,7 @@ class PlaybackViewModel : ViewModel() {
Log.d( Log.d(
this::class.simpleName, this::class.simpleName,
"update() was called with IN_GENRES, using " + "update() was called with IN_GENRES, using " +
"most prominent genre instead of the song's genre." "most prominent genre instead of the song's genre."
) )
song.album.artist.genres[0].songs song.album.artist.genres[0].songs
@ -74,6 +78,42 @@ class PlaybackViewModel : ViewModel() {
mCurrentIndex.value = mQueue.value!!.indexOf(song) mCurrentIndex.value = mQueue.value!!.indexOf(song)
} }
fun play(album: Album, isShuffled: Boolean) {
Log.d(this::class.simpleName, "Playing album ${album.name}")
val songs = orderSongsInAlbum(album)
updatePlayback(songs[0])
mQueue.value = songs
mCurrentIndex.value = 0
mCurrentMode.value = PlaybackMode.IN_ALBUM
}
fun play(artist: Artist, isShuffled: Boolean) {
Log.d(this::class.simpleName, "Playing artist ${artist.name}")
val songs = orderSongsInArtist(artist)
updatePlayback(songs[0])
mQueue.value = songs
mCurrentIndex.value = 0
mCurrentMode.value = PlaybackMode.IN_ARTIST
}
fun play(genre: Genre, isShuffled: Boolean) {
Log.d(this::class.simpleName, "Playing genre ${genre.name}")
val songs = orderSongsInGenre(genre)
updatePlayback(songs[0])
mQueue.value = songs
mCurrentIndex.value = 0
mCurrentMode.value = PlaybackMode.IN_GENRE
}
private fun updatePlayback(song: Song) { private fun updatePlayback(song: Song) {
mCurrentSong.value = song mCurrentSong.value = song
mCurrentDuration.value = 0 mCurrentDuration.value = 0
@ -83,6 +123,33 @@ class PlaybackViewModel : ViewModel() {
} }
} }
// Basic sorting functions when things are played in order
private fun orderSongsInAlbum(album: Album): MutableList<Song> {
return album.songs.sortedBy { it.track }.toMutableList()
}
private fun orderSongsInArtist(artist: Artist): MutableList<Song> {
val final = mutableListOf<Song>()
artist.albums.sortedByDescending { it.year }.forEach {
final.addAll(it.songs.sortedBy { it.track })
}
return final
}
private fun orderSongsInGenre(genre: Genre): MutableList<Song> {
val final = mutableListOf<Song>()
genre.artists.sortedBy { it.name }.forEach { artist ->
artist.albums.sortedByDescending { it.year }.forEach { album ->
final.addAll(album.songs.sortedBy { it.track })
}
}
return final
}
// Invert, not directly set the playing status // Invert, not directly set the playing status
fun invertPlayingStatus() { fun invertPlayingStatus() {
mIsPlaying.value = !mIsPlaying.value!! mIsPlaying.value = !mIsPlaying.value!!

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/background">
<group
android:pivotX="12"
android:pivotY="12"
android:rotation="-90">
<path
android:name="pause_upper"
android:fillColor="@android:color/white"
android:pathData="M6,7.5L6,10.5L18,10.5L18,7.5Z" />
<path
android:name="pause_lower"
android:fillColor="@android:color/white"
android:pathData="M6,16.5L6,13.5L18,13.5L18,16.5Z" />
</group>
</vector>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/background">
<path
android:fillColor="@android:color/white"
android:pathData="M8.25,6L8.25,12L18.75,12L18.75,12ZM8.25,18L8.25,12L18.75,12L18.75,12Z" />
</vector>

View file

@ -6,13 +6,17 @@
<data> <data>
<variable
name="album"
type="org.oxycblt.auxio.music.Album" />
<variable <variable
name="detailModel" name="detailModel"
type="org.oxycblt.auxio.detail.DetailViewModel" /> type="org.oxycblt.auxio.detail.DetailViewModel" />
<variable
name="playbackModel"
type="org.oxycblt.auxio.playback.PlaybackViewModel" />
<variable
name="album"
type="org.oxycblt.auxio.music.Album" />
</data> </data>
<LinearLayout <LinearLayout
@ -98,6 +102,23 @@
app:layout_constraintTop_toBottomOf="@+id/album_artist" app:layout_constraintTop_toBottomOf="@+id/album_artist"
tools:text="2020 / 10 Songs / 16:16" /> tools:text="2020 / 10 Songs / 16:16" />
<ImageButton
android:id="@+id/album_play"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="@dimen/margin_tiny"
android:layout_marginEnd="@dimen/margin_medium"
android:background="@drawable/ui_circular_button"
android:backgroundTint="?android:attr/colorPrimary"
android:contentDescription="@string/description_play"
android:src="@drawable/ic_play"
android:onClick="@{() -> playbackModel.play(album, false)}"
app:layout_constraintBottom_toBottomOf="@+id/album_details"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/album_artist" />
<TextView <TextView
android:id="@+id/album_song_header" android:id="@+id/album_song_header"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -121,15 +142,15 @@
android:layout_marginTop="@dimen/margin_medium" android:layout_marginTop="@dimen/margin_medium"
android:background="@drawable/ui_header_dividers" android:background="@drawable/ui_header_dividers"
android:contentDescription="@string/description_sort_button" android:contentDescription="@string/description_sort_button"
android:onClick="@{() -> detailModel.incrementAlbumSortMode()}"
android:paddingStart="@dimen/padding_medium" android:paddingStart="@dimen/padding_medium"
android:paddingTop="@dimen/padding_small" android:paddingTop="@dimen/padding_small"
android:paddingEnd="@dimen/margin_medium" android:paddingEnd="@dimen/margin_medium"
android:paddingBottom="@dimen/padding_small" android:paddingBottom="@dimen/padding_small"
android:onClick="@{() -> detailModel.incrementAlbumSortMode()}"
tools:src="@drawable/ic_sort_numeric_down"
app:layout_constraintBottom_toTopOf="@+id/album_song_recycler" app:layout_constraintBottom_toTopOf="@+id/album_song_recycler"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_details" /> app:layout_constraintTop_toBottomOf="@+id/album_details"
tools:src="@drawable/ic_sort_numeric_down" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/album_song_recycler" android:id="@+id/album_song_recycler"

View file

@ -9,6 +9,7 @@
<dimen name="padding_huge">64dp</dimen> <dimen name="padding_huge">64dp</dimen>
<!-- Margin namespace | Dimens for margin attributes --> <!-- Margin namespace | Dimens for margin attributes -->
<dimen name="margin_tiny">4dp</dimen>
<dimen name="margin_small">8dp</dimen> <dimen name="margin_small">8dp</dimen>
<dimen name="margin_mid_small">10dp</dimen> <dimen name="margin_mid_small">10dp</dimen>
<dimen name="margin_medium">16dp</dimen> <dimen name="margin_medium">16dp</dimen>

View file

@ -11,7 +11,7 @@
</style> </style>
<style name="Toolbar.Style" parent="ThemeOverlay.MaterialComponents.ActionBar"> <style name="Toolbar.Style" parent="ThemeOverlay.MaterialComponents.ActionBar">
<item name="android:searchViewStyle">@style/Toolbar.SearchViewStyle</item> <item name="android:searchViewStyle">@style/Widget.AppCompat.SearchView</item>
</style> </style>
<style name="TextAppearance.Toolbar.Header" parent="TextAppearance.Widget.AppCompat.Toolbar.Title"> <style name="TextAppearance.Toolbar.Header" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
@ -19,8 +19,6 @@
<item name="android:textColor">?android:attr/colorPrimary</item> <item name="android:textColor">?android:attr/colorPrimary</item>
</style> </style>
<style name="Toolbar.SearchViewStyle" parent="Widget.AppCompat.SearchView" />
<style name="DetailHeader"> <style name="DetailHeader">
<item name="android:textAppearance">?android:attr/textAppearanceLarge</item> <item name="android:textAppearance">?android:attr/textAppearanceLarge</item>
<item name="android:textColor">?android:attr/colorPrimary</item> <item name="android:textColor">?android:attr/colorPrimary</item>