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
- 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

View file

@ -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

View file

@ -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">
<activity

View file

@ -53,9 +53,12 @@ class MainFragment :
ViewBindingFragment<FragmentMainBinding>(), 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
}
}
}

View file

@ -84,10 +84,9 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), 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<FragmentHomeBinding>(), 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<FragmentHomeBinding>(), 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<FragmentHomeBinding>(), 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<FragmentHomeBinding>(), 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"
}
}

View file

@ -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