All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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,
    )
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy