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:
parent
0b43dd011c
commit
a2f27f303b
6 changed files with 61 additions and 51 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue