ui: add predictive back gesture

Rework the back pressed callbacks to support a predictive back gesture.

This completes the trivial Android 13 reworks.
This commit is contained in:
Alexander Capehart 2022-08-27 16:15:08 -06:00
parent 0b43dd011c
commit a2f27f303b
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
6 changed files with 61 additions and 51 deletions

View file

@ -5,9 +5,10 @@
#### What's New #### What's New
- Added Android 13 support [#129] - Added Android 13 support [#129]
- Switch to new storage permissions - Switch to new storage permissions
- Add monochrome icon - Add themed icon
- Fix issue where widget covers would not load - Fix issue where widget covers would not load
- Use new media notification panel style - Use new media notification panel style
- Add predictive back navigation
#### What's Improved #### What's Improved
- Playback bar now has a marquee effect - Playback bar now has a marquee effect

View file

@ -70,7 +70,7 @@ dependencies {
// General // General
implementation "androidx.core:core-ktx:1.8.0" implementation "androidx.core:core-ktx:1.8.0"
implementation "androidx.activity:activity-ktx:1.5.1" implementation "androidx.activity:activity-ktx:1.6.0-rc01"
implementation "androidx.fragment:fragment-ktx:1.5.2" implementation "androidx.fragment:fragment-ktx:1.5.2"
// UI // UI

View file

@ -26,6 +26,7 @@
android:theme="@style/Theme.Auxio.App" android:theme="@style/Theme.Auxio.App"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:appCategory="audio" android:appCategory="audio"
android:enableOnBackInvokedCallback="true"
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">
<activity <activity

View file

@ -53,9 +53,12 @@ class MainFragment :
ViewBindingFragment<FragmentMainBinding>(), ViewTreeObserver.OnPreDrawListener { ViewBindingFragment<FragmentMainBinding>(), ViewTreeObserver.OnPreDrawListener {
private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val navModel: NavigationViewModel by activityViewModels() private val navModel: NavigationViewModel by activityViewModels()
private var callback: DynamicBackPressedCallback? = null private val callback = DynamicBackPressedCallback()
private var lastInsets: WindowInsets? = null private var lastInsets: WindowInsets? = null
private var elevationNormal = -1f
private val elevationNormal: Float by lifecycleObject { binding ->
binding.context.getDimen(R.dimen.elevation_normal)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -69,10 +72,7 @@ class MainFragment :
// --- UI SETUP --- // --- UI SETUP ---
val context = requireActivity() val context = requireActivity()
context.onBackPressedDispatcher.addCallback( context.onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
viewLifecycleOwner, DynamicBackPressedCallback().also { callback = it })
elevationNormal = requireContext().getDimen(R.dimen.elevation_normal)
binding.root.setOnApplyWindowInsetsListener { _, insets -> binding.root.setOnApplyWindowInsetsListener { _, insets ->
lastInsets = insets lastInsets = insets
@ -128,16 +128,6 @@ class MainFragment :
requireBinding().playbackSheet.viewTreeObserver.addOnPreDrawListener(this) requireBinding().playbackSheet.viewTreeObserver.addOnPreDrawListener(this)
} }
override fun onResume() {
super.onResume()
callback?.isEnabled = true
}
override fun onPause() {
super.onPause()
callback?.isEnabled = false
}
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
requireBinding().playbackSheet.viewTreeObserver.removeOnPreDrawListener(this) requireBinding().playbackSheet.viewTreeObserver.removeOnPreDrawListener(this)
@ -209,6 +199,10 @@ class MainFragment :
tryHideAll() tryHideAll()
} }
// Since the callback is also reliant on the bottom sheets, we must also update it
// every frame.
callback.updateEnabledState()
return true return true
} }
@ -322,10 +316,8 @@ class MainFragment :
private inner class DynamicBackPressedCallback : OnBackPressedCallback(false) { private inner class DynamicBackPressedCallback : OnBackPressedCallback(false) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
val binding = requireBinding() val binding = requireBinding()
val playbackSheetBehavior = val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
val queueSheetBehavior = val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior? binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
@ -344,15 +336,29 @@ class MainFragment :
return return
} }
// First navigate upwards in the explore graph, then navigate back in the activity. binding.exploreNavHost.findNavController().navigateUp()
val navController = binding.exploreNavHost.findNavController() }
if (navController.currentDestination?.id == navController.graph.startDestinationId) {
isEnabled = false fun updateEnabledState() {
requireActivity().onBackPressed() val binding = requireBinding()
isEnabled = true val playbackSheetBehavior =
} else { binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
navController.navigateUp() val queueSheetBehavior =
} binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
val exploreNavController = binding.exploreNavHost.findNavController()
logD(
playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
queueSheetBehavior?.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
exploreNavController.currentDestination?.id !=
exploreNavController.graph.startDestinationId)
isEnabled =
playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
queueSheetBehavior?.state == NeoBottomSheetBehavior.STATE_EXPANDED ||
exploreNavController.currentDestination?.id !=
exploreNavController.graph.startDestinationId
} }
} }
} }

