ui: split up back listeners
Split up the back gesture listeners into specific components. These are still all used in MainFragment since I can't reliably set up their priority correctly if they were used in their respective fragments, but it should improve efficiency since most of these back listeners don't need to be updated on every draw.
This commit is contained in:
parent
841ea3620a
commit
5d51adfb0a
1 changed files with 98 additions and 72 deletions
|
@ -26,6 +26,7 @@ import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.fragment.app.FragmentContainerView
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavDestination
|
import androidx.navigation.NavDestination
|
||||||
|
@ -80,7 +81,10 @@ class MainFragment :
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val selectionModel: SelectionViewModel by activityViewModels()
|
private val selectionModel: SelectionViewModel by activityViewModels()
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
private val callback = DynamicBackPressedCallback()
|
private lateinit var sheetBackCallback: SheetBackPressedCallback
|
||||||
|
private lateinit var detailBackCallback: DetailBackPressedCallback
|
||||||
|
private lateinit var selectionBackCallback: SelectionBackPressedCallback
|
||||||
|
private lateinit var exploreBackCallback: ExploreBackPressedCallback
|
||||||
private var lastInsets: WindowInsets? = null
|
private var lastInsets: WindowInsets? = null
|
||||||
private var elevationNormal = 0f
|
private var elevationNormal = 0f
|
||||||
private var initialNavDestinationChange = true
|
private var initialNavDestinationChange = true
|
||||||
|
@ -96,13 +100,34 @@ class MainFragment :
|
||||||
override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) {
|
||||||
super.onBindingCreated(binding, savedInstanceState)
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
|
|
||||||
|
val playbackSheetBehavior =
|
||||||
|
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||||
|
val queueSheetBehavior =
|
||||||
|
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
||||||
|
|
||||||
elevationNormal = binding.context.getDimen(R.dimen.elevation_normal)
|
elevationNormal = binding.context.getDimen(R.dimen.elevation_normal)
|
||||||
|
|
||||||
|
// Currently all back press callbacks are handled in MainFragment, as it's not guaranteed
|
||||||
|
// that instantiating these callbacks in their respective fragments would result in the
|
||||||
|
// correct order.
|
||||||
|
sheetBackCallback =
|
||||||
|
SheetBackPressedCallback(
|
||||||
|
playbackSheetBehavior = playbackSheetBehavior,
|
||||||
|
queueSheetBehavior = queueSheetBehavior)
|
||||||
|
detailBackCallback = DetailBackPressedCallback(detailModel)
|
||||||
|
selectionBackCallback = SelectionBackPressedCallback(selectionModel)
|
||||||
|
exploreBackCallback = ExploreBackPressedCallback(binding.exploreNavHost)
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
val context = requireActivity()
|
val context = requireActivity()
|
||||||
// Override the back pressed listener so we can map back navigation to collapsing
|
// Override the back pressed listener so we can map back navigation to collapsing
|
||||||
// navigation, navigation out of detail views, etc.
|
// navigation, navigation out of detail views, etc.
|
||||||
context.onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
|
context.onBackPressedDispatcher.apply {
|
||||||
|
addCallback(viewLifecycleOwner, exploreBackCallback)
|
||||||
|
addCallback(viewLifecycleOwner, selectionBackCallback)
|
||||||
|
addCallback(viewLifecycleOwner, detailBackCallback)
|
||||||
|
addCallback(viewLifecycleOwner, sheetBackCallback)
|
||||||
|
}
|
||||||
|
|
||||||
binding.root.setOnApplyWindowInsetsListener { _, insets ->
|
binding.root.setOnApplyWindowInsetsListener { _, insets ->
|
||||||
lastInsets = insets
|
lastInsets = insets
|
||||||
|
@ -115,13 +140,9 @@ class MainFragment :
|
||||||
ViewCompat.setAccessibilityPaneTitle(
|
ViewCompat.setAccessibilityPaneTitle(
|
||||||
binding.queueSheet, context.getString(R.string.lbl_queue))
|
binding.queueSheet, context.getString(R.string.lbl_queue))
|
||||||
|
|
||||||
val queueSheetBehavior =
|
|
||||||
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
|
||||||
if (queueSheetBehavior != null) {
|
if (queueSheetBehavior != null) {
|
||||||
// In portrait mode, set up click listeners on the stacked sheets.
|
// In portrait mode, set up click listeners on the stacked sheets.
|
||||||
logD("Configuring stacked bottom sheets")
|
logD("Configuring stacked bottom sheets")
|
||||||
val playbackSheetBehavior =
|
|
||||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
|
||||||
unlikelyToBeNull(binding.queueHandleWrapper).setOnClickListener {
|
unlikelyToBeNull(binding.queueHandleWrapper).setOnClickListener {
|
||||||
if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED &&
|
if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED &&
|
||||||
queueSheetBehavior.state == BackportBottomSheetBehavior.STATE_COLLAPSED) {
|
queueSheetBehavior.state == BackportBottomSheetBehavior.STATE_COLLAPSED) {
|
||||||
|
@ -148,13 +169,15 @@ class MainFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
collect(navModel.mainNavigationAction.flow, ::handleMainNavigation)
|
collectImmediately(detailModel.editedPlaylist, detailBackCallback::invalidateEnabled)
|
||||||
collect(navModel.exploreNavigationItem.flow, ::handleExploreNavigation)
|
collectImmediately(selectionModel.selected, selectionBackCallback::invalidateEnabled)
|
||||||
collect(navModel.exploreArtistNavigationItem.flow, ::handleArtistNavigationPicker)
|
|
||||||
collect(musicModel.newPlaylistSongs.flow, ::handleNewPlaylist)
|
collect(musicModel.newPlaylistSongs.flow, ::handleNewPlaylist)
|
||||||
collect(musicModel.playlistToRename.flow, ::handleRenamePlaylist)
|
collect(musicModel.playlistToRename.flow, ::handleRenamePlaylist)
|
||||||
collect(musicModel.playlistToDelete.flow, ::handleDeletePlaylist)
|
collect(musicModel.playlistToDelete.flow, ::handleDeletePlaylist)
|
||||||
collect(musicModel.songsToAdd.flow, ::handleAddToPlaylist)
|
collect(musicModel.songsToAdd.flow, ::handleAddToPlaylist)
|
||||||
|
collect(navModel.mainNavigationAction.flow, ::handleMainNavigation)
|
||||||
|
collect(navModel.exploreNavigationItem.flow, ::handleExploreNavigation)
|
||||||
|
collect(navModel.exploreArtistNavigationItem.flow, ::handleArtistNavigationPicker)
|
||||||
collectImmediately(playbackModel.song, ::updateSong)
|
collectImmediately(playbackModel.song, ::updateSong)
|
||||||
collect(playbackModel.artistPickerSong.flow, ::handlePlaybackArtistPicker)
|
collect(playbackModel.artistPickerSong.flow, ::handlePlaybackArtistPicker)
|
||||||
collect(playbackModel.genrePickerSong.flow, ::handlePlaybackGenrePicker)
|
collect(playbackModel.genrePickerSong.flow, ::handlePlaybackGenrePicker)
|
||||||
|
@ -264,7 +287,7 @@ class MainFragment :
|
||||||
|
|
||||||
// Since the navigation listener is also reliant on the bottom sheets, we must also update
|
// Since the navigation listener is also reliant on the bottom sheets, we must also update
|
||||||
// it every frame.
|
// it every frame.
|
||||||
callback.invalidateEnabled()
|
sheetBackCallback.invalidateEnabled()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -277,6 +300,7 @@ class MainFragment :
|
||||||
// Drop the initial call by NavController that simply provides us with the current
|
// Drop the initial call by NavController that simply provides us with the current
|
||||||
// destination. This would cause the selection state to be lost every time the device
|
// destination. This would cause the selection state to be lost every time the device
|
||||||
// rotates.
|
// rotates.
|
||||||
|
exploreBackCallback.invalidateEnabled()
|
||||||
if (!initialNavDestinationChange) {
|
if (!initialNavDestinationChange) {
|
||||||
initialNavDestinationChange = true
|
initialNavDestinationChange = true
|
||||||
return
|
return
|
||||||
|
@ -400,7 +424,7 @@ class MainFragment :
|
||||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||||
if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) {
|
if (playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) {
|
||||||
// Playback sheet (and possibly queue) needs to be collapsed.
|
// Playback sheet (and possibly queue) needs to be collapsed.
|
||||||
logD("Closing playback and queue sheets")
|
logD("Collapsing playback and queue sheets")
|
||||||
val queueSheetBehavior =
|
val queueSheetBehavior =
|
||||||
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
||||||
playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED
|
playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
@ -449,82 +473,84 @@ class MainFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private class SheetBackPressedCallback(
|
||||||
* A [OnBackPressedCallback] that overrides the back button to first navigate out of internal
|
private val playbackSheetBehavior: PlaybackBottomSheetBehavior<*>,
|
||||||
* app components, such as the Bottom Sheets or Explore Navigation.
|
private val queueSheetBehavior: QueueBottomSheetBehavior<*>?
|
||||||
*/
|
) : OnBackPressedCallback(false) {
|
||||||
private inner class DynamicBackPressedCallback : OnBackPressedCallback(false) {
|
|
||||||
override fun handleOnBackPressed() {
|
override fun handleOnBackPressed() {
|
||||||
val binding = requireBinding()
|
|
||||||
val playbackSheetBehavior =
|
|
||||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
|
||||||
val queueSheetBehavior =
|
|
||||||
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
|
||||||
|
|
||||||
// If expanded, collapse the queue sheet first.
|
// If expanded, collapse the queue sheet first.
|
||||||
if (queueSheetBehavior != null &&
|
if (queueSheetShown()) {
|
||||||
queueSheetBehavior.state != BackportBottomSheetBehavior.STATE_COLLAPSED &&
|
unlikelyToBeNull(queueSheetBehavior).state =
|
||||||
playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) {
|
BackportBottomSheetBehavior.STATE_COLLAPSED
|
||||||
logD("Hiding queue sheet")
|
logD("Collapsed queue sheet")
|
||||||
queueSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If expanded, collapse the playback sheet next.
|
// If expanded, collapse the playback sheet next.
|
||||||
if (playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_COLLAPSED &&
|
if (playbackSheetShown()) {
|
||||||
playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_HIDDEN) {
|
|
||||||
logD("Hiding playback sheet")
|
|
||||||
playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED
|
playbackSheetBehavior.state = BackportBottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
logD("Collapsed playback sheet")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear out pending playlist edits.
|
|
||||||
if (detailModel.dropPlaylistEdit()) {
|
|
||||||
logD("Dropping playlist edits")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear out any prior selections.
|
|
||||||
if (selectionModel.drop()) {
|
|
||||||
logD("Dropping selection")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then try to navigate out of the explore navigation fragments (i.e Detail Views)
|
|
||||||
logD("Navigate away from explore view")
|
|
||||||
binding.exploreNavHost.findNavController().navigateUp()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Force this instance to update whether it's enabled or not. If there are no app components
|
|
||||||
* that the back button should close first, the instance is disabled and back navigation is
|
|
||||||
* delegated to the system.
|
|
||||||
*
|
|
||||||
* Normally, this listener would have just called the [MainActivity.onBackPressed] if there
|
|
||||||
* were no components to close, but that prevents adaptive back navigation from working on
|
|
||||||
* Android 14+, so we must do it this way.
|
|
||||||
*/
|
|
||||||
fun invalidateEnabled() {
|
fun invalidateEnabled() {
|
||||||
val binding = requireBinding()
|
isEnabled = queueSheetShown() || playbackSheetShown()
|
||||||
val playbackSheetBehavior =
|
}
|
||||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
|
||||||
val queueSheetBehavior =
|
|
||||||
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
|
||||||
val exploreNavController = binding.exploreNavHost.findNavController()
|
|
||||||
|
|
||||||
// TODO: Chain these listeners in some way instead of keeping them all here,
|
private fun playbackSheetShown() =
|
||||||
// assuming listeners added later have more priority
|
playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_COLLAPSED &&
|
||||||
|
playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_HIDDEN
|
||||||
|
|
||||||
|
private fun queueSheetShown() =
|
||||||
|
queueSheetBehavior != null &&
|
||||||
|
queueSheetBehavior.state != BackportBottomSheetBehavior.STATE_COLLAPSED &&
|
||||||
|
playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DetailBackPressedCallback(private val detailModel: DetailViewModel) :
|
||||||
|
OnBackPressedCallback(false) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
if (detailModel.dropPlaylistEdit()) {
|
||||||
|
logD("Dropped playlist edits")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invalidateEnabled(playlistEdit: List<Song>?) {
|
||||||
|
isEnabled = playlistEdit != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class SelectionBackPressedCallback(
|
||||||
|
private val selectionModel: SelectionViewModel
|
||||||
|
) : OnBackPressedCallback(false) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
if (selectionModel.drop()) {
|
||||||
|
logD("Dropped selection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invalidateEnabled(selection: List<Music>) {
|
||||||
|
isEnabled = selection.isNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ExploreBackPressedCallback(
|
||||||
|
private val exploreNavHost: FragmentContainerView
|
||||||
|
) : OnBackPressedCallback(false) {
|
||||||
|
// Note: We cannot cache the NavController in a variable since it's current destination
|
||||||
|
// value goes stale for some reason.
|
||||||
|
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
exploreNavHost.findNavController().navigateUp()
|
||||||
|
logD("Forwarded back navigation to explore nav host")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invalidateEnabled() {
|
||||||
|
val exploreNavController = exploreNavHost.findNavController()
|
||||||
isEnabled =
|
isEnabled =
|
||||||
(queueSheetBehavior != null &&
|
exploreNavController.currentDestination?.id !=
|
||||||
queueSheetBehavior.state != BackportBottomSheetBehavior.STATE_COLLAPSED &&
|
exploreNavController.graph.startDestinationId
|
||||||
playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED) ||
|
|
||||||
(playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_COLLAPSED &&
|
|
||||||
playbackSheetBehavior.state != BackportBottomSheetBehavior.STATE_HIDDEN) ||
|
|
||||||
detailModel.editedPlaylist.value != null ||
|
|
||||||
selectionModel.selected.value.isNotEmpty() ||
|
|
||||||
exploreNavController.currentDestination?.id !=
|
|
||||||
exploreNavController.graph.startDestinationId
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue