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

net.peanuuutz.fork.ui.foundation.animation.EnterExitEffect.kt Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2020 The Android Open Source Project
 * Modifications Copyright 2022 Peanuuutz
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.peanuuutz.fork.ui.foundation.animation

import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import net.peanuuutz.fork.ui.animation.Transition
import net.peanuuutz.fork.ui.animation.animateFloat
import net.peanuuutz.fork.ui.animation.createDeferredAnimation
import net.peanuuutz.fork.ui.animation.spec.target.composite.FiniteAnimationSpec
import net.peanuuutz.fork.ui.animation.spec.target.composite.SnapSpec
import net.peanuuutz.fork.ui.animation.spec.target.composite.SpringPhysics
import net.peanuuutz.fork.ui.animation.spec.target.composite.SpringSpec
import net.peanuuutz.fork.ui.animation.spec.target.primitive.FloatAnimationSpec
import net.peanuuutz.fork.ui.animation.spec.target.primitive.NoopFloatAnimationSpec
import net.peanuuutz.fork.ui.animation.spec.target.vectorized.VectorizedAnimationSpec
import net.peanuuutz.fork.ui.animation.spec.target.vectorized.VectorizedDimensionIndependentAnimationSpec
import net.peanuuutz.fork.ui.animation.spec.target.vectorized.VectorizedFiniteAnimationSpec
import net.peanuuutz.fork.ui.animation.vector.AnimationVector
import net.peanuuutz.fork.ui.animation.vector.AnimationVector1D
import net.peanuuutz.fork.ui.animation.vector.AnimationVector2D
import net.peanuuutz.fork.ui.animation.vector.VectorConvertor
import net.peanuuutz.fork.ui.foundation.animation.EnterExitEffect.Builder
import net.peanuuutz.fork.ui.foundation.animation.EnterExitEffect.Element
import net.peanuuutz.fork.ui.foundation.animation.EnterExitEffect.Type
import net.peanuuutz.fork.ui.foundation.animation.EnterExitState.PostExit
import net.peanuuutz.fork.ui.foundation.animation.EnterExitState.PreEnter
import net.peanuuutz.fork.ui.foundation.animation.EnterExitState.Visible
import net.peanuuutz.fork.ui.inspection.InspectInfo
import net.peanuuutz.fork.ui.ui.layout.Alignment
import net.peanuuutz.fork.ui.ui.layout.Constraints
import net.peanuuutz.fork.ui.ui.layout.LayoutOrientation
import net.peanuuutz.fork.ui.ui.layout.LayoutOrientation.Horizontal
import net.peanuuutz.fork.ui.ui.layout.LayoutOrientation.Vertical
import net.peanuuutz.fork.ui.ui.layout.Measurable
import net.peanuuutz.fork.ui.ui.layout.MeasureResult
import net.peanuuutz.fork.ui.ui.modifier.Modifier
import net.peanuuutz.fork.ui.ui.modifier.ModifierNodeElement
import net.peanuuutz.fork.ui.ui.modifier.composed
import net.peanuuutz.fork.ui.ui.modifier.draw.graphicsLayer
import net.peanuuutz.fork.ui.ui.node.LayoutModifierNode
import net.peanuuutz.fork.ui.ui.node.ModifierNode
import net.peanuuutz.fork.ui.ui.unit.IntOffset
import net.peanuuutz.fork.ui.ui.unit.IntSize

@Stable
sealed interface EnterExitEffect {
    fun  fold(
        initial: R,
        operation: (acc: R, element: Element<*>) -> R
    ): R

    operator fun > get(type: Type): E?

    operator fun plus(other: EnterExitEffect): EnterExitEffect {
        if (other === EnterExitEffect) {
            return this
        }
        return other.fold(this) { acc, element ->
            val type = element.type
            val existing = acc[type]
            if (existing == null) {
                CombinedEnterExitEffect(acc, element)
            } else {
                val merged = mergeUnsafe(existing, element)
                val remaining = acc - type
                if (remaining === EnterExitEffect) {
                    merged
                } else {
                    CombinedEnterExitEffect(remaining, merged)
                }
            }
        }
    }

    operator fun minus(type: Type<*>): EnterExitEffect

    @Stable
    interface Type>

    @Stable
    interface Element> : EnterExitEffect {
        val type: Type

        fun Modifier.transition(transition: Transition): Modifier

        infix fun merge(new: E): E {
            return new
        }

        override fun  fold(initial: R, operation: (R, Element<*>) -> R): R {
            return operation(initial, this)
        }

        override fun > get(type: Type): E? {
            return if (this.type == type) {
                @Suppress("UNCHECKED_CAST")
                this as E
            } else {
                null
            }
        }

        override fun minus(type: Type<*>): EnterExitEffect {
            return if (this.type == type) EnterExitEffect else this
        }
    }

    class Builder {
        fun append(effect: EnterExitEffect) {
            current += effect
        }

        fun remove(type: Type<*>) {
            current -= type
        }

        fun build(): EnterExitEffect {
            return current
        }

        // ======== Internal ========

        private var current: EnterExitEffect = EnterExitEffect
    }

    @Immutable
    companion object : EnterExitEffect {
        override fun  fold(initial: R, operation: (R, Element<*>) -> R): R {
            return initial
        }

        override fun > get(type: Type): E? {
            return null
        }

        override fun plus(other: EnterExitEffect): EnterExitEffect {
            return other
        }

        override fun minus(type: Type<*>): EnterExitEffect {
            return EnterExitEffect
        }

        override fun toString(): String {
            return "EnterExitEffect"
        }
    }
}

inline fun EnterExitEffect(
    block: Builder.() -> Unit
): EnterExitEffect {
    return Builder().apply(block).build()
}

context(Builder)
operator fun EnterExitEffect.unaryPlus() {
    append(this)
}

context(Builder)
operator fun Type<*>.unaryMinus() {
    remove(this)
}

// -------- Presets --------

@Stable
val Fade: Type<*> = FadeEffect

@Stable
val Slide: Type<*> = SlideEffect

@Stable
val ExpandShrink: Type<*> = ExpandShrinkEffect

@Stable
fun Fade(
    animationSpec: FiniteAnimationSpec = DefaultEffectFloatAnimationSpec,
    alphaOnAbsent: Float = 0.0f
): EnterExitEffect {
    val data = FadeEffectData(
        animationSpec = animationSpec,
        alphaOnAbsent = alphaOnAbsent
    )
    return FadeEffect(
        enter = data,
        exit = data
    )
}

@Stable
fun FadeIn(
    animationSpec: FiniteAnimationSpec = DefaultEffectFloatAnimationSpec,
    startAlpha: Float = 0.0f
): EnterExitEffect {
    val data = FadeEffectData(
        animationSpec = animationSpec,
        alphaOnAbsent = startAlpha
    )
    return FadeEffect(
        enter = data,
        exit = null
    )
}

@Stable
fun FadeOut(
    animationSpec: FiniteAnimationSpec = DefaultEffectFloatAnimationSpec,
    endAlpha: Float = 0.0f
): EnterExitEffect {
    val data = FadeEffectData(
        animationSpec = animationSpec,
        alphaOnAbsent = endAlpha
    )
    return FadeEffect(
        enter = null,
        exit = data
    )
}

@Stable
fun Slide(
    animationSpec: FiniteAnimationSpec = DefaultEffectIntOffsetAnimationSpec,
    offsetOnAbsent: (size: IntSize) -> IntOffset
): EnterExitEffect {
    val data = SlideEffectData(
        animationSpec = animationSpec,
        offsetOnAbsent = offsetOnAbsent
    )
    return SlideEffect(
        enter = data,
        exit = data
    )
}

@Stable
fun SlideIn(
    animationSpec: FiniteAnimationSpec = DefaultEffectIntOffsetAnimationSpec,
    startOffset: (size: IntSize) -> IntOffset
): EnterExitEffect {
    val data = SlideEffectData(
        animationSpec = animationSpec,
        offsetOnAbsent = startOffset
    )
    return SlideEffect(
        enter = data,
        exit = null
    )
}

