diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt index e27920402..99ea70a5c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt @@ -23,7 +23,7 @@ import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.widget.SeekBar +import androidx.core.view.iterator import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -43,7 +43,7 @@ import org.oxycblt.auxio.util.logD * also make material sliders usable maybe. * @author OxygenCobalt */ -class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { +class PlaybackFragment : Fragment() { private val playbackModel: PlaybackViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels() private val binding by memberBinding(FragmentPlaybackBinding::inflate) { @@ -90,9 +90,9 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { // Make marquee of song title work binding.playbackSong.isSelected = true - binding.playbackSeekBar.apply { - setOnSeekBarChangeListener(this@PlaybackFragment) - isEnabled = true + + binding.playbackSeekBar.onConfirmListener = { pos -> + playbackModel.setPosition(pos) } // --- VIEWMODEL SETUP -- @@ -102,7 +102,7 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { logD("Updating song display to ${song.name}.") binding.song = song - binding.playbackSeekBar.max = song.seconds.toInt() + binding.playbackSeekBar.setDuration(song.seconds) } else { logD("No song is being played, leaving.") @@ -124,14 +124,8 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { binding.playbackLoop.setImageResource(resId) } - playbackModel.isSeeking.observe(viewLifecycleOwner) { isSeeking -> - binding.playbackDurationCurrent.isActivated = isSeeking - } - - playbackModel.positionAsProgress.observe(viewLifecycleOwner) { pos -> - if (!playbackModel.isSeeking.value!!) { - binding.playbackSeekBar.progress = pos - } + playbackModel.position.observe(viewLifecycleOwner) { pos -> + binding.playbackSeekBar.setProgress(pos) } playbackModel.nextItemsInQueue.observe(viewLifecycleOwner) { @@ -165,25 +159,4 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { // inactive. We just need to set the flag. queueItem.isEnabled = !(userQueue.isEmpty() && nextQueue.isEmpty()) } - - // --- SEEK CALLBACKS --- - - override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { - if (fromUser) { - // Only update the display when seeking, as to have PlaybackService seek - // [causing possible buffering] on every movement is really odd. - playbackModel.updatePositionDisplay(progress) - } - } - - override fun onStartTrackingTouch(seekBar: SeekBar) { - playbackModel.setSeekingStatus(true) - } - - override fun onStopTrackingTouch(seekBar: SeekBar) { - playbackModel.setSeekingStatus(false) - - // Confirm the position when seeking stops. - playbackModel.setPosition(seekBar.progress) - } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSeekBar.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSeekBar.kt new file mode 100644 index 000000000..6519f9f80 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSeekBar.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2021 Auxio Project + * PlaybackSeeker.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 + +import android.content.Context +import android.util.AttributeSet +import android.widget.SeekBar +import androidx.constraintlayout.widget.ConstraintLayout +import org.oxycblt.auxio.databinding.ViewSeekBarBinding +import org.oxycblt.auxio.music.toDuration +import org.oxycblt.auxio.util.inflater + +/** + * A custom view that bundles together a seekbar with a current duration and a total duration. + * The sub-views are specifically laid out so that the seekbar has an adequate touch height while + * still not having gobs of whitespace everywhere. + * TODO: Fix the padding on this thing + * @author OxygenCobalt + */ +class PlaybackSeekBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleRes: Int = -1 +) : ConstraintLayout(context, attrs, defStyleRes), SeekBar.OnSeekBarChangeListener { + private val binding = ViewSeekBarBinding.inflate(context.inflater, this, true) + private val isSeeking: Boolean get() = binding.playbackDurationCurrent.isActivated + + var onConfirmListener: ((Long) -> Unit)? = null + + init { + binding.playbackSeekBar.setOnSeekBarChangeListener(this) + } + + fun setProgress(seconds: Long) { + // Don't update the progress while we are seeking, that will make the SeekBar jump around. + if (!isSeeking) { + binding.playbackSeekBar.progress = seconds.toInt() + binding.playbackDurationCurrent.text = seconds.toDuration() + } + } + + fun setDuration(seconds: Long) { + binding.playbackSeekBar.max = seconds.toInt() + binding.playbackSongDuration.text = seconds.toDuration() + } + + override fun onStartTrackingTouch(seekbar: SeekBar) { + binding.playbackDurationCurrent.isActivated = true + } + + override fun onStopTrackingTouch(seekbar: SeekBar) { + binding.playbackDurationCurrent.isActivated = false + onConfirmListener?.invoke(seekbar.progress.toLong()) + } + + override fun onProgressChanged(seekbar: SeekBar, value: Int, fromUser: Boolean) { + if (fromUser) { + // Don't actually seek yet when the user moves the progress bar, as to make our + // player seek during every movement is both inefficient and weird. + binding.playbackDurationCurrent.text = value.toLong().toDuration() + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 95ddccb8f..dacb1ed83 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -32,7 +32,6 @@ import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Parent import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.toDuration import org.oxycblt.auxio.playback.queue.QueueAdapter import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.playback.state.PlaybackMode @@ -68,7 +67,6 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { private val mIsInUserQueue = MutableLiveData(false) // Other - private val mIsSeeking = MutableLiveData(false) private var mIntentUri: Uri? = null /** The current song. */ @@ -92,13 +90,6 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { /** The current repeat mode, see [LoopMode] for more information */ val loopMode: LiveData get() = mLoopMode - val isSeeking: LiveData get() = mIsSeeking - - /** The position as a duration string. */ - val formattedPosition = Transformations.map(mPosition) { - it.toDuration() - } - /** The position as SeekBar progress. */ val positionAsProgress = Transformations.map(mPosition) { if (mSong.value != null) it.toInt() else 0 @@ -223,17 +214,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { /** * Update the position and push it to [PlaybackStateManager] */ - fun setPosition(progress: Int) { - playbackManager.seekTo((progress * 1000).toLong()) - } - - /** - * Update the position without pushing the change to [PlaybackStateManager]. - * This is used during seek events to give the user an idea of where they're seeking to. - * @param progress The SeekBar progress to seek to. - */ - fun updatePositionDisplay(progress: Int) { - mPosition.value = progress.toLong() + fun setPosition(progress: Long) { + playbackManager.seekTo((progress * 1000)) } // --- QUEUE FUNCTIONS --- @@ -428,15 +410,6 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { mLoopMode.value = playbackManager.loopMode } - // --- OTHER FUNCTIONS --- - - /** - * Set whether the seeking indicator should be highlighted - */ - fun setSeekingStatus(isSeeking: Boolean) { - mIsSeeking.value = isSeeking - } - // --- OVERRIDES --- override fun onCleared() { @@ -452,9 +425,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { } override fun onPositionUpdate(position: Long) { - if (!mIsSeeking.value!!) { - mPosition.value = position / 1000 - } + mPosition.value = position / 1000 } override fun onQueueUpdate(queue: List) { diff --git a/app/src/main/res/layout-land/fragment_playback.xml b/app/src/main/res/layout-land/fragment_playback.xml index 80a165152..417613444 100644 --- a/app/src/main/res/layout-land/fragment_playback.xml +++ b/app/src/main/res/layout-land/fragment_playback.xml @@ -104,55 +104,31 @@ app:layout_constraintTop_toBottomOf="@+id/playback_artist" tools:text="Album Name" /> - - - - - + app:layout_constraintTop_toBottomOf="@+id/playback_album" /> @@ -204,12 +177,12 @@ style="@style/Widget.Auxio.Button.Unbounded" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginEnd="@dimen/spacing_small" + android:layout_marginEnd="@dimen/spacing_medium" android:contentDescription="@string/desc_shuffle" android:onClick="@{() -> playbackModel.invertShuffleStatus()}" android:src="@drawable/ic_shuffle" app:layout_constraintBottom_toBottomOf="@+id/playback_skip_next" - app:layout_constraintEnd_toEndOf="@+id/playback_song_duration" + app:layout_constraintEnd_toEndOf="@+id/playback_seek_bar" app:layout_constraintTop_toTopOf="@+id/playback_skip_next" app:tint="@color/sel_accented" /> diff --git a/app/src/main/res/layout-xlarge-land/fragment_playback.xml b/app/src/main/res/layout-xlarge-land/fragment_playback.xml index 656c96d6d..dd88ab917 100644 --- a/app/src/main/res/layout-xlarge-land/fragment_playback.xml +++ b/app/src/main/res/layout-xlarge-land/fragment_playback.xml @@ -106,42 +106,17 @@ app:layout_constraintTop_toBottomOf="@+id/playback_artist" tools:text="Album Name" /> - - - - - + app:layout_constraintTop_toBottomOf="@+id/playback_album" /> diff --git a/app/src/main/res/layout-xlarge/fragment_playback.xml b/app/src/main/res/layout-xlarge/fragment_playback.xml index 299e0f90e..5a4ed2a8e 100644 --- a/app/src/main/res/layout-xlarge/fragment_playback.xml +++ b/app/src/main/res/layout-xlarge/fragment_playback.xml @@ -83,7 +83,6 @@ android:layout_height="wrap_content" android:layout_marginStart="@dimen/spacing_mid_huge" android:layout_marginEnd="@dimen/spacing_mid_huge" - android:layout_marginBottom="@dimen/spacing_medium" android:onClick="@{() -> detailModel.navToItem(playbackModel.song.album)}" android:text="@{song.album.name}" app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar" @@ -91,40 +90,15 @@ app:layout_constraintStart_toStartOf="parent" tools:text="Album Name" /> - - - - - + app:layout_constraintStart_toStartOf="parent" /> - - - - - + app:layout_constraintStart_toStartOf="parent" /> diff --git a/app/src/main/res/layout/view_seek_bar.xml b/app/src/main/res/layout/view_seek_bar.xml new file mode 100644 index 000000000..930ae5673 --- /dev/null +++ b/app/src/main/res/layout/view_seek_bar.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles_ui.xml b/app/src/main/res/values/styles_ui.xml index 61c611c3e..9fca85998 100644 --- a/app/src/main/res/values/styles_ui.xml +++ b/app/src/main/res/values/styles_ui.xml @@ -47,7 +47,6 @@ @@ -202,7 +201,6 @@ @dimen/size_btn_large @dimen/size_btn_large @drawable/ui_circle_ripple - @dimen/elevation_normal @string/desc_play_pause ?attr/colorSurface fitCenter