Add durations to PlaybackFragment

Add the song duration & the current duration to PlaybackFragment, update the SeekBar in PlaybackFragment to reflect those durations.
This commit is contained in:
OxygenCobalt 2020-10-12 16:02:26 -06:00
parent 09d4e107e0
commit 3bafc17d0c
9 changed files with 103 additions and 27 deletions

View file

@ -48,11 +48,11 @@ dependencies {
// General // General
implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.activity:activity:1.2.0-beta01' implementation 'androidx.activity:activity-ktx:1.2.0-beta01'
implementation 'androidx.fragment:fragment:1.3.0-beta01' implementation 'androidx.fragment:fragment-ktx:1.3.0-beta01'
// Layout // Layout
implementation 'androidx.constraintlayout:constraintlayout:2.0.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
// Lifecycle // Lifecycle
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"

View file

@ -14,6 +14,7 @@ sealed class BaseModel {
} }
// Song // Song
// TODO: Maybe move durations to a solely-millis system
data class Song( data class Song(
override val id: Long = -1, override val id: Long = -1,
override var name: String, override var name: String,

View file

@ -3,9 +3,11 @@ package org.oxycblt.auxio.playback
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.drawable.AnimatedVectorDrawable import android.graphics.drawable.AnimatedVectorDrawable
import android.os.Bundle import android.os.Bundle
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 android.widget.SeekBar
import androidx.core.content.ContextCompat 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
@ -15,11 +17,11 @@ import org.oxycblt.auxio.databinding.FragmentPlaybackBinding
import org.oxycblt.auxio.theme.accent import org.oxycblt.auxio.theme.accent
import org.oxycblt.auxio.theme.toColor import org.oxycblt.auxio.theme.toColor
class PlaybackFragment : Fragment() { class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
// TODO: Implement media controls
// TODO: Implement nav to artists/albums // TODO: Implement nav to artists/albums
// TODO: Add a full playback fragment
// TODO: Possibly implement a trackbar with a spectrum shown as well. // TODO: Possibly implement a trackbar with a spectrum shown as well.
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -31,6 +33,7 @@ class PlaybackFragment : Fragment() {
// Create accents & icons to use // Create accents & icons to use
val accentColor = ColorStateList.valueOf(accent.first.toColor(requireContext())) val accentColor = ColorStateList.valueOf(accent.first.toColor(requireContext()))
val inactiveColor = ColorStateList.valueOf(R.color.control_color.toColor(requireContext())) val inactiveColor = ColorStateList.valueOf(R.color.control_color.toColor(requireContext()))
val normalTextColor = binding.playbackDurationCurrent.currentTextColor
val iconPauseToPlay = ContextCompat.getDrawable( val iconPauseToPlay = ContextCompat.getDrawable(
requireContext(), R.drawable.ic_pause_to_play requireContext(), R.drawable.ic_pause_to_play
@ -50,6 +53,7 @@ class PlaybackFragment : Fragment() {
// Make marquee scroll work // Make marquee scroll work
binding.playbackSong.isSelected = true binding.playbackSong.isSelected = true
binding.playbackSeekBar.setOnSeekBarChangeListener(this)
// --- VIEWMODEL SETUP -- // --- VIEWMODEL SETUP --
@ -73,6 +77,39 @@ class PlaybackFragment : Fragment() {
} }
} }
playbackModel.isSeeking.observe(viewLifecycleOwner) {
// Highlight the current duration if the user is seeking, and revert it if not.
if (it) {
binding.playbackDurationCurrent.setTextColor(accentColor)
} else {
binding.playbackDurationCurrent.setTextColor(normalTextColor)
}
}
// Updates for the current duration TextView/Seekbar
playbackModel.formattedCurrentDuration.observe(viewLifecycleOwner) {
binding.playbackDurationCurrent.text = it
}
playbackModel.formattedSeekBarProgress.observe(viewLifecycleOwner) {
binding.playbackSeekBar.progress = it
}
Log.d(this::class.simpleName, "Fragment Created.")
return binding.root return binding.root
} }
// Seeking callbacks
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
playbackModel.updateCurrentDurationWithProgress(progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
playbackModel.setSeekingStatus(true)
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
playbackModel.setSeekingStatus(false)
}
} }

View file

@ -2,8 +2,10 @@ package org.oxycblt.auxio.playback
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.toDuration
// TODO: Implement media controls // TODO: Implement media controls
// TODO: Add the playback service itself // TODO: Add the playback service itself
@ -13,12 +15,23 @@ class PlaybackViewModel : ViewModel() {
private val mCurrentSong = MutableLiveData<Song>() private val mCurrentSong = MutableLiveData<Song>()
val currentSong: LiveData<Song> get() = mCurrentSong val currentSong: LiveData<Song> get() = mCurrentSong
private val mShouldOpenPlayback = MutableLiveData<Boolean>() private val mCurrentDuration = MutableLiveData(0L)
val shouldOpenPlayback: LiveData<Boolean> get() = mShouldOpenPlayback val currentDuration: LiveData<Long> get() = mCurrentDuration
private val mIsPlaying = MutableLiveData(false) private val mIsPlaying = MutableLiveData(false)
val isPlaying: LiveData<Boolean> get() = mIsPlaying val isPlaying: LiveData<Boolean> get() = mIsPlaying
private val mIsSeeking = MutableLiveData(false)
val isSeeking: LiveData<Boolean> get() = mIsSeeking
val formattedCurrentDuration = Transformations.map(currentDuration) {
it.toDuration()
}
val formattedSeekBarProgress = Transformations.map(currentDuration) {
((it.toDouble() / mCurrentSong.value!!.seconds) * 100).toInt()
}
fun updateSong(song: Song) { fun updateSong(song: Song) {
mCurrentSong.value = song mCurrentSong.value = song
@ -27,16 +40,17 @@ class PlaybackViewModel : ViewModel() {
} }
} }
fun openPlayback() { // Invert, not directly set the playing status
mShouldOpenPlayback.value = true
}
fun doneWithOpenPlayback() {
mShouldOpenPlayback.value = false
}
// Invert, not directly set the p
fun invertPlayingStatus() { fun invertPlayingStatus() {
mIsPlaying.value = !mIsPlaying.value!! mIsPlaying.value = !mIsPlaying.value!!
} }
fun setSeekingStatus(status: Boolean) {
mIsSeeking.value = status
}
fun updateCurrentDurationWithProgress(progress: Int) {
mCurrentDuration.value =
((progress.toDouble() / 100) * mCurrentSong.value!!.seconds).toLong()
}
} }