@Stable
fun SlideOut(
    animationSpec: FiniteAnimationSpec = DefaultEffectIntOffsetAnimationSpec,
    endOffset: (size: IntSize) -> IntOffset
): EnterExitEffect {
    val data = SlideEffectData(
        animationSpec = animationSpec,
        offsetOnAbsent = endOffset
    )
    return SlideEffect(
        enter = null,
        exit = data
    )
}

@Stable
fun Slide(
    orientation: LayoutOrientation,
    animationSpec: FiniteAnimationSpec = DefaultEffectIntAnimationSpec,
    offsetOnAbsent: (size: Int) -> Int
): EnterExitEffect {
    val data = SlideEffectData(
        orientation = orientation,
        animationSpec = animationSpec,
        axisOffsetOnAbsent = offsetOnAbsent
    )
    return SlideEffect(
        enter = data,
        exit = data
    )
}

@Stable
fun SlideIn(
    orientation: LayoutOrientation,
    animationSpec: FiniteAnimationSpec = DefaultEffectIntAnimationSpec,
    startOffset: (size: Int) -> Int
): EnterExitEffect {
    val data = SlideEffectData(
        orientation = orientation,
        animationSpec = animationSpec,
        axisOffsetOnAbsent = startOffset
    )
    return SlideEffect(
        enter = data,
        exit = null
    )
}

@Stable
fun SlideOut(
    orientation: LayoutOrientation,
    animationSpec: FiniteAnimationSpec = DefaultEffectIntAnimationSpec,
    endOffset: (size: Int) -> Int
): EnterExitEffect {
    val data = SlideEffectData(
        orientation = orientation,
        animationSpec = animationSpec,
        axisOffsetOnAbsent = endOffset
    )
    return SlideEffect(
        enter = null,
        exit = data
    )
}

@Stable
fun ExpandShrink(
    alignmentOnAbsent: Alignment,
    animationSpec: FiniteAnimationSpec = DefaultEffectIntSizeAnimationSpec,
    sizeOnAbsent: (fullSize: IntSize) -> IntSize = { IntSize.Zero }
): EnterExitEffect {
    val data = ExpandShrinkEffectData(
        alignmentOnAbsent = alignmentOnAbsent,
        animationSpec = animationSpec,
        sizeOnAbsent = sizeOnAbsent
    )
    return ExpandShrinkEffect(
        enter = data,
        exit = data
    )
}

@Stable
fun ExpandIn(
    expandFrom: Alignment,
    animationSpec: FiniteAnimationSpec = DefaultEffectIntSizeAnimationSpec,
    startSize: (fullSize: IntSize) -> IntSize = { IntSize.Zero }
): EnterExitEffect {
    val data = ExpandShrinkEffectData(
        alignmentOnAbsent = expandFrom,
        animationSpec = animationSpec,
        sizeOnAbsent = startSize
    )
    return ExpandShrinkEffect(
        enter = data,
        exit = null
    )
}

@Stable
fun ShrinkOut(
    shrinkTo: Alignment,
    animationSpec: FiniteAnimationSpec = DefaultEffectIntSizeAnimationSpec,
    endSize: (fullSize: IntSize) -> IntSize = { IntSize.Zero }
): EnterExitEffect {
    val data = ExpandShrinkEffectData(
        alignmentOnAbsent = shrinkTo,
        animationSpec = animationSpec,
        sizeOnAbsent = endSize
    )
    return ExpandShrinkEffect(
        enter = null,
        exit = data
    )
}

@Stable
fun ExpandShrink(
    alignmentOnAbsent: Alignment.Horizontal,
    animationSpec: FiniteAnimationSpec = DefaultEffectIntAnimationSpec,
    sizeOnAbsent: (fullSize: Int) -> Int = { 0 }
): EnterExitEffect {
    val data = ExpandShrinkEffectData(
        axisAlignmentOnAbsent = alignmentOnAbsent,
        animationSpec = animationSpec,
        axisSizeOnAbsent = sizeOnAbsent
    )
    return ExpandShrinkEffect(
        enter = data,
        exit = data
    )
}

@Stable
fun ExpandIn(
    expandFrom: Alignment.Horizontal,
    animationSpec: FiniteAnimationSpec = DefaultEffectIntAnimationSpec,
    startSize: (fullSize: Int) -> Int = { 0 }
): EnterExitEffect {
    val data = ExpandShrinkEffectData(
        axisAlignmentOnAbsent = expandFrom,
        animationSpec = animationSpec,
        axisSizeOnAbsent = startSize
    )
    return ExpandShrinkEffect(
        enter = data,
        exit = null
    )
}

@Stable
fun ShrinkOut(
    shrinkTo: Alignment.Horizontal,
    animationSpec: FiniteAnimationSpec = DefaultEffectIntAnimationSpec,
    endSize: (fullSize: Int) -> Int = { 0 }
): EnterExitEffect {
    val data = ExpandShrinkEffectData(
        axisAlignmentOnAbsent = shrinkTo,
        animationSpec = animationSpec,
        axisSizeOnAbsent = endSize
    )
    return ExpandShrinkEffect(
        enter = null,
        exit = data
    )
}

@Stable
fun ExpandShrink(
    alignmentOnAbsent: Alignment.Vertical,
    animationSpec: FiniteAnimationSpec = DefaultEffectIntAnimationSpec,
    sizeOnAbsent: (fullSize: Int) -> Int = { 0 }
): EnterExitEffect {
    val data = ExpandShrinkEffectData(
        axisAlignmentOnAbsent = alignmentOnAbsent,
        animationSpec = animationSpec,
        axisSizeOnAbsent = sizeOnAbsent
    )
    return ExpandShrinkEffect(
        enter = data,
        exit = data
    )
}

@Stable
fun ExpandIn(
    expandFrom: Alignment.Vertical,
    animationSpec: FiniteAnimationSpec = DefaultEffectIntAnimationSpec,
    startSize: (fullSize: Int) -> Int = { 0 }
): EnterExitEffect {
    val data = ExpandShrinkEffectData(
        axisAlignmentOnAbsent = expandFrom,
        animationSpec = animationSpec,
        axisSizeOnAbsent = startSize
    )
    return ExpandShrinkEffect(
        enter = data,
        exit = null
    )
}

@Stable
fun ShrinkOut(
    shrinkTo: Alignment.Vertical,
    animationSpec: FiniteAnimationSpec = DefaultEffectIntAnimationSpec,
    endSize: (fullSize: Int) -> Int = { 0 }
): EnterExitEffect {
    val data = ExpandShrinkEffectData(
        axisAlignmentOnAbsent = shrinkTo,
        animationSpec = animationSpec,
        axisSizeOnAbsent = endSize
    )
    return ExpandShrinkEffect(
        enter = null,
        exit = data
    )
}

// ======== Internal ========

// -------- Constants --------

private val DefaultEffectFloatAnimationSpec: FiniteAnimationSpec = SpringSpec(
    stiffness = SpringPhysics.Stiffness.MediumLow
)

private val DefaultEffectIntAnimationSpec: FiniteAnimationSpec = SpringSpec(
    stiffness = SpringPhysics.Stiffness.MediumLow
)

private val DefaultEffectIntOffsetAnimationSpec: FiniteAnimationSpec = SpringSpec(
    stiffness = SpringPhysics.Stiffness.MediumLow
)

private val DefaultEffectIntSizeAnimationSpec: FiniteAnimationSpec = SpringSpec(
    stiffness = SpringPhysics.Stiffness.MediumLow
)

private val SnapFloatAnimationSpec: FiniteAnimationSpec = SnapSpec()

private val SnapIntOffsetAnimationSpec: FiniteAnimationSpec = SnapSpec()

