Skip to content

Commit

Permalink
home: fix more speed dial touch problems
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
OxygenCobalt committed Jan 3, 2024
1 parent 7537d13 commit 2b55caa
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 12 deletions.
26 changes: 26 additions & 0 deletions app/src/main/java/org/oxycblt/auxio/MainFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand All @@ -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))
Expand All @@ -197,6 +202,7 @@ class MainFragment :

override fun onDestroyBinding(binding: FragmentMainBinding) {
super.onDestroyBinding(binding)
speedDialBackCallback = null
sheetBackCallback = null
detailBackCallback = null
selectionBackCallback = null
Expand All @@ -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
Expand Down Expand Up @@ -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
}
}
}
62 changes: 54 additions & 8 deletions app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down Expand Up @@ -212,6 +218,7 @@ class HomeFragment :
binding.homeNewPlaylistFab.apply {
inflate(R.menu.new_playlist_actions)
setOnActionSelectedListener(this@HomeFragment)
setChangeListener(homeModel::setSpeedDialOpen)
}

hideAllFabs()
Expand All @@ -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)
Expand All @@ -246,6 +254,7 @@ class HomeFragment :
storagePermissionLauncher = null
binding.homeAppbar.removeOnOffsetChangedListener(this)
binding.homeNormalToolbar.setOnMenuItemClickListener(null)
binding.homeNewPlaylistFab.setChangeListener(null)
binding.homeNewPlaylistFab.setOnActionSelectedListener(null)
}

Expand Down Expand Up @@ -577,8 +586,6 @@ class HomeFragment :
return
}

logD(binding.homeShuffleFab.isOrWillBeShown)

if (binding.homeShuffleFab.isOrWillBeShown) {
logD("Animating transition")
binding.homeShuffleFab.hide(
Expand Down Expand Up @@ -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?) {
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -126,6 +127,7 @@ class ThemedSpeedDialView : SpeedDialView {
})
start()
}
innerChangeListener?.invoke(isOpen)
}
})
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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") {
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 2b55caa

Please sign in to comment.