ui: rework bottom sheet state management
Try to make the bottom sheet states more coherent, especially regarding when playback ends.
This commit is contained in:
parent
35cfea78df
commit
de3cc7958f
12 changed files with 92 additions and 67 deletions
|
@ -52,6 +52,7 @@ class MainFragment :
|
||||||
private val navModel: NavigationViewModel by activityViewModels()
|
private val navModel: NavigationViewModel by activityViewModels()
|
||||||
private var callback: DynamicBackPressedCallback? = null
|
private var callback: DynamicBackPressedCallback? = null
|
||||||
private var lastInsets: WindowInsets? = null
|
private var lastInsets: WindowInsets? = null
|
||||||
|
private var keepPlaybackSheetHidden = false
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -117,11 +118,6 @@ class MainFragment :
|
||||||
override fun onPreDraw(): Boolean {
|
override fun onPreDraw(): Boolean {
|
||||||
// CoordinatorLayout is insane and thus makes bottom sheet callbacks insane. Do our
|
// CoordinatorLayout is insane and thus makes bottom sheet callbacks insane. Do our
|
||||||
// checks before every draw.
|
// checks before every draw.
|
||||||
handleSheetTransitions()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleSheetTransitions() {
|
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
val playbackSheetBehavior =
|
val playbackSheetBehavior =
|
||||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
|
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
|
||||||
|
@ -162,9 +158,25 @@ class MainFragment :
|
||||||
isInvisible = alpha == 0f
|
isInvisible = alpha == 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackSheetBehavior.isDraggable =
|
if (playbackModel.song.value != null) {
|
||||||
playbackSheetBehavior.state != BottomSheetBehavior.STATE_HIDDEN &&
|
// Hack around the playback sheet intercepting swipe events on the queue bar
|
||||||
|
playbackSheetBehavior.isDraggable =
|
||||||
queueSheetBehavior.state == BottomSheetBehavior.STATE_COLLAPSED
|
queueSheetBehavior.state == BottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
} else {
|
||||||
|
// Sometimes lingering drags can un-hide the playback sheet even when we intend to
|
||||||
|
// hide it, make sure we keep it hidden.
|
||||||
|
tryHideAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSong(song: Song?) {
|
||||||
|
if (song != null) {
|
||||||
|
tryUnhideAll()
|
||||||
|
} else {
|
||||||
|
tryHideAll()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleMainNavigation(action: MainNavigationAction?) {
|
private fun handleMainNavigation(action: MainNavigationAction?) {
|
||||||
|
@ -212,11 +224,10 @@ class MainFragment :
|
||||||
|
|
||||||
if (playbackSheetBehavior.state != BottomSheetBehavior.STATE_HIDDEN &&
|
if (playbackSheetBehavior.state != BottomSheetBehavior.STATE_HIDDEN &&
|
||||||
playbackSheetBehavior.state != BottomSheetBehavior.STATE_COLLAPSED) {
|
playbackSheetBehavior.state != BottomSheetBehavior.STATE_COLLAPSED) {
|
||||||
playbackSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
|
||||||
|
|
||||||
val queueSheetBehavior =
|
val queueSheetBehavior =
|
||||||
binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior
|
binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior
|
||||||
|
|
||||||
|
playbackSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
queueSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
queueSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
@ -225,15 +236,45 @@ class MainFragment :
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSong(song: Song?) {
|
private fun tryUnhideAll(): Boolean {
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
val playbackSheetBehavior =
|
val playbackSheetBehavior =
|
||||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
|
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
|
||||||
if (song != null) {
|
|
||||||
playbackSheetBehavior.unhideSafe()
|
if (playbackSheetBehavior.state == BottomSheetBehavior.STATE_HIDDEN) {
|
||||||
} else {
|
val queueSheetBehavior =
|
||||||
playbackSheetBehavior.hideSafe()
|
binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior
|
||||||
|
|
||||||
|
playbackSheetBehavior.isDraggable = true
|
||||||
|
playbackSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
|
||||||
|
queueSheetBehavior.isDraggable = true
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryHideAll(): Boolean {
|
||||||
|
val binding = requireBinding()
|
||||||
|
val playbackSheetBehavior =
|
||||||
|
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
|
||||||
|
|
||||||
|
if (playbackSheetBehavior.state != BottomSheetBehavior.STATE_HIDDEN) {
|
||||||
|
val queueSheetBehavior =
|
||||||
|
binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior
|
||||||
|
|
||||||
|
playbackSheetBehavior.isDraggable = false
|
||||||
|
queueSheetBehavior.isDraggable = false
|
||||||
|
|
||||||
|
playbackSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
queueSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -130,7 +130,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
|
||||||
|
|
||||||
val songCount = context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size)
|
val songCount = context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size)
|
||||||
|
|
||||||
val duration = "<duration>"
|
val duration = item.durationSecs.formatDuration(true)
|
||||||
|
|
||||||
text =
|
text =
|
||||||
if (item.releaseType != null) {
|
if (item.releaseType != null) {
|
||||||
|
|
|
@ -37,28 +37,9 @@ class PlaybackSheetBehavior<V : View>(context: Context, attributeSet: AttributeS
|
||||||
|
|
||||||
// Hack around issue where the playback sheet will try to intercept nested scrolling events
|
// Hack around issue where the playback sheet will try to intercept nested scrolling events
|
||||||
// before the queue sheet.
|
// before the queue sheet.
|
||||||
override fun onInterceptTouchEvent(
|
override fun onInterceptTouchEvent(parent: CoordinatorLayout, child: V, event: MotionEvent) =
|
||||||
parent: CoordinatorLayout,
|
super.onInterceptTouchEvent(parent, child, event) && state != STATE_EXPANDED
|
||||||
child: V,
|
|
||||||
event: MotionEvent
|
|
||||||
): Boolean = super.onInterceptTouchEvent(parent, child, event) && state != STATE_EXPANDED
|
|
||||||
|
|
||||||
// Note: This is an extension to Auxio's vendored BottomSheetBehavior
|
// Note: This is an extension to Auxio's vendored BottomSheetBehavior
|
||||||
override fun enableHidingGestures() = false
|
override fun enableHidingGestures() = false
|
||||||
|
|
||||||
/** Hide this sheet in a safe manner. */
|
|
||||||
fun hideSafe() {
|
|
||||||
if (state != STATE_HIDDEN) {
|
|
||||||
isDraggable = false
|
|
||||||
state = STATE_HIDDEN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Unhide this sheet in a safe manner. */
|
|
||||||
fun unhideSafe() {
|
|
||||||
if (state == STATE_HIDDEN) {
|
|
||||||
state = STATE_COLLAPSED
|
|
||||||
isDraggable = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ package org.oxycblt.auxio.playback.state
|
||||||
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
|
@ -400,14 +399,7 @@ class PlaybackStateManager private constructor() {
|
||||||
|
|
||||||
suspend fun wipeState(database: PlaybackStateDatabase) {
|
suspend fun wipeState(database: PlaybackStateDatabase) {
|
||||||
logD("Wiping state")
|
logD("Wiping state")
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) { database.write(null) }
|
||||||
delay(5000)
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
index = -1
|
|
||||||
notifyNewPlayback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sanitize the state with [newLibrary]. */
|
/** Sanitize the state with [newLibrary]. */
|
||||||
|
|
|
@ -42,8 +42,13 @@ import org.oxycblt.auxio.util.logD
|
||||||
/**
|
/**
|
||||||
* The component managing the [MediaSessionCompat] instance.
|
* The component managing the [MediaSessionCompat] instance.
|
||||||
*
|
*
|
||||||
* I really don't like how I have to do this, but until I can work with the ExoPlayer queue system
|
* Media3 is a joke. It tries so hard to be "hElpfUl" and implement so many fundamental behaviors
|
||||||
* using something like MediaSessionConnector is more or less impossible.
|
* into a one-size-fits-all package that it only ends up causing unending bugs and frustration. The
|
||||||
|
* queue system is horribly designed, the notification code is outdated, and the overstretched
|
||||||
|
* abstractions result in terrible performance bottlenecks and insane state bugs..
|
||||||
|
*
|
||||||
|
* Show me a way to adapt my internal queue into the new system and I will change my mind, but
|
||||||
|
* otherwise, I will stick with my normal system that works correctly.
|
||||||
*
|
*
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*
|
*
|
||||||
|
@ -265,12 +270,12 @@ class MediaSessionComponent(
|
||||||
|
|
||||||
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
||||||
super.onPlayFromMediaId(mediaId, extras)
|
super.onPlayFromMediaId(mediaId, extras)
|
||||||
// STUB: Unimplemented
|
// STUB: Unimplemented, no media browser
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayFromUri(uri: Uri?, extras: Bundle?) {
|
override fun onPlayFromUri(uri: Uri?, extras: Bundle?) {
|
||||||
super.onPlayFromUri(uri, extras)
|
super.onPlayFromUri(uri, extras)
|
||||||
// STUB: Unimplemented
|
// STUB: Unimplemented, no media browser
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
||||||
|
|
|
@ -185,14 +185,14 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
val pauseOnRepeat: Boolean
|
val pauseOnRepeat: Boolean
|
||||||
get() = inner.getBoolean(context.getString(R.string.set_key_repeat_pause), false)
|
get() = inner.getBoolean(context.getString(R.string.set_key_repeat_pause), false)
|
||||||
|
|
||||||
/** Whether to be actively watching for changes in the music library. */
|
|
||||||
val shouldBeObserving: Boolean
|
|
||||||
get() = inner.getBoolean(context.getString(R.string.set_key_observing), false)
|
|
||||||
|
|
||||||
/** Whether to parse metadata directly with ExoPlayer. */
|
/** Whether to parse metadata directly with ExoPlayer. */
|
||||||
val useQualityTags: Boolean
|
val useQualityTags: Boolean
|
||||||
get() = inner.getBoolean(context.getString(R.string.set_key_quality_tags), false)
|
get() = inner.getBoolean(context.getString(R.string.set_key_quality_tags), false)
|
||||||
|
|
||||||
|
/** Whether to be actively watching for changes in the music library. */
|
||||||
|
val shouldBeObserving: Boolean
|
||||||
|
get() = inner.getBoolean(context.getString(R.string.set_key_observing), false)
|
||||||
|
|
||||||
/** Get the list of directories that music should be hidden/loaded from. */
|
/** Get the list of directories that music should be hidden/loaded from. */
|
||||||
fun getMusicDirs(storageManager: StorageManager): MusicDirs {
|
fun getMusicDirs(storageManager: StorageManager): MusicDirs {
|
||||||
val dirs =
|
val dirs =
|
||||||
|
|
|
@ -80,7 +80,21 @@ private val Any.autoTag: String
|
||||||
* taking work others did and making it objectively worse so you could arbitrage a fraction of a
|
* taking work others did and making it objectively worse so you could arbitrage a fraction of a
|
||||||
* penny on every AdMob impression you get? You could do so many great things if you simply had the
|
* penny on every AdMob impression you get? You could do so many great things if you simply had the
|
||||||
* courage to come up with an idea of your own. If you still want to go on, I guess the only thing I
|
* courage to come up with an idea of your own. If you still want to go on, I guess the only thing I
|
||||||
* can say is this: JUNE 1989 TIANAMEN SQUARE PROTESTS AND MASSACRE 六四事件
|
* can say is this:
|
||||||
|
*
|
||||||
|
* JUNE 1989 TIANAMEN SQUARE PROTESTS AND MASSACRE 六四事件
|
||||||
|
*
|
||||||
|
* UYGHUR GENOCIDE 新疆种族灭绝指控
|
||||||
|
*
|
||||||
|
* XINJIANG INTERMENT CAMPS 新疆再教育營
|
||||||
|
*
|
||||||
|
* KASHMIR INDEPENDENCE MOVEMENT
|
||||||
|
*
|
||||||
|
* WOMEN'S RIGHTS IN THE ISLAMIC REPUBLIC OF IRAN حقوق زنان در ایران
|
||||||
|
*
|
||||||
|
* 2022 RUSSIAN INVASION OF UKRAINE Вторжение России на Украину
|
||||||
|
*
|
||||||
|
* KURDISTAN WORKERS PARTY KÜRDISTAN İŞÇI PARTISI (PKK)
|
||||||
*/
|
*/
|
||||||
private fun basedCopyleftNotice() {
|
private fun basedCopyleftNotice() {
|
||||||
if (BuildConfig.APPLICATION_ID != "org.oxycblt.auxio" &&
|
if (BuildConfig.APPLICATION_ID != "org.oxycblt.auxio" &&
|
||||||
|
@ -88,6 +102,6 @@ private fun basedCopyleftNotice() {
|
||||||
Log.d(
|
Log.d(
|
||||||
"Auxio Project",
|
"Auxio Project",
|
||||||
"Friendly reminder: Auxio is licensed under the " +
|
"Friendly reminder: Auxio is licensed under the " +
|
||||||
"GPLv3 and all modifications must be made open source!")
|
"GPLv3 and all derivative apps must be made open source!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:tint="?attr/colorControlNormal"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M16,20Q14.75,20 13.875,19.125Q13,18.25 13,17Q13,15.75 13.875,14.875Q14.75,14 16,14Q16.275,14 16.525,14.037Q16.775,14.075 17,14.2V6H22V8H19V17Q19,18.25 18.125,19.125Q17.25,20 16,20ZM3,16V14H11V16ZM3,12V10H15V12ZM3,8V6H15V8Z" />
|
|
||||||
</vector>
|
|
|
@ -53,6 +53,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
android:layout_margin="@dimen/spacing_medium"
|
android:layout_margin="@dimen/spacing_medium"
|
||||||
android:visibility="invisible">
|
android:visibility="invisible">
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
android:scaleType="center"
|
android:scaleType="center"
|
||||||
android:paddingBottom="@dimen/spacing_small"
|
android:paddingBottom="@dimen/spacing_small"
|
||||||
android:src="@drawable/ic_down_24"
|
android:src="@drawable/ic_down_24"
|
||||||
|
android:contentDescription="@string/desc_queue_bar"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -62,6 +63,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/lbl_queue"
|
android:text="@string/lbl_queue"
|
||||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/handle"
|
app:layout_constraintBottom_toBottomOf="@+id/handle"
|
||||||
app:layout_constraintEnd_toEndOf="@+id/handle"
|
app:layout_constraintEnd_toEndOf="@+id/handle"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
|
@ -11,7 +11,6 @@
|
||||||
android:id="@+id/queue_recycler"
|
android:id="@+id/queue_recycler"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:overScrollMode="ifContentScrolls"
|
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||||
tools:listitem="@layout/item_queue_song" />
|
tools:listitem="@layout/item_queue_song" />
|
||||||
|
|
|
@ -236,6 +236,7 @@
|
||||||
|
|
||||||
<string name="desc_clear_queue_item">Remove this queue song</string>
|
<string name="desc_clear_queue_item">Remove this queue song</string>
|
||||||
<string name="desc_queue_handle">Move this queue song</string>
|
<string name="desc_queue_handle">Move this queue song</string>
|
||||||
|
<string name="desc_queue_bar">Open the queue</string>
|
||||||
<string name="desc_tab_handle">Move this tab</string>
|
<string name="desc_tab_handle">Move this tab</string>
|
||||||
<string name="desc_clear_search">Clear search query</string>
|
<string name="desc_clear_search">Clear search query</string>
|
||||||
<string name="desc_music_dir_delete">Remove folder</string>
|
<string name="desc_music_dir_delete">Remove folder</string>
|
||||||
|
|
Loading…
Reference in a new issue