commonMain.com.arkivanov.decompose.extensions.compose.stack.animation.AbstractStackAnimation.kt Maven / Gradle / Ivy
package com.arkivanov.decompose.extensions.compose.stack.animation
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.Child
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.utils.InputConsumingOverlay
import com.arkivanov.decompose.router.stack.ChildStack
@OptIn(ExperimentalDecomposeApi::class)
internal abstract class AbstractStackAnimation(
private val disableInputDuringAnimation: Boolean,
) : StackAnimation {
@Composable
protected abstract fun Child(
item: AnimationItem,
onFinished: () -> Unit,
content: @Composable (child: Child.Created) -> Unit,
)
@Composable
override operator fun invoke(stack: ChildStack, modifier: Modifier, content: @Composable (child: Child.Created) -> Unit) {
var currentStack by remember { mutableStateOf(stack) }
var items by remember { mutableStateOf(getAnimationItems(newStack = currentStack, oldStack = null)) }
if (stack.active.key != currentStack.active.key) {
val oldStack = currentStack
currentStack = stack
items = getAnimationItems(newStack = currentStack, oldStack = oldStack)
}
Box(modifier = modifier) {
items.forEach { (key, item) ->
key(key) {
Child(
item = item,
onFinished = {
if (item.direction.isExit) {
items -= key
} else {
items += (key to item.copy(otherChild = null))
}
},
content = content,
)
}
}
// A workaround until https://issuetracker.google.com/issues/214231672.
// Normally only the exiting child should be disabled.
if (disableInputDuringAnimation && (items.size > 1)) {
InputConsumingOverlay(modifier = Modifier.matchParentSize())
}
}
}
private fun getAnimationItems(newStack: ChildStack, oldStack: ChildStack?): Map> =
when {
oldStack == null ->
listOf(AnimationItem(child = newStack.active, direction = Direction.ENTER_FRONT, isInitial = true))
(newStack.size < oldStack.size) && (newStack.active.key in oldStack.backStack) ->
listOf(
AnimationItem(child = newStack.active, direction = Direction.ENTER_BACK, otherChild = oldStack.active),
AnimationItem(child = oldStack.active, direction = Direction.EXIT_FRONT, otherChild = newStack.active),
)
else ->
listOf(
AnimationItem(child = oldStack.active, direction = Direction.EXIT_BACK, otherChild = newStack.active),
AnimationItem(child = newStack.active, direction = Direction.ENTER_FRONT, otherChild = oldStack.active),
)
}.associateBy { it.child.key }
private val ChildStack<*, *>.size: Int
get() = items.size
private operator fun Iterable>.contains(key: Any): Boolean =
any { it.key == key }
protected data class AnimationItem(
val child: Child.Created,
val direction: Direction,
val isInitial: Boolean = false,
val otherChild: Child.Created? = null,
)
}