View file

@ -16,6 +16,6 @@ https://stackoverflow.com/a/61157571/14143986
<item> <item>
<ripple <ripple
android:color="@color/selection_color" android:color="@color/selection_color"
android:radius="@dimen/size_divider_ripple"></ripple> android:radius="@dimen/size_divider_ripple" />
</item> </item>
</layer-list> </layer-list>

View file

@ -41,9 +41,9 @@
android:layout_height="0dp" android:layout_height="0dp"
android:layout_margin="@dimen/margin_mid_large" android:layout_margin="@dimen/margin_mid_large"
android:contentDescription="@{@string/description_album_cover(song.name)}" android:contentDescription="@{@string/description_album_cover(song.name)}"
app:layout_constraintDimensionRatio="1:1"
app:coverArt="@{song}" app:coverArt="@{song}"
app:layout_constraintBottom_toTopOf="@+id/playback_song" app:layout_constraintBottom_toTopOf="@+id/playback_song"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent" 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"
@ -88,10 +88,10 @@
android:id="@+id/playback_album" android:id="@+id/playback_album"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end"
android:layout_marginStart="@dimen/margin_mid_large" android:layout_marginStart="@dimen/margin_mid_large"
android:layout_marginEnd="@dimen/margin_mid_large" android:layout_marginEnd="@dimen/margin_mid_large"
android:layout_marginBottom="@dimen/margin_medium" android:layout_marginBottom="16dp"
android:ellipsize="end"
android:singleLine="true" android:singleLine="true"
android:text="@{song.album.name}" android:text="@{song.album.name}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1" android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
@ -105,23 +105,46 @@
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"
android:layout_marginBottom="@dimen/margin_medium"
android:paddingStart="@dimen/margin_mid_large" android:paddingStart="@dimen/margin_mid_large"
android:paddingEnd="@dimen/margin_mid_large" android:paddingEnd="@dimen/margin_mid_large"
android:thumbOffset="@dimen/offset_thumb"
android:clickable="true"
android:focusable="true"
android:progressBackgroundTint="?android:attr/colorControlNormal" android:progressBackgroundTint="?android:attr/colorControlNormal"
android:progressTint="?android:attr/colorPrimary" android:progressTint="?android:attr/colorPrimary"
android:splitTrack="false" android:splitTrack="false"
android:thumbTint="?android:attr/colorPrimary" android:thumbTint="?android:attr/colorPrimary"
app:layout_constraintBottom_toTopOf="@+id/playback_play_pause" app:layout_constraintBottom_toTopOf="@+id/playback_duration_current"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
tools:progress="70" /> tools:progress="70" />
<TextView
android:id="@+id/playback_duration_current"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_mid_large"
android:layout_marginBottom="@dimen/margin_medium"
app:layout_constraintBottom_toTopOf="@+id/playback_play_pause"
app:layout_constraintStart_toStartOf="parent"
tools:text="11:38" />
<TextView
android:id="@+id/playback_song_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/margin_mid_large"
android:layout_marginBottom="@dimen/margin_medium"
android:text="@{song.formattedDuration}"
app:layout_constraintBottom_toTopOf="@+id/playback_play_pause"
app:layout_constraintEnd_toEndOf="parent"
tools:text="16:16" />
<ImageButton <ImageButton
android:id="@+id/playback_play_pause" android:id="@+id/playback_play_pause"
android:layout_width="@dimen/size_play_pause" android:layout_width="@dimen/size_play_pause"
android:layout_height="@dimen/size_play_pause" android:layout_height="@dimen/size_play_pause"
android:layout_marginBottom="40dp" android:layout_marginBottom="30dp"
android:background="@drawable/ui_circular_button" android:background="@drawable/ui_circular_button"
android:backgroundTint="?android:attr/colorPrimary" android:backgroundTint="?android:attr/colorPrimary"
android:contentDescription="@{playbackModel.isPlaying ? @string/description_pause : @string/description_play}" android:contentDescription="@{playbackModel.isPlaying ? @string/description_pause : @string/description_play}"

View file

@ -41,4 +41,5 @@
<!-- Misc --> <!-- Misc -->
<dimen name="elevation_normal">4dp</dimen> <dimen name="elevation_normal">4dp</dimen>
<dimen name="offset_thumb">4dp</dimen>
</resources> </resources>

View file

@ -1,13 +1,13 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = "1.4.0" ext.kotlin_version = "1.4.10"
repositories { repositories {
google() google()
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.0.2' classpath 'com.android.tools.build:gradle:4.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0" classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0"

View file

@ -1,6 +1,6 @@
#Mon Aug 17 09:36:07 MDT 2020 #Mon Oct 12 13:43:13 MDT 2020
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip