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:
parent
7537d135f2
commit
2b55caadd1
5 changed files with 102 additions and 12 deletions
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue