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 sheetBackCallback: SheetBackPressedCallback? = null
|
||||||
private var detailBackCallback: DetailBackPressedCallback? = null
|
private var detailBackCallback: DetailBackPressedCallback? = null
|
||||||
private var selectionBackCallback: SelectionBackPressedCallback? = null
|
private var selectionBackCallback: SelectionBackPressedCallback? = null
|
||||||
|
private var speedDialBackCallback: SpeedDialBackPressedCallback? = null
|
||||||
private var selectionNavigationListener: DialogAwareNavigationListener? = null
|
private var selectionNavigationListener: DialogAwareNavigationListener? = null
|
||||||
private var lastInsets: WindowInsets? = null
|
private var lastInsets: WindowInsets? = null
|
||||||
private var elevationNormal = 0f
|
private var elevationNormal = 0f
|
||||||
|
@ -109,6 +110,8 @@ class MainFragment :
|
||||||
DetailBackPressedCallback(detailModel).also { detailBackCallback = it }
|
DetailBackPressedCallback(detailModel).also { detailBackCallback = it }
|
||||||
val selectionBackCallback =
|
val selectionBackCallback =
|
||||||
SelectionBackPressedCallback(listModel).also { selectionBackCallback = it }
|
SelectionBackPressedCallback(listModel).also { selectionBackCallback = it }
|
||||||
|
val speedDialBackCallback =
|
||||||
|
SpeedDialBackPressedCallback(homeModel).also { speedDialBackCallback = it }
|
||||||
|
|
||||||
selectionNavigationListener = DialogAwareNavigationListener(listModel::dropSelection)
|
selectionNavigationListener = DialogAwareNavigationListener(listModel::dropSelection)
|
||||||
|
|
||||||
|
@ -158,6 +161,7 @@ class MainFragment :
|
||||||
collect(detailModel.toShow.flow, ::handleShow)
|
collect(detailModel.toShow.flow, ::handleShow)
|
||||||
collectImmediately(detailModel.editedPlaylist, detailBackCallback::invalidateEnabled)
|
collectImmediately(detailModel.editedPlaylist, detailBackCallback::invalidateEnabled)
|
||||||
collectImmediately(homeModel.showOuter.flow, ::handleShowOuter)
|
collectImmediately(homeModel.showOuter.flow, ::handleShowOuter)
|
||||||
|
collectImmediately(homeModel.speedDialOpen, speedDialBackCallback::invalidateEnabled)
|
||||||
collectImmediately(listModel.selected, selectionBackCallback::invalidateEnabled)
|
collectImmediately(listModel.selected, selectionBackCallback::invalidateEnabled)
|
||||||
collectImmediately(playbackModel.song, ::updateSong)
|
collectImmediately(playbackModel.song, ::updateSong)
|
||||||
collectImmediately(playbackModel.openPanel.flow, ::handlePanel)
|
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
|
// navigation, navigation out of detail views, etc. We have to do this here in
|
||||||
// onResume or otherwise the FragmentManager will have precedence.
|
// onResume or otherwise the FragmentManager will have precedence.
|
||||||
requireActivity().onBackPressedDispatcher.apply {
|
requireActivity().onBackPressedDispatcher.apply {
|
||||||
|
addCallback(viewLifecycleOwner, requireNotNull(speedDialBackCallback))
|
||||||
addCallback(viewLifecycleOwner, requireNotNull(selectionBackCallback))
|
addCallback(viewLifecycleOwner, requireNotNull(selectionBackCallback))
|
||||||
addCallback(viewLifecycleOwner, requireNotNull(detailBackCallback))
|
addCallback(viewLifecycleOwner, requireNotNull(detailBackCallback))
|
||||||
addCallback(viewLifecycleOwner, requireNotNull(sheetBackCallback))
|
addCallback(viewLifecycleOwner, requireNotNull(sheetBackCallback))
|
||||||
|
@ -197,6 +202,7 @@ class MainFragment :
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: FragmentMainBinding) {
|
override fun onDestroyBinding(binding: FragmentMainBinding) {
|
||||||
super.onDestroyBinding(binding)
|
super.onDestroyBinding(binding)
|
||||||
|
speedDialBackCallback = null
|
||||||
sheetBackCallback = null
|
sheetBackCallback = null
|
||||||
detailBackCallback = null
|
detailBackCallback = null
|
||||||
selectionBackCallback = null
|
selectionBackCallback = null
|
||||||
|
@ -218,6 +224,13 @@ class MainFragment :
|
||||||
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
||||||
|
|
||||||
val playbackRatio = max(playbackSheetBehavior.calculateSlideOffset(), 0f)
|
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 outPlaybackRatio = 1 - playbackRatio
|
||||||
val halfOutRatio = min(playbackRatio * 2, 1f)
|
val halfOutRatio = min(playbackRatio * 2, 1f)
|
||||||
val halfInPlaybackRatio = max(playbackRatio - 0.5f, 0f) * 2
|
val halfInPlaybackRatio = max(playbackRatio - 0.5f, 0f) * 2
|
||||||
|
@ -493,4 +506,17 @@ class MainFragment :
|
||||||
isEnabled = selection.isNotEmpty()
|
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 {
|
binding.root.rootView.apply {
|
||||||
// Stock bottom sheet overlay won't work with our nested UI setup, have to replicate
|
// Stock bottom sheet overlay won't work with our nested UI setup, have to replicate
|
||||||
// it ourselves.
|
// it ourselves.
|
||||||
|
findViewById<View>(R.id.main_scrim).setOnClickListener {
|
||||||
|
homeModel.setSpeedDialOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
findViewById<View>(R.id.main_scrim).setOnTouchListener { _, event ->
|
findViewById<View>(R.id.main_scrim).setOnTouchListener { _, event ->
|
||||||
handleSpeedDialBoundaryTouch(event)
|
handleSpeedDialBoundaryTouch(event)
|
||||||
false
|
}
|
||||||
|
|
||||||
|
findViewById<View>(R.id.sheet_scrim).setOnClickListener {
|
||||||
|
homeModel.setSpeedDialOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
findViewById<View>(R.id.sheet_scrim).setOnTouchListener { _, event ->
|
findViewById<View>(R.id.sheet_scrim).setOnTouchListener { _, event ->
|
||||||
handleSpeedDialBoundaryTouch(event)
|
handleSpeedDialBoundaryTouch(event)
|
||||||
false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,6 +218,7 @@ class HomeFragment :
|
||||||
binding.homeNewPlaylistFab.apply {
|
binding.homeNewPlaylistFab.apply {
|
||||||
inflate(R.menu.new_playlist_actions)
|
inflate(R.menu.new_playlist_actions)
|
||||||
setOnActionSelectedListener(this@HomeFragment)
|
setOnActionSelectedListener(this@HomeFragment)
|
||||||
|
setChangeListener(homeModel::setSpeedDialOpen)
|
||||||
}
|
}
|
||||||
|
|
||||||
hideAllFabs()
|
hideAllFabs()
|
||||||
|
@ -224,6 +231,7 @@ class HomeFragment :
|
||||||
collect(homeModel.recreateTabs.flow, ::handleRecreate)
|
collect(homeModel.recreateTabs.flow, ::handleRecreate)
|
||||||
collectImmediately(homeModel.currentTabType, ::updateCurrentTab)
|
collectImmediately(homeModel.currentTabType, ::updateCurrentTab)
|
||||||
collectImmediately(homeModel.songList, homeModel.isFastScrolling, ::updateFab)
|
collectImmediately(homeModel.songList, homeModel.isFastScrolling, ::updateFab)
|
||||||
|
collect(homeModel.speedDialOpen, ::updateSpeedDial)
|
||||||
collect(listModel.menu.flow, ::handleMenu)
|
collect(listModel.menu.flow, ::handleMenu)
|
||||||
collectImmediately(listModel.selected, ::updateSelection)
|
collectImmediately(listModel.selected, ::updateSelection)
|
||||||
collectImmediately(musicModel.indexingState, ::updateIndexerState)
|
collectImmediately(musicModel.indexingState, ::updateIndexerState)
|
||||||
|
@ -246,6 +254,7 @@ class HomeFragment :
|
||||||
storagePermissionLauncher = null
|
storagePermissionLauncher = null
|
||||||
binding.homeAppbar.removeOnOffsetChangedListener(this)
|
binding.homeAppbar.removeOnOffsetChangedListener(this)
|
||||||
binding.homeNormalToolbar.setOnMenuItemClickListener(null)
|
binding.homeNormalToolbar.setOnMenuItemClickListener(null)
|
||||||
|
binding.homeNewPlaylistFab.setChangeListener(null)
|
||||||
binding.homeNewPlaylistFab.setOnActionSelectedListener(null)
|
binding.homeNewPlaylistFab.setOnActionSelectedListener(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -577,8 +586,6 @@ class HomeFragment :
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logD(binding.homeShuffleFab.isOrWillBeShown)
|
|
||||||
|
|
||||||
if (binding.homeShuffleFab.isOrWillBeShown) {
|
if (binding.homeShuffleFab.isOrWillBeShown) {
|
||||||
logD("Animating transition")
|
logD("Animating transition")
|
||||||
binding.homeShuffleFab.hide(
|
binding.homeShuffleFab.hide(
|
||||||
|
@ -606,12 +613,51 @@ class HomeFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSpeedDialBoundaryTouch(event: MotionEvent) {
|
private fun updateSpeedDial(open: Boolean) {
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
if (binding.homeNewPlaylistFab.isOpen &&
|
|
||||||
!binding.homeNewPlaylistFab.isUnder(event.x, event.y)) {
|
binding.root.rootView.apply {
|
||||||
binding.homeNewPlaylistFab.close()
|
// 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?) {
|
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. */
|
/** A marker for whether the user is fast-scrolling in the home view or not. */
|
||||||
val isFastScrolling: StateFlow<Boolean> = _isFastScrolling
|
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>()
|
private val _showOuter = MutableEvent<Outer>()
|
||||||
val showOuter: Event<Outer>
|
val showOuter: Event<Outer>
|
||||||
get() = _showOuter
|
get() = _showOuter
|
||||||
|
@ -293,6 +297,16 @@ constructor(
|
||||||
_isFastScrolling.value = isFastScrolling
|
_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() {
|
fun showSettings() {
|
||||||
_showOuter.put(Outer.Settings)
|
_showOuter.put(Outer.Settings)
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,7 @@ import org.oxycblt.auxio.util.getDimenPixels
|
||||||
class ThemedSpeedDialView : SpeedDialView {
|
class ThemedSpeedDialView : SpeedDialView {
|
||||||
private var mainFabAnimator: Animator? = null
|
private var mainFabAnimator: Animator? = null
|
||||||
private val spacingSmall = context.getDimenPixels(R.dimen.spacing_small)
|
private val spacingSmall = context.getDimenPixels(R.dimen.spacing_small)
|
||||||
|
private var innerChangeListener: ((Boolean) -> Unit)? = null
|
||||||
|
|
||||||
constructor(context: Context) : super(context)
|
constructor(context: Context) : super(context)
|
||||||
|
|
||||||
|
@ -126,6 +127,7 @@ class ThemedSpeedDialView : SpeedDialView {
|
||||||
})
|
})
|
||||||
start()
|
start()
|
||||||
}
|
}
|
||||||
|
innerChangeListener?.invoke(isOpen)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -178,8 +180,6 @@ class ThemedSpeedDialView : SpeedDialView {
|
||||||
val labelColor = context.getAttrColorCompat(android.R.attr.textColorSecondary)
|
val labelColor = context.getAttrColorCompat(android.R.attr.textColorSecondary)
|
||||||
val labelBackgroundColor =
|
val labelBackgroundColor =
|
||||||
context.getAttrColorCompat(com.google.android.material.R.attr.colorSurface)
|
context.getAttrColorCompat(com.google.android.material.R.attr.colorSurface)
|
||||||
val labelStroke =
|
|
||||||
context.getAttrColorCompat(com.google.android.material.R.attr.colorOutline)
|
|
||||||
val labelElevation =
|
val labelElevation =
|
||||||
context.getDimen(com.google.android.material.R.dimen.m3_card_elevated_elevation)
|
context.getDimen(com.google.android.material.R.dimen.m3_card_elevated_elevation)
|
||||||
val cornerRadius = context.getDimenPixels(R.dimen.spacing_medium)
|
val cornerRadius = context.getDimenPixels(R.dimen.spacing_medium)
|
||||||
|
@ -237,6 +237,10 @@ class ThemedSpeedDialView : SpeedDialView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setChangeListener(listener: ((Boolean) -> Unit)?) {
|
||||||
|
innerChangeListener = listener
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val VIEW_PROPERTY_BACKGROUND_TINT =
|
private val VIEW_PROPERTY_BACKGROUND_TINT =
|
||||||
object : Property<View, Int>(Int::class.java, "backgroundTint") {
|
object : Property<View, Int>(Int::class.java, "backgroundTint") {
|
||||||
|
|
|
@ -224,9 +224,9 @@ constructor(
|
||||||
* @param playlist The [Playlist] to rename,
|
* @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 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
|
* @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
|
* @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(
|
fun renamePlaylist(
|
||||||
playlist: Playlist,
|
playlist: Playlist,
|
||||||
|
|
Loading…
Reference in a new issue