From ff9e35b2e0506b8fd1de03df0acdb0b5bb72e036 Mon Sep 17 00:00:00 2001 From: Ryan Moelter Date: Sun, 14 Jan 2024 10:50:43 -0800 Subject: [PATCH 1/7] Add predictive back handling --- .../magellanx/compose/ComposeExtensions.kt | 8 ++++ .../magellanx/compose/ComposeJourney.kt | 4 +- .../compose/navigation/ComposeNavigator.kt | 31 +++++++++------ .../ComposePredictiveBackHandler.kt | 38 +++++++++++++++++++ .../core/lifecycle/LifecycleAware.kt | 2 - .../core/lifecycle/LifecycleAwareComponent.kt | 6 --- .../core/lifecycle/LifecycleRegistry.kt | 10 ----- 7 files changed, 69 insertions(+), 30 deletions(-) create mode 100644 magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposePredictiveBackHandler.kt diff --git a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeExtensions.kt b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeExtensions.kt index be9a157a..927c6c72 100644 --- a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeExtensions.kt +++ b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeExtensions.kt @@ -22,6 +22,14 @@ public fun Displayable<@Composable () -> Unit>.Content(modifier: Modifier = Modi } } +@Composable +@Suppress("ktlint:standard:function-naming") +public fun Displayable<@Composable (Arg) -> Unit>.Content(arg: Arg, modifier: Modifier = Modifier) { + Box(modifier = modifier) { + view!!(arg) + } +} + @Composable @Suppress("ktlint:standard:function-naming") public fun Displayable( diff --git a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeJourney.kt b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeJourney.kt index 9d4537f3..dcddf867 100644 --- a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeJourney.kt +++ b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeJourney.kt @@ -5,8 +5,10 @@ import com.ryanmoelter.magellanx.compose.navigation.ComposeNavigator import com.ryanmoelter.magellanx.core.lifecycle.attachFieldToLifecycle public abstract class ComposeJourney : ComposeStep() { - protected var navigator: ComposeNavigator by attachFieldToLifecycle(ComposeNavigator()) + protected var navigator: ComposeNavigator by attachFieldToLifecycle(ComposeNavigator { onNavigatedBackwards() }) @Composable protected override fun Content(): Unit = navigator.Content() + + protected open fun onNavigatedBackwards() { } } diff --git a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposeNavigator.kt b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposeNavigator.kt index 9d81a3ba..411a2218 100644 --- a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposeNavigator.kt +++ b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposeNavigator.kt @@ -13,6 +13,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.ryanmoelter.magellanx.compose.Content import com.ryanmoelter.magellanx.compose.WhenShown +import com.ryanmoelter.magellanx.compose.navigation.BackstackStatus.AT_ROOT +import com.ryanmoelter.magellanx.compose.navigation.BackstackStatus.BACK_AVAILABLE import com.ryanmoelter.magellanx.compose.navigation.Direction.BACKWARD import com.ryanmoelter.magellanx.compose.navigation.Direction.FORWARD import com.ryanmoelter.magellanx.compose.transitions.MagellanComposeTransition @@ -22,11 +24,17 @@ import com.ryanmoelter.magellanx.core.Displayable import com.ryanmoelter.magellanx.core.Navigable import com.ryanmoelter.magellanx.core.lifecycle.LifecycleAwareComponent import com.ryanmoelter.magellanx.core.lifecycle.LifecycleLimit +import com.ryanmoelter.magellanx.core.lifecycle.attachFieldToLifecycle +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map -public open class ComposeNavigator : - LifecycleAwareComponent(), Displayable<@Composable () -> Unit> { +public open class ComposeNavigator( + private val onNavigatedBackwards: () -> Unit = { } +) : LifecycleAwareComponent(), Displayable<@Composable () -> Unit> { + + private val backHandler by attachFieldToLifecycle(ComposePredictiveBackHandler(::goBack)) + /** * The backstack. The last item in each list is the top of the stack. */ @@ -37,6 +45,8 @@ public open class ComposeNavigator : */ public open val backStack: List get() = backStackFlow.value + public open val backStackStatusFlow: Flow = backStackFlow + .map { if (it.size <= 1) AT_ROOT else BACK_AVAILABLE } /** * Get a snapshot of the current navigable, i.e. the last item of the current [backStack]. @@ -65,6 +75,9 @@ public open class ComposeNavigator : val currentNavigable by currentNavigableFlow.collectAsState(null) val currentTransitionSpec by transitionFlow.collectAsState() val currentDirection by directionFlow.collectAsState() + val backstackStatus by backStackStatusFlow.collectAsState(initial = AT_ROOT) + + backHandler.Content(backstackStatus) AnimatedContent( targetState = currentNavigable, @@ -128,14 +141,12 @@ public open class ComposeNavigator : } } - public open fun goBack(): Boolean { - return if (!atRoot()) { - navigate(BACKWARD) { backStack -> - backStack - backStack.last() - } - true + public open fun goBack() { + if (!atRoot()) { + navigate(BACKWARD) { backStack -> backStack - backStack.last() } + onNavigatedBackwards() } else { - false + throw IllegalStateException("goBack() shouldn't be called with an empty backstack") } } @@ -204,8 +215,6 @@ public open class ComposeNavigator : } } - override fun onBackPressed(): Boolean = currentNavigable?.backPressed() ?: false || goBack() - public open fun atRoot(): Boolean = backStack.size <= 1 } diff --git a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposePredictiveBackHandler.kt b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposePredictiveBackHandler.kt new file mode 100644 index 00000000..c1e6f777 --- /dev/null +++ b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposePredictiveBackHandler.kt @@ -0,0 +1,38 @@ +package com.ryanmoelter.magellanx.compose.navigation + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import com.ryanmoelter.magellanx.compose.WhenShown +import com.ryanmoelter.magellanx.compose.navigation.BackstackStatus.* +import com.ryanmoelter.magellanx.core.Displayable +import com.ryanmoelter.magellanx.core.lifecycle.LifecycleAwareComponent + +/** + * Handle back events proactively, to work with Android's upcoming predictive back system. + * + * Note that you need to call [view] (or the Content(arg) extension function) in order for this to + * work. + * + * @see [Android's upcoming predictive back system](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture) + */ +public class ComposePredictiveBackHandler( + private val onBackTriggered: () -> Unit +) : LifecycleAwareComponent(), Displayable<@Composable (BackstackStatus) -> Unit> { + + override val view: @Composable (BackstackStatus) -> Unit + get() = { backstackStatus -> + WhenShown { + val enabled = when (backstackStatus) { + AT_ROOT -> false + BACK_AVAILABLE -> true + } + BackHandler(enabled = enabled) { + onBackTriggered() + } + } + } +} + +public enum class BackstackStatus { + AT_ROOT, BACK_AVAILABLE +} diff --git a/magellanx-core/src/main/java/com/ryanmoelter/magellanx/core/lifecycle/LifecycleAware.kt b/magellanx-core/src/main/java/com/ryanmoelter/magellanx/core/lifecycle/LifecycleAware.kt index 1889fcf3..34d732a7 100644 --- a/magellanx-core/src/main/java/com/ryanmoelter/magellanx/core/lifecycle/LifecycleAware.kt +++ b/magellanx-core/src/main/java/com/ryanmoelter/magellanx/core/lifecycle/LifecycleAware.kt @@ -12,6 +12,4 @@ public interface LifecycleAware { public fun hide() {} public fun destroy() {} - - public fun backPressed(): Boolean = false } diff --git a/magellanx-core/src/main/java/com/ryanmoelter/magellanx/core/lifecycle/LifecycleAwareComponent.kt b/magellanx-core/src/main/java/com/ryanmoelter/magellanx/core/lifecycle/LifecycleAwareComponent.kt index b68620a1..5a5d7b84 100644 --- a/magellanx-core/src/main/java/com/ryanmoelter/magellanx/core/lifecycle/LifecycleAwareComponent.kt +++ b/magellanx-core/src/main/java/com/ryanmoelter/magellanx/core/lifecycle/LifecycleAwareComponent.kt @@ -77,10 +77,6 @@ public abstract class LifecycleAwareComponent : LifecycleAware, LifecycleOwner { lifecycleRegistry.destroy() } - override fun backPressed(): Boolean { - return lifecycleRegistry.backPressed() || onBackPressed() - } - public fun attachToLifecycle(lifecycleAware: LifecycleAware) { attachToLifecycle(lifecycleAware, LifecycleState.Destroyed) } @@ -114,6 +110,4 @@ public abstract class LifecycleAwareComponent : LifecycleAware, LifecycleOwner { protected open fun onHide() {} protected open fun onDestroy() {} - - protected open fun onBackPressed(): Boolean = false } diff --git a/magellanx-core/src/main/java/com/ryanmoelter/magellanx/core/lifecycle/LifecycleRegistry.kt b/magellanx-core/src/main/java/com/ryanmoelter/magellanx/core/lifecycle/LifecycleRegistry.kt index 147d571b..49b1be6e 100644 --- a/magellanx-core/src/main/java/com/ryanmoelter/magellanx/core/lifecycle/LifecycleRegistry.kt +++ b/magellanx-core/src/main/java/com/ryanmoelter/magellanx/core/lifecycle/LifecycleRegistry.kt @@ -118,16 +118,6 @@ public class LifecycleRegistry : LifecycleAware, LifecycleOwner { override fun destroy() { currentState = LifecycleState.Destroyed } - - override fun backPressed(): Boolean = onAllActiveListenersUntilTrue { it.backPressed() } - - private fun onAllActiveListenersUntilTrue(action: (LifecycleAware) -> Boolean): Boolean = - listenersToMaxStates - .asSequence() - .filter { it.value >= SHOWN } - .map { it.key } - .map(action) - .any { it } } public enum class LifecycleLimit(internal val order: Int) { From 613a237dbd299a46463b1630808871642a51e153 Mon Sep 17 00:00:00 2001 From: Ryan Moelter Date: Sun, 14 Jan 2024 11:10:31 -0800 Subject: [PATCH 2/7] Allow intercepting back events --- .../com/ryanmoelter/magellanx/compose/ComposeJourney.kt | 4 ++-- .../magellanx/compose/navigation/ComposeNavigator.kt | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeJourney.kt b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeJourney.kt index dcddf867..bb7cd895 100644 --- a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeJourney.kt +++ b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeJourney.kt @@ -5,10 +5,10 @@ import com.ryanmoelter.magellanx.compose.navigation.ComposeNavigator import com.ryanmoelter.magellanx.core.lifecycle.attachFieldToLifecycle public abstract class ComposeJourney : ComposeStep() { - protected var navigator: ComposeNavigator by attachFieldToLifecycle(ComposeNavigator { onNavigatedBackwards() }) + protected var navigator: ComposeNavigator by attachFieldToLifecycle(ComposeNavigator(::interceptBack)) @Composable protected override fun Content(): Unit = navigator.Content() - protected open fun onNavigatedBackwards() { } + protected open fun interceptBack(performBack: () -> Unit): Unit = performBack() } diff --git a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposeNavigator.kt b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposeNavigator.kt index 411a2218..c473f2b5 100644 --- a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposeNavigator.kt +++ b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposeNavigator.kt @@ -30,7 +30,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map public open class ComposeNavigator( - private val onNavigatedBackwards: () -> Unit = { } + private val interceptBack: (performBack: () -> Unit) -> Unit = { } ) : LifecycleAwareComponent(), Displayable<@Composable () -> Unit> { private val backHandler by attachFieldToLifecycle(ComposePredictiveBackHandler(::goBack)) @@ -143,8 +143,9 @@ public open class ComposeNavigator( public open fun goBack() { if (!atRoot()) { - navigate(BACKWARD) { backStack -> backStack - backStack.last() } - onNavigatedBackwards() + interceptBack { + navigate(BACKWARD) { backStack -> backStack - backStack.last() } + } } else { throw IllegalStateException("goBack() shouldn't be called with an empty backstack") } From e29471d194ef384d2c6d5b59ebe0394b2a8850f5 Mon Sep 17 00:00:00 2001 From: Ryan Moelter Date: Sun, 14 Jan 2024 22:46:41 -0800 Subject: [PATCH 3/7] [WIP] Start to make a back transition --- .../magellanx/compose/ComposeJourney.kt | 7 +- .../compose/navigation/ComposeNavigator.kt | 68 ++++++++++++++++++- .../ComposePredictiveBackHandler.kt | 10 +-- .../transitions/BuiltInTransitionSpecs.kt | 3 +- 4 files changed, 78 insertions(+), 10 deletions(-) diff --git a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeJourney.kt b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeJourney.kt index bb7cd895..0f53c00c 100644 --- a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeJourney.kt +++ b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeJourney.kt @@ -5,10 +5,15 @@ import com.ryanmoelter.magellanx.compose.navigation.ComposeNavigator import com.ryanmoelter.magellanx.core.lifecycle.attachFieldToLifecycle public abstract class ComposeJourney : ComposeStep() { - protected var navigator: ComposeNavigator by attachFieldToLifecycle(ComposeNavigator(::interceptBack)) + + protected var navigator: ComposeNavigator by attachFieldToLifecycle( + ComposeNavigator(::interceptBack, ::interceptBackGestureAnimation) + ) @Composable protected override fun Content(): Unit = navigator.Content() protected open fun interceptBack(performBack: () -> Unit): Unit = performBack() + + protected open fun interceptBackGestureAnimation(): Boolean = false } diff --git a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposeNavigator.kt b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposeNavigator.kt index c473f2b5..38f7c2c2 100644 --- a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposeNavigator.kt +++ b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposeNavigator.kt @@ -2,15 +2,19 @@ package com.ryanmoelter.magellanx.compose.navigation +import androidx.activity.BackEventCompat import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale import com.ryanmoelter.magellanx.compose.Content import com.ryanmoelter.magellanx.compose.WhenShown import com.ryanmoelter.magellanx.compose.navigation.BackstackStatus.AT_ROOT @@ -30,10 +34,11 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map public open class ComposeNavigator( - private val interceptBack: (performBack: () -> Unit) -> Unit = { } + private val interceptBack: (performBack: () -> Unit) -> Unit = { }, + private val interceptBackGestureAnimation: () -> Boolean = { false } ) : LifecycleAwareComponent(), Displayable<@Composable () -> Unit> { - private val backHandler by attachFieldToLifecycle(ComposePredictiveBackHandler(::goBack)) + private val backHandler by attachFieldToLifecycle(ComposePredictiveBackHandler(::handlePredictiveBack)) /** * The backstack. The last item in each list is the top of the stack. @@ -56,12 +61,21 @@ public open class ComposeNavigator( private val currentNavigationEventFlow = backStackFlow.map { it.lastOrNull() } private val currentNavigableFlow = currentNavigationEventFlow.map { it?.navigable } + private val lastNavigableFlow = backStackFlow.map { stack -> + if (stack.size > 1) { + stack[stack.lastIndex - 1].navigable + } else { + null + } + } // TODO: make default transition configurable private val transitionFlow: MutableStateFlow = MutableStateFlow(defaultTransition) private val directionFlow: MutableStateFlow = MutableStateFlow(FORWARD) + private val backSwipeProgressFlow = MutableStateFlow(0f) + override val view: (@Composable () -> Unit) get() = { WhenShown { @@ -73,9 +87,11 @@ public open class ComposeNavigator( @Suppress("ktlint:standard:function-naming") private fun Content() { val currentNavigable by currentNavigableFlow.collectAsState(null) + val lastNavigable by lastNavigableFlow.collectAsState(null) val currentTransitionSpec by transitionFlow.collectAsState() val currentDirection by directionFlow.collectAsState() val backstackStatus by backStackStatusFlow.collectAsState(initial = AT_ROOT) + val backSwipeProgress by backSwipeProgressFlow.collectAsState() backHandler.Content(backstackStatus) @@ -103,7 +119,37 @@ public open class ComposeNavigator( }, ) Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - navigable?.Content() + val shouldShowPrevious by remember { + derivedStateOf { backSwipeProgress != 0f } + } + if (shouldShowPrevious) { + DisposableEffect(key1 = lastNavigable, key2 = shouldShowPrevious) { + val previous = lastNavigable + if (previous != null) { + lifecycleRegistry.updateMaxState(previous, LifecycleLimit.SHOWN) + onDispose { + if (children.contains(previous)) { + if (backStack.map { it.navigable }.contains(previous)) { + lifecycleRegistry.updateMaxState(previous, LifecycleLimit.CREATED) + } else { + removeFromLifecycle(previous) + } + } + } + } else { + onDispose { } + } + } + lastNavigable?.Content() + } + Box( + modifier = Modifier + .fillMaxSize() + .scale(1f - backSwipeProgress), + contentAlignment = Alignment.Center + ) { + navigable?.Content() + } } } } @@ -141,11 +187,27 @@ public open class ComposeNavigator( } } + private suspend fun handlePredictiveBack(backEventFlow: Flow) { + require(backStack.size > 1) { "Backstack is too small for predictive back gesture" } + // TODO: handle case where backstack is changed during the predictive back animation + // TODO provide an intercept opportunity before the animation starts; tie to other intercept method? + if (interceptBackGestureAnimation()) { + backEventFlow.collect { /* Avoid animating, do nothing */ } + } else { + backEventFlow.collect { backEvent -> + backSwipeProgressFlow.value = backEvent.progress + // TODO: update Content() to handle backSwipeProgress + } + } + goBack() + } + public open fun goBack() { if (!atRoot()) { interceptBack { navigate(BACKWARD) { backStack -> backStack - backStack.last() } } + backSwipeProgressFlow.value = 0f } else { throw IllegalStateException("goBack() shouldn't be called with an empty backstack") } diff --git a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposePredictiveBackHandler.kt b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposePredictiveBackHandler.kt index c1e6f777..925a4a6a 100644 --- a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposePredictiveBackHandler.kt +++ b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposePredictiveBackHandler.kt @@ -1,11 +1,13 @@ package com.ryanmoelter.magellanx.compose.navigation -import androidx.activity.compose.BackHandler +import androidx.activity.BackEventCompat +import androidx.activity.compose.PredictiveBackHandler import androidx.compose.runtime.Composable import com.ryanmoelter.magellanx.compose.WhenShown import com.ryanmoelter.magellanx.compose.navigation.BackstackStatus.* import com.ryanmoelter.magellanx.core.Displayable import com.ryanmoelter.magellanx.core.lifecycle.LifecycleAwareComponent +import kotlinx.coroutines.flow.Flow /** * Handle back events proactively, to work with Android's upcoming predictive back system. @@ -16,7 +18,7 @@ import com.ryanmoelter.magellanx.core.lifecycle.LifecycleAwareComponent * @see [Android's upcoming predictive back system](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture) */ public class ComposePredictiveBackHandler( - private val onBackTriggered: () -> Unit + private val backStarted: suspend (Flow) -> Unit ) : LifecycleAwareComponent(), Displayable<@Composable (BackstackStatus) -> Unit> { override val view: @Composable (BackstackStatus) -> Unit @@ -26,9 +28,7 @@ public class ComposePredictiveBackHandler( AT_ROOT -> false BACK_AVAILABLE -> true } - BackHandler(enabled = enabled) { - onBackTriggered() - } + PredictiveBackHandler(enabled = enabled, onBack = backStarted) } } } diff --git a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/transitions/BuiltInTransitionSpecs.kt b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/transitions/BuiltInTransitionSpecs.kt index 67046d08..b61fd150 100644 --- a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/transitions/BuiltInTransitionSpecs.kt +++ b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/transitions/BuiltInTransitionSpecs.kt @@ -48,7 +48,8 @@ public val defaultTransition: MagellanComposeTransition = } Direction.BACKWARD -> { - fadeIn() + scaleIn(initialScale = SCALE_UP_FACTOR) togetherWith + // TODO: make this gesture-back-only + fadeIn(initialAlpha = 1f) togetherWith fadeOut() + scaleOut(targetScale = SCALE_DOWN_FACTOR) } } From 5adaf43bbd7cfe396fc024c207789ee73f968d70 Mon Sep 17 00:00:00 2001 From: Ryan Moelter Date: Sun, 14 Jan 2024 22:47:09 -0800 Subject: [PATCH 4/7] [WIP] Update sample app to work with predictive back API changes --- magellanx-sample-app/src/main/AndroidManifest.xml | 3 +++ .../ryanmoelter/magellanx/doggos/MainActivity.kt | 6 ------ .../magellanx/doggos/game/RatingGameJourney.kt | 7 +++---- .../doggos/randomimages/ChooseBreedStep.kt | 13 ++++++++++--- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/magellanx-sample-app/src/main/AndroidManifest.xml b/magellanx-sample-app/src/main/AndroidManifest.xml index 4ccafc79..701ec40f 100644 --- a/magellanx-sample-app/src/main/AndroidManifest.xml +++ b/magellanx-sample-app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -13,6 +14,8 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.MagellanX" + android:enableOnBackInvokedCallback="true" + tools:targetApi="tiramisu" > - if (backHandled) { - doggoRatings = doggoRatings.dropLast(1) - } + override fun interceptBack(performBack: () -> Unit) { + performBack() + doggoRatings = doggoRatings.dropLast(1) } } diff --git a/magellanx-sample-app/src/main/java/com/ryanmoelter/magellanx/doggos/randomimages/ChooseBreedStep.kt b/magellanx-sample-app/src/main/java/com/ryanmoelter/magellanx/doggos/randomimages/ChooseBreedStep.kt index f652eaa9..709b1525 100644 --- a/magellanx-sample-app/src/main/java/com/ryanmoelter/magellanx/doggos/randomimages/ChooseBreedStep.kt +++ b/magellanx-sample-app/src/main/java/com/ryanmoelter/magellanx/doggos/randomimages/ChooseBreedStep.kt @@ -1,15 +1,20 @@ package com.ryanmoelter.magellanx.doggos.randomimages +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import com.ryanmoelter.magellanx.compose.ComposeStep import com.ryanmoelter.magellanx.doggos.home.ListItem import com.ryanmoelter.magellanx.doggos.injector import com.ryanmoelter.magellanx.doggos.utils.Loadable import com.ryanmoelter.magellanx.doggos.utils.ShowLoadingAround +import com.ryanmoelter.magellanx.doggos.utils.map import com.ryanmoelter.magellanx.doggos.utils.wrapInLoadableFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @@ -18,10 +23,10 @@ class ChooseBreedStep( val chooseBreed: (String) -> Unit ) : ComposeStep() { - val loadableBreedListFlow: MutableStateFlow>> = + private val loadableBreedListFlow: MutableStateFlow>> = MutableStateFlow(Loadable.Loading()) - val doggoApi = injector.doggoApi + private val doggoApi = injector.doggoApi @Composable override fun Content() { @@ -29,9 +34,11 @@ class ChooseBreedStep( ShowLoadingAround(loadable = loadableBreedList) { breeds -> LazyColumn { + item { Spacer(modifier = Modifier.height(12.dp)) } items(breeds, key = { it }) { breed -> - ListItem(title = breed, onClick = { chooseBreed(breed) }) + ListItem(title = breed.replaceFirstChar { it.uppercase() }, onClick = { chooseBreed(breed) }) } + item { Spacer(modifier = Modifier.height(12.dp)) } } } } From 4ba05a81ea8c2696e5f54b7093857d2542eff716 Mon Sep 17 00:00:00 2001 From: Ryan Moelter Date: Sun, 3 Mar 2024 10:14:49 -0800 Subject: [PATCH 5/7] Fix merge conflicts --- .../compose/navigation/ComposeNavigator.kt | 42 ++++++++++--------- .../ComposePredictiveBackHandler.kt | 22 +++++----- .../magellanx/doggos/home/HomeStep.kt | 3 +- 3 files changed, 37 insertions(+), 30 deletions(-) diff --git a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposeNavigator.kt b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposeNavigator.kt index 95a43715..71ac8371 100644 --- a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposeNavigator.kt +++ b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposeNavigator.kt @@ -17,10 +17,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import com.ryanmoelter.magellanx.compose.Content -import com.ryanmoelter.magellanx.compose.WhenShown +import com.ryanmoelter.magellanx.compose.WhenStarted import com.ryanmoelter.magellanx.compose.navigation.BackstackStatus.AT_ROOT import com.ryanmoelter.magellanx.compose.navigation.BackstackStatus.BACK_AVAILABLE -import com.ryanmoelter.magellanx.compose.WhenStarted import com.ryanmoelter.magellanx.compose.navigation.Direction.BACKWARD import com.ryanmoelter.magellanx.compose.navigation.Direction.FORWARD import com.ryanmoelter.magellanx.compose.transitions.MagellanComposeTransition @@ -32,7 +31,6 @@ import com.ryanmoelter.magellanx.core.lifecycle.LifecycleAwareComponent import com.ryanmoelter.magellanx.core.lifecycle.LifecycleLimit import com.ryanmoelter.magellanx.core.lifecycle.LifecycleOwner import com.ryanmoelter.magellanx.core.lifecycle.LifecycleState -import kotlinx.coroutines.flow.Flow import com.ryanmoelter.magellanx.core.lifecycle.attachFieldToLifecycle import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -40,10 +38,11 @@ import kotlinx.coroutines.flow.map public open class ComposeNavigator( private val interceptBack: (performBack: () -> Unit) -> Unit = { }, - private val interceptBackGestureAnimation: () -> Boolean = { false } + private val interceptBackGestureAnimation: () -> Boolean = { false }, ) : LifecycleAwareComponent(), Displayable<@Composable () -> Unit> { - - private val backHandler by attachFieldToLifecycle(ComposePredictiveBackHandler(::handlePredictiveBack)) + private val backHandler by attachFieldToLifecycle( + ComposePredictiveBackHandler(::handlePredictiveBack), + ) /** * The backstack. The last item in each list is the top of the stack. @@ -55,8 +54,9 @@ public open class ComposeNavigator( */ public open val backStack: List get() = backStackFlow.value - public open val backStackStatusFlow: Flow = backStackFlow - .map { if (it.size <= 1) AT_ROOT else BACK_AVAILABLE } + public open val backStackStatusFlow: Flow = + backStackFlow + .map { if (it.size <= 1) AT_ROOT else BACK_AVAILABLE } /** * Get a snapshot of the current navigable, i.e. the last item of the current [backStack]. @@ -67,13 +67,14 @@ public open class ComposeNavigator( private val currentNavigationEventFlow = backStackFlow.map { it.lastOrNull() } public val currentNavigableFlow: Flow Unit>?> = currentNavigationEventFlow.map { it?.navigable } - private val lastNavigableFlow = backStackFlow.map { stack -> - if (stack.size > 1) { - stack[stack.lastIndex - 1].navigable - } else { - null + private val lastNavigableFlow = + backStackFlow.map { stack -> + if (stack.size > 1) { + stack[stack.lastIndex - 1].navigable + } else { + null + } } - } // TODO: make default transition configurable private val transitionFlow: MutableStateFlow = @@ -131,10 +132,12 @@ public open class ComposeNavigator( // The navigable on top of the backstack is Shown if not visible lifecycleRegistry.updateMaxState(navigable, LifecycleLimit.SHOWN) } + backStack.map { it.navigable }.contains(navigable) -> { // Any navigable still in the backstack but not visible is Created lifecycleRegistry.updateMaxState(navigable, LifecycleLimit.CREATED) } + else -> { // Any navigable that's been removed from the backstack should also be removed // from this LifecycleOwner @@ -154,7 +157,7 @@ public open class ComposeNavigator( DisposableEffect(key1 = lastNavigable, key2 = shouldShowPrevious) { val previous = lastNavigable if (previous != null) { - lifecycleRegistry.updateMaxState(previous, LifecycleLimit.SHOWN) + lifecycleRegistry.updateMaxState(previous, LifecycleLimit.STARTED) onDispose { if (children.contains(previous)) { if (backStack.map { it.navigable }.contains(previous)) { @@ -171,10 +174,11 @@ public open class ComposeNavigator( lastNavigable?.Content() } Box( - modifier = Modifier - .fillMaxSize() - .scale(1f - backSwipeProgress), - contentAlignment = Alignment.Center + modifier = + Modifier + .fillMaxSize() + .scale(1f - backSwipeProgress * 0.5f), + contentAlignment = Alignment.Center, ) { navigable?.Content() } diff --git a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposePredictiveBackHandler.kt b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposePredictiveBackHandler.kt index 925a4a6a..cb94d934 100644 --- a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposePredictiveBackHandler.kt +++ b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposePredictiveBackHandler.kt @@ -3,8 +3,9 @@ package com.ryanmoelter.magellanx.compose.navigation import androidx.activity.BackEventCompat import androidx.activity.compose.PredictiveBackHandler import androidx.compose.runtime.Composable -import com.ryanmoelter.magellanx.compose.WhenShown -import com.ryanmoelter.magellanx.compose.navigation.BackstackStatus.* +import com.ryanmoelter.magellanx.compose.WhenStarted +import com.ryanmoelter.magellanx.compose.navigation.BackstackStatus.AT_ROOT +import com.ryanmoelter.magellanx.compose.navigation.BackstackStatus.BACK_AVAILABLE import com.ryanmoelter.magellanx.core.Displayable import com.ryanmoelter.magellanx.core.lifecycle.LifecycleAwareComponent import kotlinx.coroutines.flow.Flow @@ -18,21 +19,22 @@ import kotlinx.coroutines.flow.Flow * @see [Android's upcoming predictive back system](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture) */ public class ComposePredictiveBackHandler( - private val backStarted: suspend (Flow) -> Unit + private val backStarted: suspend (Flow) -> Unit, ) : LifecycleAwareComponent(), Displayable<@Composable (BackstackStatus) -> Unit> { - override val view: @Composable (BackstackStatus) -> Unit get() = { backstackStatus -> - WhenShown { - val enabled = when (backstackStatus) { - AT_ROOT -> false - BACK_AVAILABLE -> true - } + WhenStarted { + val enabled = + when (backstackStatus) { + AT_ROOT -> false + BACK_AVAILABLE -> true + } PredictiveBackHandler(enabled = enabled, onBack = backStarted) } } } public enum class BackstackStatus { - AT_ROOT, BACK_AVAILABLE + AT_ROOT, + BACK_AVAILABLE, } diff --git a/magellanx-sample-app/src/main/java/com/ryanmoelter/magellanx/doggos/home/HomeStep.kt b/magellanx-sample-app/src/main/java/com/ryanmoelter/magellanx/doggos/home/HomeStep.kt index 1900c25a..deb29648 100644 --- a/magellanx-sample-app/src/main/java/com/ryanmoelter/magellanx/doggos/home/HomeStep.kt +++ b/magellanx-sample-app/src/main/java/com/ryanmoelter/magellanx/doggos/home/HomeStep.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowForward import androidx.compose.material.icons.rounded.ArrowForward import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -70,7 +71,7 @@ fun ListItem( ) { Text(title, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f)) Icon( - imageVector = Icons.Rounded.ArrowForward, + imageVector = Icons.AutoMirrored.Rounded.ArrowForward, contentDescription = "Open a new page", tint = MaterialTheme.colorScheme.primary, ) From 39bc605159d1300185c31ef44200694f177692b3 Mon Sep 17 00:00:00 2001 From: Ryan Moelter Date: Sun, 3 Mar 2024 10:57:39 -0800 Subject: [PATCH 6/7] Remove problematic transition animation --- .../magellanx/compose/ComposeExtensions.kt | 68 ++++++++++++++++++- .../magellanx/compose/ComposeStep.kt | 28 ++------ .../compose/navigation/ComposeNavigator.kt | 58 ++-------------- .../ComposePredictiveBackHandler.kt | 21 ++---- .../transitions/BuiltInTransitionSpecs.kt | 3 +- .../doggos/game/RatingGameJourney.kt | 2 +- 6 files changed, 88 insertions(+), 92 deletions(-) diff --git a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeExtensions.kt b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeExtensions.kt index d1a22e4e..66e0d911 100644 --- a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeExtensions.kt +++ b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeExtensions.kt @@ -2,15 +2,24 @@ package com.ryanmoelter.magellanx.compose import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable +import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.ryanmoelter.magellanx.core.Displayable +import com.ryanmoelter.magellanx.core.lifecycle.LifecycleAware import com.ryanmoelter.magellanx.core.lifecycle.LifecycleOwner import com.ryanmoelter.magellanx.core.lifecycle.LifecycleState.Resumed import com.ryanmoelter.magellanx.core.lifecycle.LifecycleState.Started +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext @Composable @Suppress("ktlint:standard:function-naming") @@ -22,7 +31,14 @@ public fun Displayable<@Composable () -> Unit>.Content(modifier: Modifier = Modi @Composable @Suppress("ktlint:standard:function-naming") -public fun Displayable<@Composable (Arg) -> Unit>.Content(arg: Arg, modifier: Modifier = Modifier) { +public fun Displayable< + @Composable ( + Arg, + ) -> Unit, +>.Content( + arg: Arg, + modifier: Modifier = Modifier, +) { Box(modifier = modifier) { view!!(arg) } @@ -62,3 +78,53 @@ public fun LifecycleOwner.WhenResumed(content: @Composable () -> Unit) { content() } } + +/** + * Like [collectAsState], but stops emitting values when this ComposeStep is not Resumed. This is + * primarily useful for preventing values from updating during transitions. + * + * Note that this keeps a subscription to the underlying [Flow] even while not Resumed; any + * emitted values are simply ignored until this ComposeStep is Resumed, at which point the most + * recent value will be emitted before any subsequent values. + */ +@Suppress("StateFlowValueCalledInComposition") +@Composable +public fun StateFlow.collectAsStateWhileResumed( + lifecycleComponent: L, + context: CoroutineContext = EmptyCoroutineContext, +): State where L : LifecycleAware, L : LifecycleOwner = + collectAsStateWhileResumed(value, lifecycleComponent, context) + +/** + * Like [collectAsState], but stops emitting values when this ComposeStep is not Resumed. This is + * primarily useful for preventing values from updating during transitions. + * + * Note that this keeps a subscription to the underlying [Flow] even while not Resumed; any + * emitted values are simply ignored until this ComposeStep is Resumed, at which point the most + * recent value will be emitted before any subsequent values. + */ +@Composable +public fun Flow.collectAsStateWhileResumed( + initial: R, + lifecycleComponent: L, + context: CoroutineContext = EmptyCoroutineContext, +): State where L : LifecycleAware, L : LifecycleOwner = + produceState(initial, this, context) { + if (context == EmptyCoroutineContext) { + combine(lifecycleComponent.currentStateFlow) { it, currentState -> it to currentState } + .collect { (it, currentState) -> + if (currentState == Resumed) { + value = it + } + } + } else { + withContext(context) { + combine(lifecycleComponent.currentStateFlow) { it, currentState -> it to currentState } + .collect { (it, currentState) -> + if (currentState == Resumed) { + value = it + } + } + } + } + } diff --git a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeStep.kt b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeStep.kt index e5fd57fc..48cf9aba 100644 --- a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeStep.kt +++ b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeStep.kt @@ -4,7 +4,6 @@ import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.produceState import com.ryanmoelter.magellanx.core.Displayable import com.ryanmoelter.magellanx.core.Navigable import com.ryanmoelter.magellanx.core.coroutines.CreatedLifecycleScope @@ -12,15 +11,12 @@ import com.ryanmoelter.magellanx.core.coroutines.ResumedLifecycleScope import com.ryanmoelter.magellanx.core.coroutines.ShownLifecycleScope import com.ryanmoelter.magellanx.core.coroutines.StartedLifecycleScope import com.ryanmoelter.magellanx.core.lifecycle.LifecycleAwareComponent -import com.ryanmoelter.magellanx.core.lifecycle.LifecycleState import com.ryanmoelter.magellanx.core.lifecycle.attachFieldToLifecycle import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.withContext public abstract class ComposeStep : ComposeSection(), Navigable<@Composable () -> Unit> @@ -76,23 +72,9 @@ public abstract class ComposeSection : initial: R, context: CoroutineContext = EmptyCoroutineContext, ): State = - produceState(initial, this, context) { - if (context == EmptyCoroutineContext) { - combine(lifecycleRegistry.currentStateFlow) { it, currentState -> it to currentState } - .collect { (it, currentState) -> - if (currentState == LifecycleState.Resumed) { - value = it - } - } - } else { - withContext(context) { - combine(lifecycleRegistry.currentStateFlow) { it, currentState -> it to currentState } - .collect { (it, currentState) -> - if (currentState == LifecycleState.Resumed) { - value = it - } - } - } - } - } + collectAsStateWhileResumed( + initial = initial, + lifecycleComponent = this@ComposeSection, + context = context, + ) } diff --git a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposeNavigator.kt b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposeNavigator.kt index 71ac8371..7058072f 100644 --- a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposeNavigator.kt +++ b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposeNavigator.kt @@ -10,16 +10,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.scale import com.ryanmoelter.magellanx.compose.Content import com.ryanmoelter.magellanx.compose.WhenStarted -import com.ryanmoelter.magellanx.compose.navigation.BackstackStatus.AT_ROOT -import com.ryanmoelter.magellanx.compose.navigation.BackstackStatus.BACK_AVAILABLE +import com.ryanmoelter.magellanx.compose.navigation.BackHandlerStatus.DISABLED +import com.ryanmoelter.magellanx.compose.navigation.BackHandlerStatus.ENABLED import com.ryanmoelter.magellanx.compose.navigation.Direction.BACKWARD import com.ryanmoelter.magellanx.compose.navigation.Direction.FORWARD import com.ryanmoelter.magellanx.compose.transitions.MagellanComposeTransition @@ -37,7 +34,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map public open class ComposeNavigator( - private val interceptBack: (performBack: () -> Unit) -> Unit = { }, + private val interceptBack: (performBack: () -> Unit) -> Unit = { performBack -> performBack() }, private val interceptBackGestureAnimation: () -> Boolean = { false }, ) : LifecycleAwareComponent(), Displayable<@Composable () -> Unit> { private val backHandler by attachFieldToLifecycle( @@ -54,9 +51,9 @@ public open class ComposeNavigator( */ public open val backStack: List get() = backStackFlow.value - public open val backStackStatusFlow: Flow = + public open val backStackStatusFlow: Flow = backStackFlow - .map { if (it.size <= 1) AT_ROOT else BACK_AVAILABLE } + .map { if (it.size <= 1) DISABLED else ENABLED } /** * Get a snapshot of the current navigable, i.e. the last item of the current [backStack]. @@ -67,14 +64,6 @@ public open class ComposeNavigator( private val currentNavigationEventFlow = backStackFlow.map { it.lastOrNull() } public val currentNavigableFlow: Flow Unit>?> = currentNavigationEventFlow.map { it?.navigable } - private val lastNavigableFlow = - backStackFlow.map { stack -> - if (stack.size > 1) { - stack[stack.lastIndex - 1].navigable - } else { - null - } - } // TODO: make default transition configurable private val transitionFlow: MutableStateFlow = @@ -95,11 +84,9 @@ public open class ComposeNavigator( private fun Content() { val backStack by backStackFlow.collectAsState() val currentNavigable by currentNavigableFlow.collectAsState(null) - val lastNavigable by lastNavigableFlow.collectAsState(null) val currentTransitionSpec by transitionFlow.collectAsState() val currentDirection by directionFlow.collectAsState() - val backstackStatus by backStackStatusFlow.collectAsState(initial = AT_ROOT) - val backSwipeProgress by backSwipeProgressFlow.collectAsState() + val backstackStatus by backStackStatusFlow.collectAsState(initial = DISABLED) backHandler.Content(backstackStatus) @@ -150,38 +137,7 @@ public open class ComposeNavigator( ) } Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - val shouldShowPrevious by remember { - derivedStateOf { backSwipeProgress != 0f } - } - if (shouldShowPrevious) { - DisposableEffect(key1 = lastNavigable, key2 = shouldShowPrevious) { - val previous = lastNavigable - if (previous != null) { - lifecycleRegistry.updateMaxState(previous, LifecycleLimit.STARTED) - onDispose { - if (children.contains(previous)) { - if (backStack.map { it.navigable }.contains(previous)) { - lifecycleRegistry.updateMaxState(previous, LifecycleLimit.CREATED) - } else { - removeFromLifecycle(previous) - } - } - } - } else { - onDispose { } - } - } - lastNavigable?.Content() - } - Box( - modifier = - Modifier - .fillMaxSize() - .scale(1f - backSwipeProgress * 0.5f), - contentAlignment = Alignment.Center, - ) { - navigable?.Content() - } + navigable?.Content() } } } diff --git a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposePredictiveBackHandler.kt b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposePredictiveBackHandler.kt index cb94d934..f4b98510 100644 --- a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposePredictiveBackHandler.kt +++ b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/navigation/ComposePredictiveBackHandler.kt @@ -4,8 +4,6 @@ import androidx.activity.BackEventCompat import androidx.activity.compose.PredictiveBackHandler import androidx.compose.runtime.Composable import com.ryanmoelter.magellanx.compose.WhenStarted -import com.ryanmoelter.magellanx.compose.navigation.BackstackStatus.AT_ROOT -import com.ryanmoelter.magellanx.compose.navigation.BackstackStatus.BACK_AVAILABLE import com.ryanmoelter.magellanx.core.Displayable import com.ryanmoelter.magellanx.core.lifecycle.LifecycleAwareComponent import kotlinx.coroutines.flow.Flow @@ -20,21 +18,16 @@ import kotlinx.coroutines.flow.Flow */ public class ComposePredictiveBackHandler( private val backStarted: suspend (Flow) -> Unit, -) : LifecycleAwareComponent(), Displayable<@Composable (BackstackStatus) -> Unit> { - override val view: @Composable (BackstackStatus) -> Unit - get() = { backstackStatus -> +) : LifecycleAwareComponent(), Displayable<@Composable (BackHandlerStatus) -> Unit> { + override val view: @Composable (BackHandlerStatus) -> Unit + get() = { backHandlerStatus -> WhenStarted { - val enabled = - when (backstackStatus) { - AT_ROOT -> false - BACK_AVAILABLE -> true - } - PredictiveBackHandler(enabled = enabled, onBack = backStarted) + PredictiveBackHandler(enabled = backHandlerStatus.enabled, onBack = backStarted) } } } -public enum class BackstackStatus { - AT_ROOT, - BACK_AVAILABLE, +public enum class BackHandlerStatus(public val enabled: Boolean) { + ENABLED(true), + DISABLED(false), } diff --git a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/transitions/BuiltInTransitionSpecs.kt b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/transitions/BuiltInTransitionSpecs.kt index b61fd150..67046d08 100644 --- a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/transitions/BuiltInTransitionSpecs.kt +++ b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/transitions/BuiltInTransitionSpecs.kt @@ -48,8 +48,7 @@ public val defaultTransition: MagellanComposeTransition = } Direction.BACKWARD -> { - // TODO: make this gesture-back-only - fadeIn(initialAlpha = 1f) togetherWith + fadeIn() + scaleIn(initialScale = SCALE_UP_FACTOR) togetherWith fadeOut() + scaleOut(targetScale = SCALE_DOWN_FACTOR) } } diff --git a/magellanx-sample-app/src/main/java/com/ryanmoelter/magellanx/doggos/game/RatingGameJourney.kt b/magellanx-sample-app/src/main/java/com/ryanmoelter/magellanx/doggos/game/RatingGameJourney.kt index 96ce69b0..59ce6ad8 100644 --- a/magellanx-sample-app/src/main/java/com/ryanmoelter/magellanx/doggos/game/RatingGameJourney.kt +++ b/magellanx-sample-app/src/main/java/com/ryanmoelter/magellanx/doggos/game/RatingGameJourney.kt @@ -71,8 +71,8 @@ class RatingGameJourney( } override fun interceptBack(performBack: () -> Unit) { - performBack() doggoRatings = doggoRatings.dropLast(1) + performBack() } } From b116a5a23aefb0fc4d7270a543ddb24c8b4bc6dd Mon Sep 17 00:00:00 2001 From: Ryan Moelter Date: Thu, 7 Mar 2024 12:36:02 -0800 Subject: [PATCH 7/7] Lint --- .../java/com/ryanmoelter/magellanx/compose/ComposeJourney.kt | 3 +-- .../ryanmoelter/magellanx/core/lifecycle/LifecycleRegistry.kt | 1 - .../magellanx/doggos/randomimages/ChooseBreedStep.kt | 2 -- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeJourney.kt b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeJourney.kt index 0f53c00c..f7ff29c2 100644 --- a/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeJourney.kt +++ b/magellanx-compose/src/main/java/com/ryanmoelter/magellanx/compose/ComposeJourney.kt @@ -5,9 +5,8 @@ import com.ryanmoelter.magellanx.compose.navigation.ComposeNavigator import com.ryanmoelter.magellanx.core.lifecycle.attachFieldToLifecycle public abstract class ComposeJourney : ComposeStep() { - protected var navigator: ComposeNavigator by attachFieldToLifecycle( - ComposeNavigator(::interceptBack, ::interceptBackGestureAnimation) + ComposeNavigator(::interceptBack, ::interceptBackGestureAnimation), ) @Composable diff --git a/magellanx-core/src/main/java/com/ryanmoelter/magellanx/core/lifecycle/LifecycleRegistry.kt b/magellanx-core/src/main/java/com/ryanmoelter/magellanx/core/lifecycle/LifecycleRegistry.kt index 95d73fcb..ff7c27f5 100644 --- a/magellanx-core/src/main/java/com/ryanmoelter/magellanx/core/lifecycle/LifecycleRegistry.kt +++ b/magellanx-core/src/main/java/com/ryanmoelter/magellanx/core/lifecycle/LifecycleRegistry.kt @@ -1,7 +1,6 @@ package com.ryanmoelter.magellanx.core.lifecycle import com.ryanmoelter.magellanx.core.lifecycle.LifecycleLimit.NO_LIMIT -import com.ryanmoelter.magellanx.core.lifecycle.LifecycleLimit.SHOWN import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/magellanx-sample-app/src/main/java/com/ryanmoelter/magellanx/doggos/randomimages/ChooseBreedStep.kt b/magellanx-sample-app/src/main/java/com/ryanmoelter/magellanx/doggos/randomimages/ChooseBreedStep.kt index 6deeb055..adeee4bc 100644 --- a/magellanx-sample-app/src/main/java/com/ryanmoelter/magellanx/doggos/randomimages/ChooseBreedStep.kt +++ b/magellanx-sample-app/src/main/java/com/ryanmoelter/magellanx/doggos/randomimages/ChooseBreedStep.kt @@ -14,7 +14,6 @@ import com.ryanmoelter.magellanx.doggos.home.ListItem import com.ryanmoelter.magellanx.doggos.injector import com.ryanmoelter.magellanx.doggos.utils.Loadable import com.ryanmoelter.magellanx.doggos.utils.ShowLoadingAround -import com.ryanmoelter.magellanx.doggos.utils.map import com.ryanmoelter.magellanx.doggos.utils.wrapInLoadableFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @@ -22,7 +21,6 @@ import kotlinx.coroutines.launch class ChooseBreedStep( val chooseBreed: (String) -> Unit, ) : ComposeStep() { - private val loadableBreedListFlow: MutableStateFlow>> = MutableStateFlow(Loadable.Loading())