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:
parent
09d4e107e0
commit
3bafc17d0c
9 changed files with 103 additions and 27 deletions
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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}"
|
||||||
|
|
|
@ -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>
|
|
@ -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"
|
||||||
|
|
||||||
|
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue