home: fix more speed dial touch problems

Handle back presses gracefully without finicky behavior when doing back
gestures.

I've spent far too much time trying to make this sensible. I'm going to
take a break.
This commit is contained in:
Alexander Capehart 2024-01-02 17:16:13 -07:00
parent 7537d135f2
commit 2b55caadd1
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
5 changed files with 102 additions and 12 deletions

View file

@ -76,6 +76,7 @@ class MainFragment :
private var sheetBackCallback: SheetBackPressedCallback? = null
private var detailBackCallback: DetailBackPressedCallback? = null
private var selectionBackCallback: SelectionBackPressedCallback? = null
private var speedDialBackCallback: SpeedDialBackPressedCallback? = null
private var selectionNavigationListener: DialogAwareNavigationListener? = null
private var lastInsets: WindowInsets? = null
private var elevationNormal = 0f
@ -109,6 +110,8 @@ class MainFragment :
DetailBackPressedCallback(detailModel).also { detailBackCallback = it }
val selectionBackCallback =
SelectionBackPressedCallback(listModel).also { selectionBackCallback = it }
val speedDialBackCallback =
SpeedDialBackPressedCallback(homeModel).also { speedDialBackCallback = it }
selectionNavigationListener = DialogAwareNavigationListener(listModel::dropSelection)
@ -158,6 +161,7 @@ class MainFragment :
collect(detailModel.toShow.flow, ::handleShow)
collectImmediately(detailModel.editedPlaylist, detailBackCallback::invalidateEnabled)
collectImmediately(homeModel.showOuter.flow, ::handleShowOuter)
collectImmediately(homeModel.speedDialOpen, speedDialBackCallback::invalidateEnabled)
collectImmediately(listModel.selected, selectionBackCallback::invalidateEnabled)
collectImmediately(playbackModel.song, ::updateSong)
collectImmediately(playbackModel.openPanel.flow, ::handlePanel)
@ -181,6 +185,7 @@ class MainFragment :
// navigation, navigation out of detail views, etc. We have to do this here in
// onResume or otherwise the FragmentManager will have precedence.
requireActivity().onBackPressedDispatcher.apply {
addCallback(viewLifecycleOwner, requireNotNull(speedDialBackCallback))
addCallback(viewLifecycleOwner, requireNotNull(selectionBackCallback))
addCallback(viewLifecycleOwner, requireNotNull(detailBackCallback))
addCallback(viewLifecycleOwner, requireNotNull(sheetBackCallback))
@ -197,6 +202,7 @@ class MainFragment :
override fun onDestroyBinding(binding: FragmentMainBinding) {
super.onDestroyBinding(binding)
speedDialBackCallback = null
sheetBackCallback = null
detailBackCallback = null
selectionBackCallback = null
@ -218,6 +224,13 @@ class MainFragment :
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
val playbackRatio = max(playbackSheetBehavior.calculateSlideOffset(), 0f)
if (playbackRatio > 0f && homeModel.speedDialOpen.value) {
// Stupid hack to prevent you from sliding the sheet up without closing the speed
// dial. Filtering out ACTION_MOVE events will cause back gestures to close the speed
// dial, which is super finicky behavior.
homeModel.setSpeedDialOpen(false)
}
val outPlaybackRatio = 1 - playbackRatio
val halfOutRatio = min(playbackRatio * 2, 1f)
val halfInPlaybackRatio = max(playbackRatio - 0.5f, 0f) * 2
@ -493,4 +506,17 @@ class MainFragment :
isEnabled = selection.isNotEmpty()
}
}
private inner class SpeedDialBackPressedCallback(private val homeModel: HomeViewModel) :
OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
if (homeModel.speedDialOpen.value) {
homeModel.setSpeedDialOpen(false)
}
}
fun invalidateEnabled(open: Boolean) {
isEnabled = open
}
}
}

View file

@ -149,14 +149,20 @@ class HomeFragment :
binding.root.rootView.apply {
// Stock bottom sheet overlay won't work with our nested UI setup, have to replicate
// it ourselves.
findViewById<View>(R.id.main_scrim).setOnClickListener {
homeModel.setSpeedDialOpen(false)
}
findViewById<View>(R.id.main_scrim).setOnTouchListener { _, event ->
handleSpeedDialBoundaryTouch(event)
false
}
findViewById<View>(R.id.sheet_scrim).setOnClickListener {
homeModel.setSpeedDialOpen(false)
}
findViewById<View>(R.id.sheet_scrim).setOnTouchListener { _, event ->
handleSpeedDialBoundaryTouch(event)
false
}
}
@ -212,6 +218,7 @@ class HomeFragment :
binding.homeNewPlaylistFab.apply {
inflate(R.menu.new_playlist_actions)
setOnActionSelectedListener(this@HomeFragment)
setChangeListener(homeModel::setSpeedDialOpen)
}
hideAllFabs()
@ -224,6 +231,7 @@ class HomeFragment :
collect(homeModel.recreateTabs.flow, ::handleRecreate)
collectImmediately(homeModel.currentTabType, ::updateCurrentTab)
collectImmediately(homeModel.songList, homeModel.isFastScrolling, ::updateFab)
collect(homeModel.speedDialOpen, ::updateSpeedDial)
collect(listModel.menu.flow, ::handleMenu)
collectImmediately(listModel.selected, ::updateSelection)
collectImmediately(musicModel.indexingState, ::updateIndexerState)
@ -246,6 +254,7 @@ class HomeFragment :
storagePermissionLauncher = null
binding.homeAppbar.removeOnOffsetChangedListener(this)
binding.homeNormalToolbar.setOnMenuItemClickListener(null)
binding.homeNewPlaylistFab.setChangeListener(null)
binding.homeNewPlaylistFab.setOnActionSelectedListener(null)
}
@ -577,8 +586,6 @@ class HomeFragment :
return
}
logD(binding.homeShuffleFab.isOrWillBeShown)
if (binding.homeShuffleFab.isOrWillBeShown) {
logD("Animating transition")
binding.homeShuffleFab.hide(
@ -606,12 +613,51 @@ class HomeFragment :
}
}
private fun handleSpeedDialBoundaryTouch(event: MotionEvent) {
private fun updateSpeedDial(open: Boolean) {
val binding = requireBinding()
if (binding.homeNewPlaylistFab.isOpen &&
!binding.homeNewPlaylistFab.isUnder(event.x, event.y)) {
binding.homeNewPlaylistFab.close()
binding.root.rootView.apply {
// Stock bottom sheet overlay won't work with our nested UI setup, have to replicate
// it ourselves.
findViewById<View>(R.id.main_scrim).isClickable = open
findViewById<View>(R.id.sheet_scrim).isClickable = open
}
if (open) {
binding.homeNewPlaylistFab.open(true)
} else {
binding.homeNewPlaylistFab.close(true)
}
}
private fun handleSpeedDialBoundaryTouch(event: MotionEvent): Boolean {
val binding = binding ?: return false
if (binding.homeNewPlaylistFab.isUnder(event.x, event.y)) {
// Convert absolute coordinates to relative coordinates
val offsetX = event.x - binding.homeNewPlaylistFab.x
val offsetY = event.y - binding.homeNewPlaylistFab.y
// Create a new MotionEvent with relative coordinates
val relativeEvent =
MotionEvent.obtain(
event.downTime,
event.eventTime,
event.action,
offsetX,
offsetY,
event.metaState)
// Dispatch the relative MotionEvent to the target child view
val handled = binding.homeNewPlaylistFab.dispatchTouchEvent(relativeEvent)
// Recycle the relative MotionEvent
relativeEvent.recycle()
return handled
}
return false
}
private fun handleShow(show: Show?) {

View file

@ -156,6 +156,10 @@ constructor(
/** A marker for whether the user is fast-scrolling in the home view or not. */
val isFastScrolling: StateFlow<Boolean> = _isFastScrolling
private val _speedDialOpen = MutableStateFlow(false)
/** A marker for whether the speed dial is open or not. */
val speedDialOpen: StateFlow<Boolean> = _speedDialOpen
private val _showOuter = MutableEvent<Outer>()
val showOuter: Event<Outer>
get() = _showOuter
@ -293,6 +297,16 @@ constructor(
_isFastScrolling.value = isFastScrolling
}
/**
* Update whether the speed dial is open or not.
*
* @param speedDialOpen true if the speed dial is open, false otherwise.
*/
fun setSpeedDialOpen(speedDialOpen: Boolean) {
logD("Updating speed dial state: $speedDialOpen")
_speedDialOpen.value = speedDialOpen
}
fun showSettings() {
_showOuter.put(Outer.Settings)
}

View file

@ -66,6 +66,7 @@ import org.oxycblt.auxio.util.getDimenPixels
class ThemedSpeedDialView : SpeedDialView {
private var mainFabAnimator: Animator? = null
private val spacingSmall = context.getDimenPixels(R.dimen.spacing_small)
private var innerChangeListener: ((Boolean) -> Unit)? = null
constructor(context: Context) : super(context)
@ -126,6 +127,7 @@ class ThemedSpeedDialView : SpeedDialView {
})
start()
}
innerChangeListener?.invoke(isOpen)
}
})
}
@ -178,8 +180,6 @@ class ThemedSpeedDialView : SpeedDialView {
val labelColor = context.getAttrColorCompat(android.R.attr.textColorSecondary)
val labelBackgroundColor =
context.getAttrColorCompat(com.google.android.material.R.attr.colorSurface)
val labelStroke =
context.getAttrColorCompat(com.google.android.material.R.attr.colorOutline)
val labelElevation =
context.getDimen(com.google.android.material.R.dimen.m3_card_elevated_elevation)
val cornerRadius = context.getDimenPixels(R.dimen.spacing_medium)
@ -237,6 +237,10 @@ class ThemedSpeedDialView : SpeedDialView {
}
}
fun setChangeListener(listener: ((Boolean) -> Unit)?) {
innerChangeListener = listener
}
companion object {
private val VIEW_PROPERTY_BACKGROUND_TINT =
object : Property<View, Int>(Int::class.java, "backgroundTint") {

View file

@ -224,9 +224,9 @@ constructor(
* @param playlist The [Playlist] to rename,
* @param name The new name of the [Playlist]. If null, the user will be prompted for a name.
* @param applySongs The songs to apply to the playlist after renaming. If empty, no songs will
* be applied. This argument is internal and does not need to be specified in normal use.
* be applied. This argument is internal and does not need to be specified in normal use.
* @param reason The reason why the playlist is being renamed. This argument is internal and
* does not need to be specified in normal use.
* does not need to be specified in normal use.
*/
fun renamePlaylist(
playlist: Playlist,