private val SnapIntSizeAnimationSpec: FiniteAnimationSpec = SnapSpec()

// -------- Combined Enter Exit Effect --------

private data class CombinedEnterExitEffect(
    val outer: EnterExitEffect,
    val inner: Element<*>
) : EnterExitEffect {
    override fun  fold(initial: R, operation: (R, Element<*>) -> R): R {
        return inner.fold(outer.fold(initial, operation), operation)
    }

    override fun > get(type: Type): E? {
        return outer[type] ?: inner[type]
    }

    override fun minus(type: Type<*>): EnterExitEffect {
        if (inner[type] != null) {
            return outer
        }
        return when (val remainingOuter = outer - type) {
            outer -> this
            EnterExitEffect -> inner
            else -> CombinedEnterExitEffect(remainingOuter, inner)
        }
    }

    override fun toString(): String {
        val content = fold("") { acc, element ->
            if (acc.isNotEmpty()) "$acc, $element" else element.toString()
        }
        return "[$content]"
    }
}

// -------- Merge Element --------

private fun > mergeUnsafe(
    old: Element,
    new: Element<*>
): Element {
    require(old::class.java == new::class.java) {
        "Cannot cast ${new::class.simpleName} to ${old::class.simpleName}. " +
                "Did you use a single Type for two different Element implementations?"
    }
    @Suppress("UNCHECKED_CAST")
    new as E
    return old merge new
}

// -------- Fade --------

private data class FadeEffect(
    val enter: FadeEffectData?,
    val exit: FadeEffectData?
) : Element {
    override val type: Type
        get() = FadeEffect

    override fun Modifier.transition(transition: Transition): Modifier {
        return composed {
            val alpha by transition.animateFloat(
                transitionSpec = {
                    when {
                        PreEnter to Visible || PostExit to Visible -> enter?.animationSpec ?: SnapFloatAnimationSpec
                        Visible to PostExit -> exit?.animationSpec ?: SnapFloatAnimationSpec
                        else -> SnapFloatAnimationSpec
                    }
                }
            ) { state ->
                when (state) {
                    PreEnter -> enter?.alphaOnAbsent ?: 1.0f
                    Visible -> 1.0f
                    PostExit -> exit?.alphaOnAbsent ?: 1.0f
                }
            }
            graphicsLayer {
                this.alpha = alpha
            }
        }
    }

    override fun merge(new: FadeEffect): FadeEffect {
        val enter = new.enter ?: this.enter
        val exit = new.exit ?: this.exit
        return FadeEffect(
            enter = enter,
            exit = exit
        )
    }

    companion object : Type
}

private data class FadeEffectData(
    val animationSpec: FiniteAnimationSpec,
    val alphaOnAbsent: Float
)

// -------- Slide --------

private data class SlideEffect(
    val enter: SlideEffectData?,
    val exit: SlideEffectData?
) : Element {
    override val type: Type
        get() = SlideEffect

    override fun Modifier.transition(transition: Transition): Modifier {
        return composed {
            val offsetAnimation = transition.createDeferredAnimation(IntOffset.VectorConvertor)
            val element = remember(offsetAnimation) {
                SlideEffectModifier(
                    offsetAnimation = offsetAnimation,
                    enter = enter,
                    exit = exit
                )
            }
            this then element
        }
    }

    override fun merge(new: SlideEffect): SlideEffect {
        val enter = new.enter ?: this.enter
        val exit = new.exit ?: this.exit
        return SlideEffect(
            enter = enter,
            exit = exit
        )
    }

    companion object : Type
}

private data class SlideEffectData(
    val animationSpec: FiniteAnimationSpec,
    val offsetOnAbsent: (IntSize) -> IntOffset
) {
    constructor(
        orientation: LayoutOrientation,
        animationSpec: FiniteAnimationSpec,
        axisOffsetOnAbsent: (Int) -> Int
    ) : this(
        animationSpec = AxisAnimationSpec(
            orientation = orientation,
            original = animationSpec
        ),
        offsetOnAbsent = { size ->
            when (orientation) {
                Horizontal -> {
                    val x = axisOffsetOnAbsent(size.width)
                    IntOffset(x, 0)
                }
                Vertical -> {
                    val y = axisOffsetOnAbsent(size.height)
                    IntOffset(0, y)
                }
            }
        }
    )
}

private data class SlideEffectModifier(
    val offsetAnimation: Transition.DeferredAnimation,
    val enter: SlideEffectData?,
    val exit: SlideEffectData?
) : ModifierNodeElement() {
    override fun create(): SlideEffectModifierNode {
        return SlideEffectModifierNode(
            offsetAnimation = offsetAnimation,
            enter = enter,
            exit = exit
        )
    }

    override fun update(node: SlideEffectModifierNode) {
        node.offsetAnimation = offsetAnimation
        node.enter = enter
        node.exit = exit
    }

    override fun InspectInfo.inspect() {
        set("enter", enter)
        set("exit", exit)
    }
}

private class SlideEffectModifierNode(
    var offsetAnimation: Transition.DeferredAnimation,
    var enter: SlideEffectData?,
    var exit: SlideEffectData?
) : ModifierNode(), LayoutModifierNode {
    private val transitionSpec: Transition.Segment.() -> FiniteAnimationSpec = {
        when {
            PreEnter to Visible || PostExit to Visible -> enter?.animationSpec ?: SnapIntOffsetAnimationSpec
            Visible to PostExit -> exit?.animationSpec ?: SnapIntOffsetAnimationSpec
            else -> SnapIntOffsetAnimationSpec
        }
    }

    override fun measure(measurable: Measurable, constraints: Constraints): MeasureResult {
        val placeable = measurable.measure(constraints)
        val size = IntSize(placeable.width, placeable.height)
        return MeasureResult(placeable.width, placeable.height) {
            val offset by offsetAnimation.animate(
                transitionSpec = transitionSpec
            ) { state ->
                when (state) {
                    PreEnter -> enter?.offsetOnAbsent?.invoke(size) ?: IntOffset.Zero
                    Visible -> IntOffset.Zero
                    PostExit -> exit?.offsetOnAbsent?.invoke(size) ?: IntOffset.Zero
                }
            }
            placeable.place(offset)
        }
    }
}

// -------- Expand Shrink --------

private data class ExpandShrinkEffect(
    val enter: ExpandShrinkEffectData?,
    val exit: ExpandShrinkEffectData?
) : Element {
    override val type: Type
        get() = Companion

    override fun Modifier.transition(transition: Transition): Modifier {
        return composed {
            val sizeAnimation = transition.createDeferredAnimation(IntSize.VectorConvertor)
            val currentAlignment = if (transition.state == transition.targetState) {
                Alignment.TopLeft
            } else {
                with(transition.segment) {
                    when {
                        PreEnter to Visible || PostExit to Visible -> enter?.alignmentOnAbsent ?: Alignment.TopLeft
                        Visible to PostExit -> exit?.alignmentOnAbsent ?: Alignment.TopLeft
                        else -> Alignment.TopLeft
                    }
                }
            }
            val element = remember(sizeAnimation, currentAlignment) {
                ExpandShrinkEffectModifier(
                    sizeAnimation = sizeAnimation,
                    enter = enter,
                    exit = exit,
                    currentAlignment = currentAlignment
                )
            }
            this then element
        }
    }

    override fun merge(new: ExpandShrinkEffect): ExpandShrinkEffect {
        val enter = new.enter ?: this.enter
        val exit = new.exit ?: this.exit
        return ExpandShrinkEffect(
            enter = enter,
            exit = exit
        )
    }

    companion object : Type
}

