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

commonMain.androidx.compose.material3.adaptive.layout.PaneMotion.kt Maven / Gradle / Ivy

/*
 * Copyright 2024 The Android Open Source Project
 *
 * 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 androidx.compose.material3.adaptive.layout

import androidx.annotation.VisibleForTesting
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import kotlin.jvm.JvmInline
import kotlin.math.max
import kotlin.math.min

@ExperimentalMaterial3AdaptiveApi
internal interface PaneMotionScope : LookaheadScope {
    val positionAnimationSpec: FiniteAnimationSpec
    val sizeAnimationSpec: FiniteAnimationSpec
    val delayedPositionAnimationSpec: FiniteAnimationSpec
    val slideInFromLeftOffset: Int
    val slideInFromRightOffset: Int
    val slideOutToLeftOffset: Int
    val slideOutToRightOffset: Int
    val motionProgress: () -> Float
}

@ExperimentalMaterial3AdaptiveApi
internal interface PaneMotion {
    val PaneMotionScope.enterTransition: EnterTransition
    val PaneMotionScope.exitTransition: ExitTransition
    val PaneMotionScope.animateBoundsModifier: Modifier
}

@ExperimentalMaterial3AdaptiveApi
@JvmInline
internal value class DefaultPaneMotion private constructor(val value: Int) : PaneMotion {
    companion object {
        val NoMotion = DefaultPaneMotion(0)
        val AnimateBounds = DefaultPaneMotion(1)
        val EnterFromLeft = DefaultPaneMotion(2)
        val EnterFromRight = DefaultPaneMotion(3)
        val EnterFromLeftDelayed = DefaultPaneMotion(4)
        val EnterFromRightDelayed = DefaultPaneMotion(5)
        val ExitToLeft = DefaultPaneMotion(6)
        val ExitToRight = DefaultPaneMotion(7)
        val EnterWithExpand = DefaultPaneMotion(8)
        val ExitWithShrink = DefaultPaneMotion(9)
    }

    override val PaneMotionScope.enterTransition: EnterTransition
        get() =
            when (this@DefaultPaneMotion) {
                EnterFromLeft ->
                    slideInHorizontally(positionAnimationSpec) { slideInFromLeftOffset }
                EnterFromRight ->
                    slideInHorizontally(positionAnimationSpec) { slideInFromRightOffset }
                EnterFromLeftDelayed ->
                    slideInHorizontally(delayedPositionAnimationSpec) { slideInFromLeftOffset }
                EnterFromRightDelayed ->
                    slideInHorizontally(delayedPositionAnimationSpec) { slideInFromRightOffset }
                // TODO(conradche): Figure out how to expand with position change
                EnterWithExpand ->
                    expandHorizontally(sizeAnimationSpec, Alignment.CenterHorizontally)
                else -> EnterTransition.None
            }

    override val PaneMotionScope.exitTransition: ExitTransition
        get() =
            when (this@DefaultPaneMotion) {
                ExitToLeft -> slideOutHorizontally(positionAnimationSpec) { slideOutToLeftOffset }
                ExitToRight -> slideOutHorizontally(positionAnimationSpec) { slideOutToRightOffset }
                // TODO(conradche): Figure out how to shrink with position change
                ExitWithShrink ->
                    shrinkHorizontally(sizeAnimationSpec, Alignment.CenterHorizontally)
                else -> ExitTransition.None
            }

    override val PaneMotionScope.animateBoundsModifier: Modifier
        get() =
            Modifier.animateBounds(
                motionProgress,
                sizeAnimationSpec,
                positionAnimationSpec,
                this,
                this@DefaultPaneMotion == AnimateBounds
            )

    override fun toString(): String =
        when (this) {
            NoMotion -> "NoMotion"
            AnimateBounds -> "AnimateBounds"
            EnterFromLeft -> "EnterFromLeft"
            EnterFromRight -> "EnterFromRight"
            EnterFromLeftDelayed -> "EnterFromLeftDelayed"
            EnterFromRightDelayed -> "EnterFromRightDelayed"
            ExitToLeft -> "ExitToLeft"
            ExitToRight -> "ExitToRight"
            EnterWithExpand -> "EnterWithExpand"
            ExitWithShrink -> "ExitWithShrink"
            else -> "Undefined($value)"
        }
}

@ExperimentalMaterial3AdaptiveApi
@VisibleForTesting
internal fun  calculatePaneMotion(
    previousScaffoldValue: PaneScaffoldValue,
    currentScaffoldValue: PaneScaffoldValue,
    paneOrder: PaneScaffoldHorizontalOrder
): Array {
    val numOfPanes = paneOrder.size
    val paneStatus = Array(numOfPanes) { PaneMotionStatus.Hidden }
    val paneMotions = Array(numOfPanes) { DefaultPaneMotion.NoMotion }
    var firstShownPaneIndex = numOfPanes
    var firstEnteringPaneIndex = numOfPanes
    var lastShownPaneIndex = -1
    var lastEnteringPaneIndex = -1
    // First pass, to decide the entering/exiting status of each pane, and collect info for
    // deciding, given a certain pane, if there's a pane on its left or on its right that is
    // entering or keep showing during the transition.
    // Also set up the motions of all panes that keep showing to AnimateBounds.
    paneOrder.forEachIndexed { i, role ->
        paneStatus[i] =
            PaneMotionStatus.calculate(previousScaffoldValue[role], currentScaffoldValue[role])
        when (paneStatus[i]) {
            PaneMotionStatus.Shown -> {
                firstShownPaneIndex = min(firstShownPaneIndex, i)
                lastShownPaneIndex = max(lastShownPaneIndex, i)
                paneMotions[i] = DefaultPaneMotion.AnimateBounds
            }
            PaneMotionStatus.Entering -> {
                firstEnteringPaneIndex = min(firstEnteringPaneIndex, i)
                lastEnteringPaneIndex = max(lastEnteringPaneIndex, i)
            }
        }
    }
    // Second pass, to decide the exiting motions of all exiting panes.
    // Also collects info for the next pass to decide the entering motions of entering panes.
    var hasPanesExitToRight = false
    var hasPanesExitToLeft = false
    var firstPaneExitToRightIndex = numOfPanes
    var lastPaneExitToLeftIndex = -1
    paneOrder.forEachIndexed { i, _ ->
        val hasShownPanesOnLeft = firstShownPaneIndex < i
        val hasEnteringPanesOnLeft = firstEnteringPaneIndex < i
        val hasShownPanesOnRight = lastShownPaneIndex > i
        val hasEnteringPanesOnRight = lastEnteringPaneIndex > i
        if (paneStatus[i] == PaneMotionStatus.Exiting) {
            paneMotions[i] =
                if (!hasShownPanesOnRight && !hasEnteringPanesOnRight) {
                    // No panes will interfere the motion on the right, exit to right.
                    hasPanesExitToRight = true
                    firstPaneExitToRightIndex = min(firstPaneExitToRightIndex, i)
                    DefaultPaneMotion.ExitToRight
                } else if (!hasShownPanesOnLeft && !hasEnteringPanesOnLeft) {
                    // No panes will interfere the motion on the left, exit to left.
                    hasPanesExitToLeft = true
                    lastPaneExitToLeftIndex = max(lastPaneExitToLeftIndex, i)
                    DefaultPaneMotion.ExitToLeft
                } else if (!hasShownPanesOnRight) {
                    // Only showing panes can interfere the motion on the right, exit to right.
                    hasPanesExitToRight = true
                    firstPaneExitToRightIndex = min(firstPaneExitToRightIndex, i)
                    DefaultPaneMotion.ExitToRight
                } else if (!hasShownPanesOnLeft) { // Only showing panes on left
                    // Only showing panes can interfere the motion on the left, exit to left.
                    hasPanesExitToLeft = true
                    lastPaneExitToLeftIndex = max(lastPaneExitToLeftIndex, i)
                    DefaultPaneMotion.ExitToLeft
                } else {
                    // Both sides has panes that keep being visible during transition, shrink to
                    // exit
                    DefaultPaneMotion.ExitWithShrink
                }
        }
    }
    // Third pass, to decide the entering motions of all entering panes.
    paneOrder.forEachIndexed { i, _ ->
        val hasShownPanesOnLeft = firstShownPaneIndex < i
        val hasShownPanesOnRight = lastShownPaneIndex > i
        val hasLeftPaneExitToRight = firstPaneExitToRightIndex < i
        val hasRightPaneExitToLeft = lastPaneExitToLeftIndex > i
        // For a given pane, if there's another pane that keeps showing on its right, or there's
        // a pane on its right that's exiting to its left, the pane cannot enter from right since
        // doing so will either interfere with the showing pane, or cause incorrect order of the
        // pane position during the transition. In other words, this case is considered "blocking".
        // Same on the other side.
        val noBlockingPanesOnRight = !hasShownPanesOnRight && !hasRightPaneExitToLeft
        val noBlockingPanesOnLeft = !hasShownPanesOnLeft && !hasLeftPaneExitToRight
        if (paneStatus[i] == PaneMotionStatus.Entering) {
            paneMotions[i] =
                if (noBlockingPanesOnRight && !hasPanesExitToRight) {
                    // No panes will block the motion on the right, enter from right.
                    DefaultPaneMotion.EnterFromRight
                } else if (noBlockingPanesOnLeft && !hasPanesExitToLeft) {
                    // No panes will block the motion on the left, enter from left.
                    DefaultPaneMotion.EnterFromLeft
                } else if (noBlockingPanesOnRight) {
                    // Only hiding panes can interfere the motion on the right, enter from right.
                    DefaultPaneMotion.EnterFromRightDelayed
                } else if (noBlockingPanesOnLeft) {
                    // Only hiding panes can interfere the motion on the left, enter from left.
                    DefaultPaneMotion.EnterFromLeftDelayed
                } else {
                    // Both sides has panes that keep being visible during transition, expand to
                    // enter
                    DefaultPaneMotion.EnterWithExpand
                }
        }
    }
    return paneMotions
}

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@JvmInline
private value class PaneMotionStatus private constructor(val value: Int) {
    companion object {
        val Hidden = PaneMotionStatus(0)
        val Exiting = PaneMotionStatus(1)
        val Entering = PaneMotionStatus(2)
        val Shown = PaneMotionStatus(3)

        fun calculate(
            previousValue: PaneAdaptedValue,
            currentValue: PaneAdaptedValue
        ): PaneMotionStatus {
            val wasShown = if (previousValue == PaneAdaptedValue.Hidden) 0 else 1
            val isShown = if (currentValue == PaneAdaptedValue.Hidden) 0 else 2
            return PaneMotionStatus(wasShown or isShown)
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy