From a2f27f303b014df8ac3ad886fc9ba3515ef50138 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 27 Aug 2022 16:15:08 -0600 Subject: [PATCH] ui: add predictive back gesture Rework the back pressed callbacks to support a predictive back gesture. This completes the trivial Android 13 reworks. --- CHANGELOG.md | 3 +- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 1 + .../java/org/oxycblt/auxio/MainFragment.kt | 60 ++++++++++--------- .../org/oxycblt/auxio/home/HomeFragment.kt | 42 ++++++------- .../auxio/playback/AnimatedMaterialButton.kt | 4 +- 6 files changed, 61 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88897910b..949f8e756 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,10 @@ #### What's New - Added Android 13 support [#129] - Switch to new storage permissions - - Add monochrome icon + - Add themed icon - Fix issue where widget covers would not load - Use new media notification panel style + - Add predictive back navigation #### What's Improved - Playback bar now has a marquee effect diff --git a/app/build.gradle b/app/build.gradle index 0a0c5b385..0716ff346 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -70,7 +70,7 @@ dependencies { // General 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" // UI diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5393cbd0e..7f31072e4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -26,6 +26,7 @@ android:theme="@style/Theme.Auxio.App" android:dataExtractionRules="@xml/data_extraction_rules" android:appCategory="audio" + android:enableOnBackInvokedCallback="true" tools:ignore="UnusedAttribute"> (), ViewTreeObserver.OnPreDrawListener { private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val navModel: NavigationViewModel by activityViewModels() - private var callback: DynamicBackPressedCallback? = null + private val callback = DynamicBackPressedCallback() 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?) { super.onCreate(savedInstanceState) @@ -69,10 +72,7 @@ class MainFragment : // --- UI SETUP --- val context = requireActivity() - context.onBackPressedDispatcher.addCallback( - viewLifecycleOwner, DynamicBackPressedCallback().also { callback = it }) - - elevationNormal = requireContext().getDimen(R.dimen.elevation_normal) + context.onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback) binding.root.setOnApplyWindowInsetsListener { _, insets -> lastInsets = insets @@ -128,16 +128,6 @@ class MainFragment : requireBinding().playbackSheet.viewTreeObserver.addOnPreDrawListener(this) } - override fun onResume() { - super.onResume() - callback?.isEnabled = true - } - - override fun onPause() { - super.onPause() - callback?.isEnabled = false - } - override fun onStop() { super.onStop() requireBinding().playbackSheet.viewTreeObserver.removeOnPreDrawListener(this) @@ -209,6 +199,10 @@ class MainFragment : tryHideAll() } + // Since the callback is also reliant on the bottom sheets, we must also update it + // every frame. + callback.updateEnabledState() + return true } @@ -322,10 +316,8 @@ class MainFragment : private inner class DynamicBackPressedCallback : OnBackPressedCallback(false) { override fun handleOnBackPressed() { val binding = requireBinding() - val playbackSheetBehavior = binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior - val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior? @@ -344,15 +336,29 @@ class MainFragment : return } - // First navigate upwards in the explore graph, then navigate back in the activity. - val navController = binding.exploreNavHost.findNavController() - if (navController.currentDestination?.id == navController.graph.startDestinationId) { - isEnabled = false - requireActivity().onBackPressed() - isEnabled = true - } else { - navController.navigateUp() - } + binding.exploreNavHost.findNavController().navigateUp() + } + + fun updateEnabledState() { + val binding = requireBinding() + val playbackSheetBehavior = + binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior + 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 } } } 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 c00163b2c..af7135d72 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -84,10 +84,9 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI // 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 // our transitions. - if (savedInstanceState.getBoolean(KEY_INIT_WITH_SEARCH_TRANSITIONS)) { - initSearchTransitions() - } else { - initDetailTransitions() + val axis = savedInstanceState.getInt(KEY_LAST_TRANSITION_AXIS, -1) + if (axis > -1) { + initAxisTransitions(axis) } } } @@ -152,7 +151,11 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI } 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) } @@ -165,7 +168,7 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI when (item.itemId) { R.id.action_search -> { logD("Navigating to search") - initSearchTransitions() + initAxisTransitions(MaterialSharedAxis.Z) findNavController().navigate(HomeFragmentDirections.actionShowSearch()) } R.id.action_settings -> { @@ -376,23 +379,22 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI else -> return } - initDetailTransitions() + initAxisTransitions(MaterialSharedAxis.X) findNavController().navigate(action) } - private fun initSearchTransitions() { - enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) - returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) - exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) - reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) - } + private fun initAxisTransitions(axis: Int) { + // Sanity check + if (axis != MaterialSharedAxis.X && axis != MaterialSharedAxis.Z) { + logW("Invalid axis provided") + return + } - private fun initDetailTransitions() { - enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) - returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) - exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) - reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + enterTransition = MaterialSharedAxis(axis, true) + returnTransition = MaterialSharedAxis(axis, false) + exitTransition = MaterialSharedAxis(axis, true) + reenterTransition = MaterialSharedAxis(axis, false) } /** @@ -433,7 +435,7 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI lazyReflectedField(ViewPager2::class, "mRecyclerView") private val VIEW_PAGER_TOUCH_SLOP_FIELD: Field by lazyReflectedField(RecyclerView::class, "mTouchSlop") - private const val KEY_INIT_WITH_SEARCH_TRANSITIONS = - BuildConfig.APPLICATION_ID + ".key.INIT_WITH_SEARCH_TRANSITIONS" + private const val KEY_LAST_TRANSITION_AXIS = + BuildConfig.APPLICATION_ID + ".key.LAST_TRANSITION_AXIS" } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/AnimatedMaterialButton.kt b/app/src/main/java/org/oxycblt/auxio/playback/AnimatedMaterialButton.kt index d0f599a11..c7f1c7471 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/AnimatedMaterialButton.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/AnimatedMaterialButton.kt @@ -23,8 +23,8 @@ import android.util.AttributeSet import com.google.android.material.button.MaterialButton /** - * A [MaterialButton] that automatically morphs from a circle to a squircle shape appearance when - * it is activated. + * A [MaterialButton] that automatically morphs from a circle to a squircle shape appearance when it + * is activated. */ class AnimatedMaterialButton @JvmOverloads