private data class ExpandShrinkEffectData(
    val alignmentOnAbsent: Alignment,
    val animationSpec: FiniteAnimationSpec,
    val sizeOnAbsent: (IntSize) -> IntSize
) {
    constructor(
        axisAlignmentOnAbsent: Alignment.Horizontal,
        animationSpec: FiniteAnimationSpec,
        axisSizeOnAbsent: (Int) -> Int
    ) : this(
        alignmentOnAbsent = HorizontalAlignmentAdapter(axisAlignmentOnAbsent),
        animationSpec = AxisAnimationSpec(
            orientation = Horizontal,
            original = animationSpec
        ),
        sizeOnAbsent = { size ->
            IntSize(
                width = axisSizeOnAbsent(size.width),
                height = size.height
            )
        }
    )

    constructor(
        axisAlignmentOnAbsent: Alignment.Vertical,
        animationSpec: FiniteAnimationSpec,
        axisSizeOnAbsent: (Int) -> Int
    ) : this(
        alignmentOnAbsent = VerticalAlignmentAdapter(axisAlignmentOnAbsent),
        animationSpec = AxisAnimationSpec(
            orientation = Vertical,
            original = animationSpec
        ),
        sizeOnAbsent = { size ->
            IntSize(
                width = size.width,
                height = axisSizeOnAbsent(size.height)
            )
        }
    )
}

private data class ExpandShrinkEffectModifier(
    val sizeAnimation: Transition.DeferredAnimation,
    val enter: ExpandShrinkEffectData?,
    val exit: ExpandShrinkEffectData?,
    val currentAlignment: Alignment
) : ModifierNodeElement() {
    override fun create(): ExpandShrinkEffectModifierNode {
        return ExpandShrinkEffectModifierNode(
            sizeAnimation = sizeAnimation,
            enter = enter,
            exit = exit,
            currentAlignment = currentAlignment
        )
    }

    override fun update(node: ExpandShrinkEffectModifierNode) {
        node.sizeAnimation = sizeAnimation
        node.enter = enter
        node.exit = exit
        node.currentAlignment = currentAlignment
    }

    override fun InspectInfo.inspect() {
        set("enter", enter)
        set("exit", exit)
    }
}

private class ExpandShrinkEffectModifierNode(
    var sizeAnimation: Transition.DeferredAnimation,
    var enter: ExpandShrinkEffectData?,
    var exit: ExpandShrinkEffectData?,
    var currentAlignment: Alignment
) : ModifierNode(), LayoutModifierNode {
    private val transitionSpec: Transition.Segment.() -> FiniteAnimationSpec = {
        when {
            PreEnter to Visible || PostExit to Visible -> enter?.animationSpec ?: SnapIntSizeAnimationSpec
            Visible to PostExit -> exit?.animationSpec ?: SnapIntSizeAnimationSpec
            else -> SnapIntSizeAnimationSpec
        }
    }

    override fun measure(measurable: Measurable, constraints: Constraints): MeasureResult {
        val placeable = measurable.measure(constraints)
        val fullSize = IntSize(placeable.width, placeable.height)
        val size by sizeAnimation.animate(
            transitionSpec = transitionSpec
        ) { state ->
            when (state) {
                PreEnter -> enter?.sizeOnAbsent?.invoke(fullSize) ?: fullSize
                Visible -> fullSize
                PostExit -> exit?.sizeOnAbsent?.invoke(fullSize) ?: fullSize
            }
        }
        val position = currentAlignment.align(
            contentSize = fullSize,
            availableSpace = size
        )
        return MeasureResult(size.width, size.height) {
            placeable.place(position)
        }
    }
}

// -------- Axis Animation Spec --------

private data class AxisAnimationSpec(
    val orientation: LayoutOrientation,
    val original: FiniteAnimationSpec
) : FiniteAnimationSpec {
    override fun > vectorize(
        convertor: VectorConvertor
    ): VectorizedFiniteAnimationSpec {
        // We simply ignore convertor because it must be (IntOffset/IntSize).VectorConvertor
        val dynamicAxisAnimationSpec = original.vectorize(Int.VectorConvertor)
        val dynamicAxisFloatAnimationSpec = DelegatedFloatAnimationSpec(dynamicAxisAnimationSpec)
        val axisFloatAnimationSpecs = when (orientation) {
            Horizontal -> arrayOf(dynamicAxisFloatAnimationSpec, NoopFloatAnimationSpec)
            Vertical -> arrayOf(NoopFloatAnimationSpec, dynamicAxisFloatAnimationSpec)
        }
        return VectorizedAxisAnimationSpec(axisFloatAnimationSpecs)
    }
}

