From 5ebe17d0ad06cfe2bb0f92000a750d7b0e218623 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Tue, 5 Oct 2021 19:30:45 -0600 Subject: [PATCH] playback: make dedicated seekbar view Make a dedicated seekbar view so that does the layout magic necessary to have an adequate touch target while not taking up too much space. Isolating this makes handling the playback layout's view much easier. --- .../auxio/playback/PlaybackFragment.kt | 43 ++-------- .../oxycblt/auxio/playback/PlaybackSeekBar.kt | 79 +++++++++++++++++++ .../auxio/playback/PlaybackViewModel.kt | 35 +------- .../res/layout-land/fragment_playback.xml | 49 +++--------- .../layout-xlarge-land/fragment_playback.xml | 40 ++-------- .../res/layout-xlarge/fragment_playback.xml | 39 ++------- app/src/main/res/layout/fragment_playback.xml | 48 +++-------- app/src/main/res/layout/view_seek_bar.xml | 45 +++++++++++ app/src/main/res/values/styles_ui.xml | 2 - 9 files changed, 167 insertions(+), 213 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/playback/PlaybackSeekBar.kt create mode 100644 app/src/main/res/layout/view_seek_bar.xml 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