From 2b55caadd105c34de3d827a4222a26423f953f75 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 2 Jan 2024 17:16:13 -0700 Subject: [PATCH] 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. --- .../java/org/oxycblt/auxio/MainFragment.kt | 26 ++++++++ .../org/oxycblt/auxio/home/HomeFragment.kt | 62 ++++++++++++++++--- .../org/oxycblt/auxio/home/HomeViewModel.kt | 14 +++++ .../oxycblt/auxio/home/ThemedSpeedDialView.kt | 8 ++- .../org/oxycblt/auxio/music/MusicViewModel.kt | 4 +- 5 files changed, 102 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index ed1b47c7a..742018420 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -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 + } + } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index fca2bf924..aacf82749 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -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(R.id.main_scrim).setOnClickListener { + homeModel.setSpeedDialOpen(false) + } + findViewById(R.id.main_scrim).setOnTouchListener { _, event -> handleSpeedDialBoundaryTouch(event) - false + } + + findViewById(R.id.sheet_scrim).setOnClickListener { + homeModel.setSpeedDialOpen(false) } findViewById(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(R.id.main_scrim).isClickable = open + findViewById(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?) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index bb9311c84..51bc04976 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -156,6 +156,10 @@ constructor( /** A marker for whether the user is fast-scrolling in the home view or not. */ val isFastScrolling: StateFlow = _isFastScrolling + private val _speedDialOpen = MutableStateFlow(false) + /** A marker for whether the speed dial is open or not. */ + val speedDialOpen: StateFlow = _speedDialOpen + private val _showOuter = MutableEvent() val showOuter: Event 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) } diff --git a/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt b/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt index ab978eed4..1a9b5174d 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/ThemedSpeedDialView.kt @@ -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(Int::class.java, "backgroundTint") { diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt index 97a8fd8f0..6140261a9 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -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,