Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ final val com.arkivanov.decompose.extensions.compose.experimental.panels/com_ark
final val com.arkivanov.decompose.extensions.compose.experimental.stack.animation/LocalStackAnimationProvider // com.arkivanov.decompose.extensions.compose.experimental.stack.animation/LocalStackAnimationProvider|{}LocalStackAnimationProvider[0]
final fun <get-LocalStackAnimationProvider>(): androidx.compose.runtime/ProvidableCompositionLocal<com.arkivanov.decompose.extensions.compose.experimental.stack.animation/StackAnimationProvider> // com.arkivanov.decompose.extensions.compose.experimental.stack.animation/LocalStackAnimationProvider.<get-LocalStackAnimationProvider>|<get-LocalStackAnimationProvider>(){}[0]
final val com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation$stableprop // com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation$stableprop|#static{}com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation$stableprop[0]
final val com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Finishing$stableprop // com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Finishing$stableprop|#static{}com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Finishing$stableprop[0]
final val com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Idle$stableprop // com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Idle$stableprop|#static{}com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Idle$stableprop[0]
final val com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Progress$stableprop // com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Progress$stableprop|#static{}com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Progress$stableprop[0]
final val com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Started$stableprop // com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Started$stableprop|#static{}com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Started$stableprop[0]
final val com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimator$stableprop // com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimator$stableprop|#static{}com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimator$stableprop[0]
final val com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_PredictiveBackParams$stableprop // com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_PredictiveBackParams$stableprop|#static{}com_arkivanov_decompose_extensions_compose_experimental_stack_animation_PredictiveBackParams$stableprop[0]
final val com.arkivanov.decompose.extensions.compose.experimental/com_arkivanov_decompose_extensions_compose_experimental_BroadcastBackHandler$stableprop // com.arkivanov.decompose.extensions.compose.experimental/com_arkivanov_decompose_extensions_compose_experimental_BroadcastBackHandler$stableprop|#static{}com_arkivanov_decompose_extensions_compose_experimental_BroadcastBackHandler$stableprop[0]
Expand All @@ -78,6 +82,10 @@ final fun <#A: kotlin/Any, #B: kotlin/Any> com.arkivanov.decompose.extensions.co
final fun com.arkivanov.decompose.extensions.compose.experimental.panels/com_arkivanov_decompose_extensions_compose_experimental_panels_ChildPanelsAnimators$stableprop_getter(): kotlin/Int // com.arkivanov.decompose.extensions.compose.experimental.panels/com_arkivanov_decompose_extensions_compose_experimental_panels_ChildPanelsAnimators$stableprop_getter|com_arkivanov_decompose_extensions_compose_experimental_panels_ChildPanelsAnimators$stableprop_getter(){}[0]
final fun com.arkivanov.decompose.extensions.compose.experimental.panels/com_arkivanov_decompose_extensions_compose_experimental_panels_HorizontalChildPanelsLayout$stableprop_getter(): kotlin/Int // com.arkivanov.decompose.extensions.compose.experimental.panels/com_arkivanov_decompose_extensions_compose_experimental_panels_HorizontalChildPanelsLayout$stableprop_getter|com_arkivanov_decompose_extensions_compose_experimental_panels_HorizontalChildPanelsLayout$stableprop_getter(){}[0]
final fun com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation$stableprop_getter(): kotlin/Int // com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation$stableprop_getter|com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation$stableprop_getter(){}[0]
final fun com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Finishing$stableprop_getter(): kotlin/Int // com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Finishing$stableprop_getter|com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Finishing$stableprop_getter(){}[0]
final fun com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Idle$stableprop_getter(): kotlin/Int // com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Idle$stableprop_getter|com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Idle$stableprop_getter(){}[0]
final fun com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Progress$stableprop_getter(): kotlin/Int // com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Progress$stableprop_getter|com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Progress$stableprop_getter(){}[0]
final fun com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Started$stableprop_getter(): kotlin/Int // com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Started$stableprop_getter|com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimation_State_Started$stableprop_getter(){}[0]
final fun com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimator$stableprop_getter(): kotlin/Int // com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimator$stableprop_getter|com_arkivanov_decompose_extensions_compose_experimental_stack_animation_DefaultStackAnimator$stableprop_getter(){}[0]
final fun com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_PredictiveBackParams$stableprop_getter(): kotlin/Int // com.arkivanov.decompose.extensions.compose.experimental.stack.animation/com_arkivanov_decompose_extensions_compose_experimental_stack_animation_PredictiveBackParams$stableprop_getter|com_arkivanov_decompose_extensions_compose_experimental_stack_animation_PredictiveBackParams$stableprop_getter(){}[0]
final fun com.arkivanov.decompose.extensions.compose.experimental.stack.animation/fade(androidx.compose.animation.core/FiniteAnimationSpec<kotlin/Float> = ..., kotlin/Float = ...): com.arkivanov.decompose.extensions.compose.experimental.stack.animation/StackAnimator // com.arkivanov.decompose.extensions.compose.experimental.stack.animation/fade|fade(androidx.compose.animation.core.FiniteAnimationSpec<kotlin.Float>;kotlin.Float){}[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ internal class DefaultStackAnimation<C : Any, T : Any>(
) {
var currentStack by remember { mutableStateOf(stack) }
var items by remember { mutableStateOf(getAnimationItems(newStack = currentStack)) }
var nextItems: Map<Any, AnimationItem<C, T>>? by remember { mutableStateOf(null) }
var nextItems: Map<String, AnimationItem<C, T>>? by remember { mutableStateOf(null) }
val stackKeys = remember(stack) { stack.items.map { it.key } }
val currentStackKeys = remember(currentStack) { currentStack.items.map { it.key } }

Expand Down Expand Up @@ -150,7 +150,7 @@ internal class DefaultStackAnimation<C : Any, T : Any>(
}
}