// V must be AnimationVector2D (IntOffset/IntSize)
private class VectorizedAxisAnimationSpec>(
    val axisFloatAnimationSpecs: Array
) : VectorizedDimensionIndependentAnimationSpec(),
    VectorizedFiniteAnimationSpec
{
    override fun get(index: Int): FloatAnimationSpec {
        return axisFloatAnimationSpecs[index]
    }
}

// -------- Delegated Float Animation Spec --------

// This class may break @Stable contract, so we won't open it
private data class DelegatedFloatAnimationSpec(
    val delegate: VectorizedAnimationSpec
) : FloatAnimationSpec {
    // Cache some vectors to reduce overhead
    private val initialValueVector: AnimationVector1D = AnimationVector1D()

    private val targetValueVector: AnimationVector1D = AnimationVector1D()

    private val initialVelocityVector: AnimationVector1D = AnimationVector1D()

    override fun getValueFromMillis(
        playTimeMillis: Int,
        initialValue: Float,
        targetValue: Float,
        initialVelocity: Float
    ): Float {
        val initialValueVector = initialValueVector.apply { v1 = initialValue }
        val targetValueVector = targetValueVector.apply { v1 = targetValue }
        val initialVelocityVector = initialVelocityVector.apply { v1 = initialVelocity }
        val valueVector = delegate.getValueFromMillis(
            playTimeMillis = playTimeMillis,
            initialValue = initialValueVector,
            targetValue = targetValueVector,
            initialVelocity = initialVelocityVector
        )
        return valueVector.v1
    }

    override fun getVelocityFromMillis(
        playTimeMillis: Int,
        initialValue: Float,
        targetValue: Float,
        initialVelocity: Float
    ): Float {
        val initialValueVector = initialValueVector.apply { v1 = initialValue }
        val targetValueVector = targetValueVector.apply { v1 = targetValue }
        val initialVelocityVector = initialVelocityVector.apply { v1 = initialVelocity }
        val velocityVector = delegate.getVelocityFromMillis(
            playTimeMillis = playTimeMillis,
            initialValue = initialValueVector,
            targetValue = targetValueVector,
            initialVelocity = initialVelocityVector
        )
        return velocityVector.v1
    }

    override fun getDurationMillis(
        initialValue: Float,
        targetValue: Float,
        initialVelocity: Float
    ): Int {
        val initialValueVector = initialValueVector.apply { v1 = initialValue }
        val targetValueVector = targetValueVector.apply { v1 = targetValue }
        val initialVelocityVector = initialVelocityVector.apply { v1 = initialVelocity }
        return delegate.getDurationMillis(
            initialValue = initialValueVector,
            targetValue = targetValueVector,
            initialVelocity = initialVelocityVector
        )
    }
}

// -------- Alignment Adapter --------

private data class HorizontalAlignmentAdapter(
    val original: Alignment.Horizontal
) : Alignment {
    override fun align(contentSize: IntSize, availableSpace: IntSize): IntOffset {
        val x = original.alignHorizontally(contentSize.width, availableSpace.width)
        val y = Alignment.CenterVertically.alignVertically(contentSize.height, availableSpace.height)
        return IntOffset(x, y)
    }
}

private data class VerticalAlignmentAdapter(
    val original: Alignment.Vertical
) : Alignment {
    override fun align(contentSize: IntSize, availableSpace: IntSize): IntOffset {
        val x = Alignment.CenterHorizontally.alignHorizontally(contentSize.width, availableSpace.width)
        val y = original.alignVertically(contentSize.height, availableSpace.height)
        return IntOffset(x, y)
    }
}