View file

@ -84,10 +84,9 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
// Orientation change will wipe whatever transition we were using prior, which will // Orientation change will wipe whatever transition we were using prior, which will
// result in no transition when the user navigates back. Make sure we re-initialize // result in no transition when the user navigates back. Make sure we re-initialize
// our transitions. // our transitions.
if (savedInstanceState.getBoolean(KEY_INIT_WITH_SEARCH_TRANSITIONS)) { val axis = savedInstanceState.getInt(KEY_LAST_TRANSITION_AXIS, -1)
initSearchTransitions() if (axis > -1) {
} else { initAxisTransitions(axis)
initDetailTransitions()
} }
} }
} }
@ -152,7 +151,11 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(KEY_INIT_WITH_SEARCH_TRANSITIONS, enterTransition is MaterialSharedAxis) val enter = enterTransition
if (enter is MaterialSharedAxis) {
outState.putInt(KEY_LAST_TRANSITION_AXIS, enter.axis)
}
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
@ -165,7 +168,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
when (item.itemId) { when (item.itemId) {
R.id.action_search -> { R.id.action_search -> {
logD("Navigating to search") logD("Navigating to search")
initSearchTransitions() initAxisTransitions(MaterialSharedAxis.Z)
findNavController().navigate(HomeFragmentDirections.actionShowSearch()) findNavController().navigate(HomeFragmentDirections.actionShowSearch())
} }
R.id.action_settings -> { R.id.action_settings -> {
@ -376,23 +379,22 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
else -> return else -> return
} }
initDetailTransitions() initAxisTransitions(MaterialSharedAxis.X)
findNavController().navigate(action) findNavController().navigate(action)
} }
private fun initSearchTransitions() { private fun initAxisTransitions(axis: Int) {
enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) // Sanity check
returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) if (axis != MaterialSharedAxis.X && axis != MaterialSharedAxis.Z) {
exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) logW("Invalid axis provided")
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) return
} }
private fun initDetailTransitions() { enterTransition = MaterialSharedAxis(axis, true)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) returnTransition = MaterialSharedAxis(axis, false)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) exitTransition = MaterialSharedAxis(axis, true)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) reenterTransition = MaterialSharedAxis(axis, false)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
} }
/** /**
@ -433,7 +435,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
lazyReflectedField(ViewPager2::class, "mRecyclerView") lazyReflectedField(ViewPager2::class, "mRecyclerView")
private val VIEW_PAGER_TOUCH_SLOP_FIELD: Field by private val VIEW_PAGER_TOUCH_SLOP_FIELD: Field by
lazyReflectedField(RecyclerView::class, "mTouchSlop") lazyReflectedField(RecyclerView::class, "mTouchSlop")
private const val KEY_INIT_WITH_SEARCH_TRANSITIONS = private const val KEY_LAST_TRANSITION_AXIS =
BuildConfig.APPLICATION_ID + ".key.INIT_WITH_SEARCH_TRANSITIONS" BuildConfig.APPLICATION_ID + ".key.LAST_TRANSITION_AXIS"
} }
} }

View file

@ -23,8 +23,8 @@ import android.util.AttributeSet
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
/** /**
* A [MaterialButton] that automatically morphs from a circle to a squircle shape appearance when * A [MaterialButton] that automatically morphs from a circle to a squircle shape appearance when it
* it is activated. * is activated.
*/ */
class AnimatedMaterialButton class AnimatedMaterialButton
@JvmOverloads @JvmOverloads