private fun getAnimationItems(newStack: ChildStack<C, T>, oldStack: ChildStack<C, T>? = null): Map<Any, AnimationItem<C, T>> =
private fun getAnimationItems(newStack: ChildStack<C, T>, oldStack: ChildStack<C, T>? = null): Map<String, AnimationItem<C, T>> =
when {
(oldStack == null) || (newStack.active.key == oldStack.active.key) ->
keyedItemsOf(
Expand Down Expand Up @@ -199,7 +199,7 @@ internal class DefaultStackAnimation<C : Any, T : Any>(
private fun PredictiveBackController(
stack: ChildStack<C, T>,
predictiveBackParams: PredictiveBackParams,
setItems: (Map<Any, AnimationItem<C, T>>) -> Unit,
setItems: (Map<String, AnimationItem<C, T>>) -> Unit,
) {
val scope = rememberCoroutineScope()

Expand Down Expand Up @@ -242,29 +242,30 @@ internal class DefaultStackAnimation<C : Any, T : Any>(
private val stack: ChildStack<C, T>,
private val scope: CoroutineScope,
private val predictiveBackParams: PredictiveBackParams,
private val setItems: (Map<Any, AnimationItem<C, T>>) -> Unit,
private val setItems: (Map<String, AnimationItem<C, T>>) -> Unit,
) : BackCallback() {
private var animationHandler: AnimationHandler? = null
private var initialBackEvent: BackEvent? = null
private var state: State = State.Idle

override fun onBackStarted(backEvent: BackEvent) {
initialBackEvent = backEvent
if (state is State.Idle) {
state = State.Started(backEvent)
}
}

override fun onBackProgressed(backEvent: BackEvent) {
startIfNeeded()
val currentState = state as? State.Progress ?: return

scope.launch {
animationHandler?.progress(backEvent)
currentState.animationHandler.progress(backEvent)
}
}

private fun startIfNeeded() {
val backEvent = initialBackEvent ?: return
initialBackEvent = null

val currentState = state as? State.Started ?: return
val backEvent = currentState.initialBackEvent
val animationHandler = AnimationHandler(animatable = predictiveBackParams.animatable(backEvent))
this.animationHandler = animationHandler
state = State.Progress(animationHandler)
val exitChild = stack.active
val enterChild = stack.backStack.last()

Expand Down Expand Up @@ -295,32 +296,45 @@ internal class DefaultStackAnimation<C : Any, T : Any>(
}

override fun onBackCancelled() {
initialBackEvent = null
val currentState = state
if (currentState is State.Progress) {
state = State.Finishing

scope.launch {
animationHandler?.also { handler ->
handler.cancel()
animationHandler = null
scope.launch {
currentState.animationHandler.cancel()
state = State.Idle
setItems(getAnimationItems(newStack = stack))
}
} else if (currentState !is State.Finishing) {
state = State.Idle
}
}

override fun onBack() {
initialBackEvent = null
val currentState = state
if (currentState is State.Progress) {
state = State.Finishing

scope.launch {
animationHandler?.also { handler ->
handler.finish()
animationHandler = null
scope.launch {
currentState.animationHandler.finish()
state = State.Idle
setItems(getAnimationItems(newStack = stack.dropLast()))
predictiveBackParams.onBack()
}

} else if (currentState !is State.Finishing) {
state = State.Idle
predictiveBackParams.onBack()
}
Comment thread
arkivanov marked this conversation as resolved.
}
}

private sealed interface State {
data object Idle : State
data class Started(val initialBackEvent: BackEvent) : State
data class Progress(val animationHandler: AnimationHandler) : State
data object Finishing : State
}

private class AnimationHandler(
val animatable: PredictiveBackAnimatable?,
) {
Expand Down Expand Up @@ -380,7 +394,7 @@ private data class AnimationItem<out C : Any, out T : Any>(
)

@ExperimentalDecomposeApi
private fun <C : Any, T : Any> keyedItemsOf(vararg items: AnimationItem<C, T>): Map<Any, AnimationItem<C, T>> =
private fun <C : Any, T : Any> keyedItemsOf(vararg items: AnimationItem<C, T>): Map<String, AnimationItem<C, T>> =
items.associateBy { it.child.key }

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.arkivanov.decompose.extensions.compose.stack.animation.predictiveback
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.essenty.backhandler.BackDispatcher
import com.arkivanov.essenty.backhandler.BackEvent
import kotlinx.coroutines.suspendCancellableCoroutine
import org.junit.Rule
import kotlin.test.Test
import kotlin.test.assertEquals
Expand Down Expand Up @@ -216,7 +217,7 @@ class PredictiveBackGestureTest {
@Test
fun GIVEN_gesture_started_WHEN_stack_popped_THEN_gesture_cancelled() {
var stack by mutableStateOf(stack("1", "2"))
val animation = DefaultStackAnimation(animator = fade(), onBack = { stack = stack.dropLast() },)
val animation = DefaultStackAnimation(animator = fade(), onBack = { stack = stack.dropLast() })

composeRule.setContent {
animation(stack, Modifier) {
Expand Down Expand Up @@ -492,6 +493,88 @@ class PredictiveBackGestureTest {
assertEquals(0.7F, values["2"])
}

@Test
fun GIVEN_gesture_finishing_WHEN_new_gesture_started_THEN_new_animation_not_started() {
var stack by mutableStateOf(stack("1", "2"))
var animationCount = 0

val animation =
DefaultStackAnimation(
predictiveBackAnimatable = {
animationCount++

TestAnimatable(
initialBackEvent = it,
finish = {
suspendCancellableCoroutine {
// Simulate a long-running animation
}
},
)
},
onBack = { stack = stack.dropLast() },
)

composeRule.setContent {
animation(stack, Modifier) {
Text(text = it.configuration)
}
}

backDispatcher.startPredictiveBack(BackEvent(progress = 0F))
composeRule.waitForIdle()
backDispatcher.progressPredictiveBack(BackEvent(progress = 0.5F))
composeRule.waitForIdle()
backDispatcher.back()
composeRule.waitForIdle()
backDispatcher.startPredictiveBack(BackEvent(progress = 0F))
composeRule.waitForIdle()
backDispatcher.progressPredictiveBack(BackEvent(progress = 0.5F))
composeRule.waitForIdle()

assertEquals(1, animationCount)
}

@Test
fun GIVEN_gesture_finishing_WHEN_back_THEN_stack_not_popped() {
var stack by mutableStateOf(stack("1", "2"))
var animationCount = 0

val animation =
DefaultStackAnimation(
predictiveBackAnimatable = {
animationCount++

TestAnimatable(
initialBackEvent = it,
finish = {
suspendCancellableCoroutine {
// Simulate a long-running animation
}
},
)
},
onBack = { stack = stack.dropLast() },
)

composeRule.setContent {
animation(stack, Modifier) {
Text(text = it.configuration)
}
}

backDispatcher.startPredictiveBack(BackEvent(progress = 0F))
composeRule.waitForIdle()
backDispatcher.progressPredictiveBack(BackEvent(progress = 0.5F))
composeRule.waitForIdle()
backDispatcher.back()
composeRule.waitForIdle()
backDispatcher.back()
composeRule.waitForIdle()

assertEquals(stack("1", "2"), stack)
}

private fun DefaultStackAnimation(
predictiveBackAnimatable: (initialBackEvent: BackEvent) -> PredictiveBackAnimatable? = ::TestAnimatable,
animator: StackAnimator? = null,
Expand Down Expand Up @@ -538,6 +621,7 @@ class PredictiveBackGestureTest {

private class TestAnimatable(
initialBackEvent: BackEvent,
private val finish: suspend () -> Unit = {},
) : PredictiveBackAnimatable {
private var progress by mutableStateOf(initialBackEvent.progress)

Expand All @@ -550,6 +634,7 @@ class PredictiveBackGestureTest {

override suspend fun finish() {
progress = 1F
finish.invoke()
}

override suspend fun cancel() {
Expand Down
Loading