playback: implement "safe" slider wrapper
Implement a safe slider wrapper that does not crash with invalid values as often. Slider is a terrible component that is not designed with Auxio's use-case in the slightest. Instead of gracefully degrading with invalid values, it just crashes the entire app, which is horrible for UX. Since SeekBar is a useless buggy version-specific sh******ed mess too, we have no choice but to wrap Slider in a safe view layout that hopefully hacks with the input enough to not crash the app when doing simple seeking actions. I hate android so much. Resolves #140.
This commit is contained in:
parent
852630ab38
commit
c6d7d8fe39
12 changed files with 201 additions and 211 deletions
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
## dev [v2.3.1, v2.4.0, or v3.0.0]
|
## dev [v2.3.1, v2.4.0, or v3.0.0]
|
||||||
|
|
||||||
|
#### What's Fixed
|
||||||
|
- Fixed crash when seeking to the end of a track as the track changed to a track with a lower duration
|
||||||
|
|
||||||
## v2.3.0
|
## v2.3.0
|
||||||
|
|
||||||
#### What's New
|
#### What's New
|
||||||
|
|
|
@ -102,6 +102,7 @@ data class Song(
|
||||||
val uri: Uri
|
val uri: Uri
|
||||||
get() =
|
get() =
|
||||||
ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, _mediaStoreId)
|
ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, _mediaStoreId)
|
||||||
|
|
||||||
/** The duration of this song, in seconds (rounded down) */
|
/** The duration of this song, in seconds (rounded down) */
|
||||||
val durationSecs: Long
|
val durationSecs: Long
|
||||||
get() = durationMs / 1000
|
get() = durationMs / 1000
|
||||||
|
|
|
@ -57,6 +57,10 @@ class MusicViewModel : ViewModel(), MusicStore.Callback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload the music library. Note that this call will result in unexpected behavior in the case
|
||||||
|
* that music is reloaded after a loading process has already exceeded.
|
||||||
|
*/
|
||||||
fun reloadMusic(context: Context) {
|
fun reloadMusic(context: Context) {
|
||||||
logD("Reloading music library")
|
logD("Reloading music library")
|
||||||
_loaderResponse.value = null
|
_loaderResponse.value = null
|
||||||
|
|
|
@ -24,8 +24,6 @@ import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import com.google.android.material.slider.Slider
|
|
||||||
import kotlin.math.max
|
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
|
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
|
@ -34,7 +32,6 @@ import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.formatDuration
|
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
import org.oxycblt.auxio.util.textSafe
|
import org.oxycblt.auxio.util.textSafe
|
||||||
|
@ -50,8 +47,7 @@ import org.oxycblt.auxio.util.textSafe
|
||||||
*/
|
*/
|
||||||
class PlaybackPanelFragment :
|
class PlaybackPanelFragment :
|
||||||
ViewBindingFragment<FragmentPlaybackPanelBinding>(),
|
ViewBindingFragment<FragmentPlaybackPanelBinding>(),
|
||||||
Slider.OnChangeListener,
|
StyledSeekBar.Callback,
|
||||||
Slider.OnSliderTouchListener,
|
|
||||||
Toolbar.OnMenuItemClickListener {
|
Toolbar.OnMenuItemClickListener {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val navModel: NavigationViewModel by activityViewModels()
|
private val navModel: NavigationViewModel by activityViewModels()
|
||||||
|
@ -93,10 +89,7 @@ class PlaybackPanelFragment :
|
||||||
playbackModel.song.value?.let { navModel.exploreNavigateTo(it.album) }
|
playbackModel.song.value?.let { navModel.exploreNavigateTo(it.album) }
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.playbackSeekBar.apply {
|
binding.playbackSeekBar.callback = this
|
||||||
addOnChangeListener(this@PlaybackPanelFragment)
|
|
||||||
addOnSliderTouchListener(this@PlaybackPanelFragment)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.playbackRepeat.setOnClickListener { playbackModel.incrementRepeatMode() }
|
binding.playbackRepeat.setOnClickListener { playbackModel.incrementRepeatMode() }
|
||||||
binding.playbackSkipPrev.setOnClickListener { playbackModel.prev() }
|
binding.playbackSkipPrev.setOnClickListener { playbackModel.prev() }
|
||||||
|
@ -110,8 +103,6 @@ class PlaybackPanelFragment :
|
||||||
binding.playbackSkipNext.setOnClickListener { playbackModel.next() }
|
binding.playbackSkipNext.setOnClickListener { playbackModel.next() }
|
||||||
binding.playbackShuffle.setOnClickListener { playbackModel.invertShuffled() }
|
binding.playbackShuffle.setOnClickListener { playbackModel.invertShuffled() }
|
||||||
|
|
||||||
binding.playbackSeekBar.apply {}
|
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP --
|
// --- VIEWMODEL SETUP --
|
||||||
|
|
||||||
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
playbackModel.song.observe(viewLifecycleOwner, ::updateSong)
|
||||||
|
@ -133,8 +124,7 @@ class PlaybackPanelFragment :
|
||||||
override fun onDestroyBinding(binding: FragmentPlaybackPanelBinding) {
|
override fun onDestroyBinding(binding: FragmentPlaybackPanelBinding) {
|
||||||
binding.playbackToolbar.setOnMenuItemClickListener(null)
|
binding.playbackToolbar.setOnMenuItemClickListener(null)
|
||||||
binding.playbackSong.isSelected = false
|
binding.playbackSong.isSelected = false
|
||||||
binding.playbackSeekBar.removeOnChangeListener(this)
|
binding.playbackSeekBar.callback = null
|
||||||
binding.playbackSeekBar.removeOnChangeListener(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
|
@ -147,19 +137,8 @@ class PlaybackPanelFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartTrackingTouch(slider: Slider) {
|
override fun seekTo(positionSecs: Long) {
|
||||||
requireBinding().playbackPosition.isActivated = true
|
playbackModel.seekTo(positionSecs)
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStopTrackingTouch(slider: Slider) {
|
|
||||||
requireBinding().playbackPosition.isActivated = false
|
|
||||||
playbackModel.seekTo(slider.value.toLong())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
|
||||||
if (fromUser) {
|
|
||||||
requireBinding().playbackPosition.textSafe = value.toLong().formatDuration(true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSong(song: Song?) {
|
private fun updateSong(song: Song?) {
|
||||||
|
@ -171,14 +150,7 @@ class PlaybackPanelFragment :
|
||||||
binding.playbackSong.textSafe = song.resolveName(context)
|
binding.playbackSong.textSafe = song.resolveName(context)
|
||||||
binding.playbackArtist.textSafe = song.resolveIndividualArtistName(context)
|
binding.playbackArtist.textSafe = song.resolveIndividualArtistName(context)
|
||||||
binding.playbackAlbum.textSafe = song.album.resolveName(context)
|
binding.playbackAlbum.textSafe = song.album.resolveName(context)
|
||||||
|
binding.playbackSeekBar.durationSecs = song.durationSecs
|
||||||
// Normally if a song had a duration
|
|
||||||
val seconds = song.durationSecs
|
|
||||||
binding.playbackDuration.textSafe = seconds.formatDuration(false)
|
|
||||||
binding.playbackSeekBar.apply {
|
|
||||||
isEnabled = seconds > 0L
|
|
||||||
valueTo = max(seconds, 1L).toFloat()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateParent(parent: MusicParent?) {
|
private fun updateParent(parent: MusicParent?) {
|
||||||
|
@ -186,14 +158,8 @@ class PlaybackPanelFragment :
|
||||||
parent?.resolveName(requireContext()) ?: getString(R.string.lbl_all_songs)
|
parent?.resolveName(requireContext()) ?: getString(R.string.lbl_all_songs)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePosition(position: Long) {
|
private fun updatePosition(positionSecs: Long) {
|
||||||
// Don't update the progress while we are seeking, that will make the SeekBar jump
|
requireBinding().playbackSeekBar.positionSecs = positionSecs
|
||||||
// around.
|
|
||||||
val binding = requireBinding()
|
|
||||||
if (!binding.playbackPosition.isActivated) {
|
|
||||||
binding.playbackSeekBar.value = position.toFloat()
|
|
||||||
binding.playbackPosition.textSafe = position.formatDuration(true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateRepeat(repeatMode: RepeatMode) {
|
private fun updateRepeat(repeatMode: RepeatMode) {
|
||||||
|
|
125
app/src/main/java/org/oxycblt/auxio/playback/StyledSeekBar.kt
Normal file
125
app/src/main/java/org/oxycblt/auxio/playback/StyledSeekBar.kt
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.oxycblt.auxio.playback
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import com.google.android.material.slider.Slider
|
||||||
|
import kotlin.math.max
|
||||||
|
import org.oxycblt.auxio.databinding.ViewSeekBarBinding
|
||||||
|
import org.oxycblt.auxio.util.formatDuration
|
||||||
|
import org.oxycblt.auxio.util.inflater
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
|
import org.oxycblt.auxio.util.textSafe
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper around [Slider] that shows not only position and duration values, but also basically
|
||||||
|
* hacks in behavior consistent with a normal SeekBar in a way that will not crash the app.
|
||||||
|
*
|
||||||
|
* SeekBar, like most android OS components, is a version-specific mess that requires constant
|
||||||
|
* hacks on older versions. Instead, we use the more "modern" slider component, but it is not
|
||||||
|
* designed for the job that Auxio's progress bar has. It does not gracefully degrade when
|
||||||
|
* positions don't make sense (which happens incredibly often), it just crashes the entire app,
|
||||||
|
* which is insane but also checks out for something more meant for configuration than seeking.
|
||||||
|
*
|
||||||
|
* Instead, we wrap it in a safe class that hopefully implements enough safety to not crash the
|
||||||
|
* app or result in blatantly janky behavior. Mostly.
|
||||||
|
*
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
|
class StyledSeekBar
|
||||||
|
@JvmOverloads
|
||||||
|
constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0,
|
||||||
|
) :
|
||||||
|
FrameLayout(context, attrs, defStyleAttr),
|
||||||
|
Slider.OnSliderTouchListener,
|
||||||
|
Slider.OnChangeListener {
|
||||||
|
private val binding = ViewSeekBarBinding.inflate(context.inflater, this, true)
|
||||||
|
|
||||||
|
init {
|
||||||
|
binding.seekBarSlider.addOnSliderTouchListener(this)
|
||||||
|
binding.seekBarSlider.addOnChangeListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
var callback: Callback? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current position, in seconds. This is the current value of the SeekBar and is indicated
|
||||||
|
* by the start TextView in the layout.
|
||||||
|
*/
|
||||||
|
var positionSecs: Long
|
||||||
|
get() = binding.seekBarSlider.value.toLong()
|
||||||
|
set(value) {
|
||||||
|
// Sanity check: Ensure that this value is within the duration and will not crash
|
||||||
|
// the app, and that the user is not currently seeking (which would cause the SeekBar
|
||||||
|
// to jump around).
|
||||||
|
if (value <= durationSecs && !isActivated) {
|
||||||
|
binding.seekBarSlider.value = value.toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current duration, in seconds. This is the end value of the SeekBar and is indicated
|
||||||
|
* by the end TextView in the layout.
|
||||||
|
*/
|
||||||
|
var durationSecs: Long
|
||||||
|
get() = binding.seekBarSlider.valueTo.toLong()
|
||||||
|
set(value) {
|
||||||
|
// Sanity check 1: If this is a value so low that it effectively rounds down to
|
||||||
|
// zero, use 1 instead and disable the SeekBar.
|
||||||
|
val to = max(value, 1)
|
||||||
|
isEnabled = value > 0
|
||||||
|
|
||||||
|
// Sanity check 2: If the current value exceeds the new duration value, clamp it
|
||||||
|
// down so that we don't crash and instead have an annoying visual flicker.
|
||||||
|
if (positionSecs > to) {
|
||||||
|
binding.seekBarSlider.value = to.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.seekBarSlider.valueTo = to.toFloat()
|
||||||
|
binding.seekBarDuration.textSafe = value.formatDuration(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartTrackingTouch(slider: Slider) {
|
||||||
|
// User has begun seeking, place the SeekBar into a "Suspended" mode in which no
|
||||||
|
// position updates are sent and is indicated by the position value turning accented.
|
||||||
|
isActivated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopTrackingTouch(slider: Slider) {
|
||||||
|
// End of seek event, send off new value to callback.
|
||||||
|
isActivated = false
|
||||||
|
callback?.seekTo(slider.value.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
||||||
|
binding.seekBarPosition.textSafe = value.toLong().formatDuration(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
/**
|
||||||
|
* Called when a seek event was completed and the new position must be seeked to by
|
||||||
|
* the app.
|
||||||
|
*/
|
||||||
|
fun seekTo(positionSecs: Long)
|
||||||
|
}
|
||||||
|
}
|
|
@ -77,46 +77,14 @@
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_artist"
|
app:layout_constraintTop_toBottomOf="@+id/playback_artist"
|
||||||
tools:text="Album Name" />
|
tools:text="Album Name" />
|
||||||
|
|
||||||
<com.google.android.material.slider.Slider
|
<org.oxycblt.auxio.playback.StyledSeekBar
|
||||||
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/spacing_medium"
|
|
||||||
android:valueFrom="0"
|
|
||||||
android:valueTo="1"
|
|
||||||
app:haloRadius="@dimen/slider_halo_radius"
|
|
||||||
app:labelBehavior="gone"
|
|
||||||
app:labelStyle="@style/TextAppearance.Auxio.BodySmall"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/playback_play_pause"
|
app:layout_constraintBottom_toTopOf="@+id/playback_play_pause"
|
||||||
app:layout_constraintEnd_toEndOf="@+id/playback_song_container"
|
app:layout_constraintEnd_toEndOf="@+id/playback_song_container"
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
app:thumbRadius="@dimen/slider_thumb_radius"
|
|
||||||
app:trackColorInactive="@color/sel_track" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/playback_position"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="@dimen/spacing_medium"
|
|
||||||
android:layout_marginTop="@dimen/spacing_small_inv"
|
|
||||||
android:textAppearance="@style/TextAppearance.Auxio.BodyMedium"
|
|
||||||
android:textColor="@color/sel_accented_secondary"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_seek_bar"
|
|
||||||
tools:text="11:38" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/playback_duration"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/spacing_small_inv"
|
|
||||||
android:layout_marginEnd="@dimen/spacing_medium"
|
|
||||||
android:textAppearance="@style/TextAppearance.Auxio.BodyMedium"
|
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_seek_bar"
|
|
||||||
tools:text="16:16" />
|
|
||||||
|
|
||||||
<org.oxycblt.auxio.ui.StyledImageButton
|
<org.oxycblt.auxio.ui.StyledImageButton
|
||||||
android:id="@+id/playback_repeat"
|
android:id="@+id/playback_repeat"
|
||||||
|
|
|
@ -72,49 +72,17 @@
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_artist"
|
app:layout_constraintTop_toBottomOf="@+id/playback_artist"
|
||||||
tools:text="Album Name" />
|
tools:text="Album Name" />
|
||||||
|
|
||||||
<com.google.android.material.slider.Slider
|
<org.oxycblt.auxio.playback.StyledSeekBar
|
||||||
android:id="@+id/playback_seek_bar"
|
android:id="@+id/playback_seek_bar"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/spacing_small"
|
android:layout_marginStart="@dimen/spacing_small"
|
||||||
android:layout_marginEnd="@dimen/spacing_small"
|
android:layout_marginEnd="@dimen/spacing_small"
|
||||||
android:layout_marginBottom="@dimen/spacing_medium"
|
|
||||||
android:valueFrom="0"
|
|
||||||
android:valueTo="1"
|
|
||||||
app:haloRadius="@dimen/slider_halo_radius"
|
|
||||||
app:labelBehavior="gone"
|
|
||||||
app:labelStyle="@style/TextAppearance.Auxio.BodySmall"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/playback_play_pause"
|
app:layout_constraintBottom_toTopOf="@+id/playback_play_pause"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.5"
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
app:layout_constraintStart_toEndOf="@+id/playback_cover"
|
app:layout_constraintStart_toEndOf="@+id/playback_cover"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_album"
|
app:layout_constraintTop_toBottomOf="@+id/playback_album" />
|
||||||
app:thumbRadius="@dimen/slider_thumb_radius"
|
|
||||||
app:trackColorInactive="@color/sel_track" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/playback_position"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="@dimen/spacing_medium"
|
|
||||||
android:layout_marginTop="@dimen/spacing_small_inv"
|
|
||||||
android:textAppearance="@style/TextAppearance.Auxio.BodyMedium"
|
|
||||||
android:textColor="@color/sel_accented_secondary"
|
|
||||||
app:layout_constraintStart_toStartOf="@+id/playback_seek_bar"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_seek_bar"
|
|
||||||
tools:text="11:38" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/playback_duration"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/spacing_small_inv"
|
|
||||||
android:layout_marginEnd="@dimen/spacing_medium"
|
|
||||||
android:textAppearance="@style/TextAppearance.Auxio.BodyMedium"
|
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
|
||||||
app:layout_constraintEnd_toEndOf="@+id/playback_seek_bar"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_seek_bar"
|
|
||||||
tools:text="16:16" />
|
|
||||||
|
|
||||||
<org.oxycblt.auxio.ui.StyledImageButton
|
<org.oxycblt.auxio.ui.StyledImageButton
|
||||||
android:id="@+id/playback_repeat"
|
android:id="@+id/playback_repeat"
|
||||||
|
|
|
@ -62,48 +62,16 @@
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
tools:text="Album Name" />
|
tools:text="Album Name" />
|
||||||
|
|
||||||
<com.google.android.material.slider.Slider
|
<org.oxycblt.auxio.playback.StyledSeekBar
|
||||||
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_marginStart="@dimen/spacing_medium"
|
android:layout_marginStart="@dimen/spacing_medium"
|
||||||
android:layout_marginEnd="@dimen/spacing_medium"
|
android:layout_marginEnd="@dimen/spacing_medium"
|
||||||
android:layout_marginBottom="@dimen/spacing_medium"
|
|
||||||
android:valueFrom="0"
|
|
||||||
android:valueTo="1"
|
|
||||||
app:haloRadius="@dimen/slider_halo_radius"
|
|
||||||
app:labelBehavior="gone"
|
|
||||||
app:labelStyle="@style/TextAppearance.Auxio.BodySmall"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/playback_play_pause"
|
app:layout_constraintBottom_toTopOf="@+id/playback_play_pause"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
app:thumbRadius="@dimen/slider_thumb_radius"
|
|
||||||
app:trackColorInactive="@color/sel_track" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/playback_position"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="@dimen/spacing_medium"
|
|
||||||
android:layout_marginTop="@dimen/spacing_small_inv"
|
|
||||||
android:textAppearance="@style/TextAppearance.Auxio.BodyMedium"
|
|
||||||
android:textColor="@color/sel_accented_secondary"
|
|
||||||
app:layout_constraintStart_toStartOf="@+id/playback_seek_bar"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_seek_bar"
|
|
||||||
tools:text="11:38" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/playback_duration"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/spacing_small_inv"
|
|
||||||
android:layout_marginEnd="@dimen/spacing_medium"
|
|
||||||
android:textAppearance="@style/TextAppearance.Auxio.BodyMedium"
|
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
|
||||||
app:layout_constraintEnd_toEndOf="@+id/playback_seek_bar"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_seek_bar"
|
|
||||||
tools:text="16:16" />
|
|
||||||
|
|
||||||
<org.oxycblt.auxio.ui.StyledImageButton
|
<org.oxycblt.auxio.ui.StyledImageButton
|
||||||
android:id="@+id/playback_repeat"
|
android:id="@+id/playback_repeat"
|
||||||
|
|
|
@ -77,46 +77,15 @@
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_artist"
|
app:layout_constraintTop_toBottomOf="@+id/playback_artist"
|
||||||
tools:text="Album Name" />
|
tools:text="Album Name" />
|
||||||
|
|
||||||
<com.google.android.material.slider.Slider
|
<org.oxycblt.auxio.playback.StyledSeekBar
|
||||||
android:id="@+id/playback_seek_bar"
|
android:id="@+id/playback_seek_bar"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="@dimen/spacing_medium"
|
|
||||||
android:valueFrom="0"
|
|
||||||
android:valueTo="1"
|
|
||||||
app:haloRadius="@dimen/slider_halo_radius"
|
|
||||||
app:labelBehavior="gone"
|
|
||||||
app:labelStyle="@style/TextAppearance.Auxio.BodySmall"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/playback_play_pause"
|
app:layout_constraintBottom_toTopOf="@+id/playback_play_pause"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.5"
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
app:layout_constraintStart_toEndOf="@+id/playback_cover"
|
app:layout_constraintStart_toEndOf="@+id/playback_cover"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_album"
|
app:layout_constraintTop_toBottomOf="@+id/playback_album" />
|
||||||
app:thumbRadius="@dimen/slider_thumb_radius" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/playback_position"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="@dimen/spacing_medium"
|
|
||||||
android:layout_marginTop="@dimen/spacing_small_inv"
|
|
||||||
android:textAppearance="@style/TextAppearance.Auxio.BodyMedium"
|
|
||||||
android:textColor="@color/sel_accented_secondary"
|
|
||||||
app:layout_constraintStart_toStartOf="@+id/playback_seek_bar"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_seek_bar"
|
|
||||||
tools:text="11:38" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/playback_duration"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/spacing_small_inv"
|
|
||||||
android:layout_marginEnd="@dimen/spacing_medium"
|
|
||||||
android:textAppearance="@style/TextAppearance.Auxio.BodyMedium"
|
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
|
||||||
app:layout_constraintEnd_toEndOf="@+id/playback_seek_bar"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_seek_bar"
|
|
||||||
tools:text="16:16" />
|
|
||||||
|
|
||||||
<org.oxycblt.auxio.ui.StyledImageButton
|
<org.oxycblt.auxio.ui.StyledImageButton
|
||||||
android:id="@+id/playback_repeat"
|
android:id="@+id/playback_repeat"
|
||||||
|
|
|
@ -61,46 +61,14 @@
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
tools:text="Album Name" />
|
tools:text="Album Name" />
|
||||||
|
|
||||||
<com.google.android.material.slider.Slider
|
<org.oxycblt.auxio.playback.StyledSeekBar
|
||||||
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/spacing_medium"
|
|
||||||
android:valueFrom="0"
|
|
||||||
android:valueTo="1"
|
|
||||||
app:haloRadius="@dimen/slider_halo_radius"
|
|
||||||
app:labelBehavior="gone"
|
|
||||||
app:labelStyle="@style/TextAppearance.Auxio.BodySmall"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/playback_play_pause"
|
app:layout_constraintBottom_toTopOf="@+id/playback_play_pause"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
app:thumbRadius="@dimen/slider_thumb_radius"
|
|
||||||
app:trackColorInactive="@color/sel_track" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/playback_position"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="@dimen/spacing_medium"
|
|
||||||
android:layout_marginTop="@dimen/spacing_small_inv"
|
|
||||||
android:textAppearance="@style/TextAppearance.Auxio.BodyMedium"
|
|
||||||
android:textColor="@color/sel_accented_secondary"
|
|
||||||
app:layout_constraintStart_toStartOf="@+id/playback_seek_bar"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_seek_bar"
|
|
||||||
tools:text="11:38" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/playback_duration"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/spacing_small_inv"
|
|
||||||
android:layout_marginEnd="@dimen/spacing_medium"
|
|
||||||
android:textAppearance="@style/TextAppearance.Auxio.BodyMedium"
|
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_seek_bar"
|
|
||||||
tools:text="16:16" />
|
|
||||||
|
|
||||||
<org.oxycblt.auxio.ui.StyledImageButton
|
<org.oxycblt.auxio.ui.StyledImageButton
|
||||||
android:id="@+id/playback_repeat"
|
android:id="@+id/playback_repeat"
|
||||||
|
|
49
app/src/main/res/layout/view_seek_bar.xml
Normal file
49
app/src/main/res/layout/view_seek_bar.xml
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<com.google.android.material.slider.Slider
|
||||||
|
android:id="@+id/seek_bar_slider"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="@dimen/spacing_medium"
|
||||||
|
android:valueFrom="0"
|
||||||
|
android:valueTo="1"
|
||||||
|
app:haloRadius="@dimen/slider_halo_radius"
|
||||||
|
app:labelBehavior="gone"
|
||||||
|
app:labelStyle="@style/TextAppearance.Auxio.BodySmall"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/playback_play_pause"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:thumbRadius="@dimen/slider_thumb_radius"
|
||||||
|
app:trackColorInactive="@color/sel_track" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/seek_bar_position"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/spacing_medium"
|
||||||
|
android:textAppearance="@style/TextAppearance.Auxio.BodyMedium"
|
||||||
|
android:textColor="@color/sel_accented_secondary"
|
||||||
|
android:layout_marginBottom="@dimen/spacing_tiny"
|
||||||
|
android:layout_gravity="bottom|start"
|
||||||
|
tools:text="11:38" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/seek_bar_duration"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="@dimen/spacing_medium"
|
||||||
|
android:layout_marginBottom="@dimen/spacing_tiny"
|
||||||
|
android:textAppearance="@style/TextAppearance.Auxio.BodyMedium"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/playback_seek_bar"
|
||||||
|
tools:text="16:16" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
|
@ -1,6 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<!-- Spacing Namespace | Dimens for padding/margin attributes -->
|
<!-- Spacing Namespace | Dimens for padding/margin attributes -->
|
||||||
|
<dimen name="spacing_tiny">4dp</dimen>
|
||||||
<dimen name="spacing_small">8dp</dimen>
|
<dimen name="spacing_small">8dp</dimen>
|
||||||
<dimen name="spacing_medium">16dp</dimen>
|
<dimen name="spacing_medium">16dp</dimen>
|
||||||
<dimen name="spacing_mid_large">24dp</dimen>
|
<dimen name="spacing_mid_large">24dp</dimen>
|
||||||
|
|
Loading…
Reference in a new issue