SwipeToDismissBox.kt

/*
 * Copyright 2021 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.wear.compose.material

import androidx.annotation.RestrictTo
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.TweenSpec
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.LocalSwipeToDismissBackgroundScrimColor
import androidx.wear.compose.foundation.LocalSwipeToDismissContentScrimColor
import androidx.wear.compose.foundation.edgeSwipeToDismiss as foundationEdgeSwipeToDismiss

/**
 * Wear Material [SwipeToDismissBox] that handles the swipe-to-dismiss gesture. Takes a single
 * slot for the background (only displayed during the swipe gesture) and the foreground content.
 *
 * Example of a [SwipeToDismissBox] with stateful composables:
 * @sample androidx.wear.compose.material.samples.StatefulSwipeToDismissBox
 *
 * Example of using [Modifier.edgeSwipeToDismiss] with [SwipeToDismissBox]
 * @sample androidx.wear.compose.material.samples.EdgeSwipeForSwipeToDismiss
 *
 * For more information, see the
 * [Swipe to dismiss](https://developer.android.com/training/wearables/components/swipe-to-dismiss)
 * guide.
 *
 * @param state State containing information about ongoing swipe or animation.
 * @param modifier Optional [Modifier] for this component.
 * @param backgroundScrimColor Color for background scrim
 * @param contentScrimColor Optional [Color] used for the scrim over the
 * content composable during the swipe gesture.
 * @param backgroundKey Optional [key] which identifies the content currently composed in
 * the [content] block when isBackground == true. Provide the backgroundKey if your background
 * content will be displayed as a foreground after the swipe animation ends
 * (as is common when [SwipeToDismissBox] is used for the navigation). This allows
 * remembered state to be correctly moved between background and foreground.
 * @Param contentKey Optional [key] which identifies the content currently composed in the
 * [content] block when isBackground == false. See [backgroundKey].
 * @Param hasBackground Optional [Boolean] used to indicate if the content has no background,
 * in which case the swipe gesture is disabled (since there is no parent destination).
 * @param content Slot for content, with the isBackground parameter enabling content to be
 * displayed behind the foreground content - the background is normally hidden,
 * is shown behind a scrim during the swipe gesture,
 * and is shown without scrim once the finger passes the swipe-to-dismiss threshold.
 */
@Composable
public fun SwipeToDismissBox(
    state: androidx.wear.compose.foundation.SwipeToDismissBoxState,
    modifier: Modifier = Modifier,
    backgroundScrimColor: Color = MaterialTheme.colors.background,
    contentScrimColor: Color = MaterialTheme.colors.background,
    backgroundKey: Any = SwipeToDismissKeys.Background,
    contentKey: Any = SwipeToDismissKeys.Content,
    hasBackground: Boolean = true,
    content: @Composable BoxScope.(isBackground: Boolean) -> Unit
) {
    CompositionLocalProvider(
        LocalSwipeToDismissBackgroundScrimColor provides backgroundScrimColor,
        LocalSwipeToDismissContentScrimColor provides contentScrimColor
    ) {
        androidx.wear.compose.foundation.BasicSwipeToDismissBox(
            state = state,
            modifier = modifier,
            backgroundKey = backgroundKey,
            contentKey = contentKey,
            userSwipeEnabled = hasBackground,
            content = content
        )
    }
}

/**
 * Wear Material [SwipeToDismissBox] that handles the swipe-to-dismiss gesture. Takes a single
 * slot for the background (only displayed during the swipe gesture) and the foreground content.
 *
 * Example of a [SwipeToDismissBox] with stateful composables:
 * @sample androidx.wear.compose.material.samples.StatefulSwipeToDismissBox
 *
 * Example of using [Modifier.edgeSwipeToDismiss] with [SwipeToDismissBox]
 * @sample androidx.wear.compose.material.samples.EdgeSwipeForSwipeToDismiss
 *
 * For more information, see the
 * [Swipe to dismiss](https://developer.android.com/training/wearables/components/swipe-to-dismiss)
 * guide.
 *
 * @param state State containing information about ongoing swipe or animation.
 * @param modifier Optional [Modifier] for this component.
 * @param backgroundScrimColor Color for background scrim
 * @param contentScrimColor Optional [Color] used for the scrim over the
 * content composable during the swipe gesture.
 * @param backgroundKey Optional [key] which identifies the content currently composed in
 * the [content] block when isBackground == true. Provide the backgroundKey if your background
 * content will be displayed as a foreground after the swipe animation ends
 * (as is common when [SwipeToDismissBox] is used for the navigation). This allows
 * remembered state to be correctly moved between background and foreground.
 * @Param contentKey Optional [key] which identifies the content currently composed in the
 * [content] block when isBackground == false. See [backgroundKey].
 * @Param hasBackground Optional [Boolean] used to indicate if the content has no background,
 * in which case the swipe gesture is disabled (since there is no parent destination).
 * @param content Slot for content, with the isBackground parameter enabling content to be
 * displayed behind the foreground content - the background is normally hidden,
 * is shown behind a scrim during the swipe gesture,
 * and is shown without scrim once the finger passes the swipe-to-dismiss threshold.
 */
@Suppress("DEPRECATION")
@Deprecated(
    "This overload is provided for backwards compatibility. " +
        "A newer overload is available that uses " +
        "androidx.wear.compose.foundation.SwipeToDismissBoxState.",
    replaceWith = ReplaceWith("SwipeToDismissBox(" +
        "state, modifier, backgroundScrimColor, contentScrimColor, backgroundKey, contentKey," +
        "hasBackground, content)"
    )
)
@Composable
public fun SwipeToDismissBox(
    state: SwipeToDismissBoxState,
    modifier: Modifier = Modifier,
    backgroundScrimColor: Color = MaterialTheme.colors.background,
    contentScrimColor: Color = MaterialTheme.colors.background,
    backgroundKey: Any = SwipeToDismissKeys.Background,
    contentKey: Any = SwipeToDismissKeys.Content,
    hasBackground: Boolean = true,
    content: @Composable BoxScope.(isBackground: Boolean) -> Unit
) {
    CompositionLocalProvider(
        LocalSwipeToDismissBackgroundScrimColor provides backgroundScrimColor,
        LocalSwipeToDismissContentScrimColor provides contentScrimColor
    ) {
        androidx.wear.compose.foundation.BasicSwipeToDismissBox(
            state = state.foundationState,
            modifier = modifier,
            backgroundKey = backgroundKey,
            contentKey = contentKey,
            userSwipeEnabled = hasBackground,
            content = content
        )
    }
}

/**
 * Wear Material [SwipeToDismissBox] that handles the swipe-to-dismiss gesture.
 * This overload takes an [onDismissed] parameter which is used to execute a command when the
 * swipe to dismiss has completed, such as navigating to another screen.
 *
 * Example of a simple SwipeToDismissBox:
 * @sample androidx.wear.compose.material.samples.SimpleSwipeToDismissBox
 *
 * Example of using [Modifier.edgeSwipeToDismiss] with [SwipeToDismissBox]
 * @sample androidx.wear.compose.material.samples.EdgeSwipeForSwipeToDismiss
 *
 * For more information, see the
 * [Swipe to dismiss](https://developer.android.com/training/wearables/components/swipe-to-dismiss)
 * guide.
 *
 * @param onDismissed Executes when the swipe to dismiss has completed.
 * @param modifier Optional [Modifier] for this component.
 * @param state State containing information about ongoing swipe or animation.
 * @param backgroundScrimColor Color for background scrim
 * @param contentScrimColor Optional [Color] used for the scrim over the
 * content composable during the swipe gesture.
 * @param backgroundKey Optional [key] which identifies the content currently composed in
 * the [content] block when isBackground == true. Provide the backgroundKey if your background
 * content will be displayed as a foreground after the swipe animation ends
 * (as is common when [SwipeToDismissBox] is used for the navigation). This allows
 * remembered state to be correctly moved between background and foreground.
 * @Param contentKey Optional [key] which identifies the content currently composed in the
 * [content] block when isBackground == false. See [backgroundKey].
 * @Param hasBackground Optional [Boolean] used to indicate if the content has no background,
 * in which case the swipe gesture is disabled (since there is no parent destination).
 * @param content Slot for content, with the isBackground parameter enabling content to be
 * displayed behind the foreground content - the background is normally hidden,
 * is shown behind a scrim during the swipe gesture,
 * and is shown without scrim once the finger passes the swipe-to-dismiss threshold.
 */
@Composable
public fun SwipeToDismissBox(
    onDismissed: () -> Unit,
    modifier: Modifier = Modifier,
    state: androidx.wear.compose.foundation.SwipeToDismissBoxState =
        androidx.wear.compose.foundation.rememberSwipeToDismissBoxState(),
    backgroundScrimColor: Color = MaterialTheme.colors.background,
    contentScrimColor: Color = MaterialTheme.colors.background,
    backgroundKey: Any = SwipeToDismissKeys.Background,
    contentKey: Any = SwipeToDismissKeys.Content,
    hasBackground: Boolean = true,
    content: @Composable BoxScope.(isBackground: Boolean) -> Unit
) {
    CompositionLocalProvider(
        LocalSwipeToDismissBackgroundScrimColor provides backgroundScrimColor,
        LocalSwipeToDismissContentScrimColor provides contentScrimColor
    ) {
        androidx.wear.compose.foundation.BasicSwipeToDismissBox(
            state = state,
            modifier = modifier,
            onDismissed = onDismissed,
            backgroundKey = backgroundKey,
            contentKey = contentKey,
            userSwipeEnabled = hasBackground,
            content = content
        )
    }
}

/**
 * Wear Material [SwipeToDismissBox] that handles the swipe-to-dismiss gesture.
 * This overload takes an [onDismissed] parameter which is used to execute a command when the
 * swipe to dismiss has completed, such as navigating to another screen.
 *
 * Example of a simple SwipeToDismissBox:
 * @sample androidx.wear.compose.material.samples.SimpleSwipeToDismissBox
 *
 * Example of using [Modifier.edgeSwipeToDismiss] with [SwipeToDismissBox]
 * @sample androidx.wear.compose.material.samples.EdgeSwipeForSwipeToDismiss
 *
 * For more information, see the
 * [Swipe to dismiss](https://developer.android.com/training/wearables/components/swipe-to-dismiss)
 * guide.
 *
 * @param onDismissed Executes when the swipe to dismiss has completed.
 * @param modifier Optional [Modifier] for this component.
 * @param state State containing information about ongoing swipe or animation.
 * @param backgroundScrimColor Color for background scrim
 * @param contentScrimColor Optional [Color] used for the scrim over the
 * content composable during the swipe gesture.
 * @param backgroundKey Optional [key] which identifies the content currently composed in
 * the [content] block when isBackground == true. Provide the backgroundKey if your background
 * content will be displayed as a foreground after the swipe animation ends
 * (as is common when [SwipeToDismissBox] is used for the navigation). This allows
 * remembered state to be correctly moved between background and foreground.
 * @Param contentKey Optional [key] which identifies the content currently composed in the
 * [content] block when isBackground == false. See [backgroundKey].
 * @Param hasBackground Optional [Boolean] used to indicate if the content has no background,
 * in which case the swipe gesture is disabled (since there is no parent destination).
 * @param content Slot for content, with the isBackground parameter enabling content to be
 * displayed behind the foreground content - the background is normally hidden,
 * is shown behind a scrim during the swipe gesture,
 * and is shown without scrim once the finger passes the swipe-to-dismiss threshold.
 */
@Suppress("DEPRECATION")
@Deprecated(
    "This overload is provided for backwards compatibility. " +
        "A newer overload is available that uses " +
        "androidx.wear.compose.foundation.SwipeToDismissBoxState.",
    replaceWith = ReplaceWith("SwipeToDismissBox(" +
        "onDismiss, modifier, state, backgroundScrimColor, contentScrimColor, backgroundKey," +
        "contentKey, hasBackground, content)"
    ),
    level = DeprecationLevel.HIDDEN
)
@Composable
public fun SwipeToDismissBox(
    onDismissed: () -> Unit,
    modifier: Modifier = Modifier,
    state: SwipeToDismissBoxState = rememberSwipeToDismissBoxState(),
    backgroundScrimColor: Color = MaterialTheme.colors.background,
    contentScrimColor: Color = MaterialTheme.colors.background,
    backgroundKey: Any = SwipeToDismissKeys.Background,
    contentKey: Any = SwipeToDismissKeys.Content,
    hasBackground: Boolean = true,
    content: @Composable BoxScope.(isBackground: Boolean) -> Unit
) {
    CompositionLocalProvider(
        LocalSwipeToDismissBackgroundScrimColor provides backgroundScrimColor,
        LocalSwipeToDismissContentScrimColor provides contentScrimColor
    ) {
        androidx.wear.compose.foundation.BasicSwipeToDismissBox(
            state = state.foundationState,
            modifier = modifier,
            onDismissed = onDismissed,
            backgroundKey = backgroundKey,
            contentKey = contentKey,
            userSwipeEnabled = hasBackground,
            content = content
        )
    }
}

/**
 * State for [SwipeToDismissBox].
 *
 * @param animationSpec The default animation that will be used to animate to a new state.
 * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
 */
@Suppress("DEPRECATION")
@Deprecated(
    "SwipeToDismissBoxState has been migrated, please import it " +
        "from androidx.wear.compose.foundation.",
)
@Stable
public class SwipeToDismissBoxState(
    animationSpec: AnimationSpec<Float> = SwipeToDismissBoxDefaults.AnimationSpec,
    confirmStateChange: (SwipeToDismissValue) -> Boolean = { true },
) {
    /**
     * The current value of the state.
     *
     * Before and during a swipe, corresponds to [SwipeToDismissValue.Default], then switches to
     * [SwipeToDismissValue.Dismissed] if the swipe has been completed.
     */
    public val currentValue: SwipeToDismissValue
        get() = convertFromFoundationSwipeToDismissValue(foundationState.currentValue)

    /**
     * The target value of the state.
     *
     * If a swipe is in progress, this is the value that the state would animate to if the
     * swipe finished. If an animation is running, this is the target value of that animation.
     * Finally, if no swipe or animation is in progress, this is the same as the [currentValue].
     */
    public val targetValue: SwipeToDismissValue
        get() = convertFromFoundationSwipeToDismissValue(foundationState.targetValue)

    /**
     * Whether the state is currently animating.
     */
    public val isAnimationRunning: Boolean
        get() = foundationState.isAnimationRunning

    /**
     * Set the state without any animation and suspend until it's set
     *
     * @param targetValue The new target value to set [currentValue] to.
     */
    public suspend fun snapTo(targetValue: SwipeToDismissValue) =
        foundationState.snapTo(convertToFoundationSwipeToDismissValue(targetValue))

    /**
     * Foundation version of the [SwipeToDismissBoxState].
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    val foundationState = androidx.wear.compose.foundation.SwipeToDismissBoxState(
        animationSpec = animationSpec,
        confirmStateChange = { value: androidx.wear.compose.foundation.SwipeToDismissValue ->
            confirmStateChange(convertFromFoundationSwipeToDismissValue(value))
        }
    )
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        get() = field
}

/**
 * Create a [SwipeToDismissBoxState] and remember it.
 *
 * @param animationSpec The default animation used to animate to a new state.
 * @param confirmStateChange Optional callback to confirm or veto a pending state change.
 */
@Suppress("DEPRECATION")
@Deprecated(
    "Please import rememberSwipeToDismissBoxState from androidx.wear.compose.foundation.",
    ReplaceWith(
        "androidx.wear.compose.foundation.rememberSwipeToDismissBoxState(",
        "animationSpec, confirmStateChange)"
    )
)
@Composable
public fun rememberSwipeToDismissBoxState(
    animationSpec: AnimationSpec<Float> = SWIPE_TO_DISMISS_BOX_ANIMATION_SPEC,
    confirmStateChange: (SwipeToDismissValue) -> Boolean = { true },
): SwipeToDismissBoxState {
    return remember(animationSpec, confirmStateChange) {
        SwipeToDismissBoxState(animationSpec, confirmStateChange)
    }
}

/**
 * Contains defaults for [SwipeToDismissBox].
 */
@Deprecated(
    "Please import SwipeToDismissBoxDefaults from androidx.wear.compose.foundation.",
)
@Stable
public object SwipeToDismissBoxDefaults {
    /**
     * The default animation that will be used to animate to a new state after the swipe gesture.
     */
    @OptIn(ExperimentalWearMaterialApi::class)
    public val AnimationSpec = SwipeableDefaults.AnimationSpec

    /**
     * The default width of the area which might trigger a swipe
     * with [edgeSwipeToDismiss] modifier
     */
    public val EdgeWidth = 30.dp
}

/**
 * Keys used to persistent state in [SwipeToDismissBox].
 */
public enum class SwipeToDismissKeys {
    /**
     * The default background key to identify the content displayed by the content block
     * when isBackground == true. Specifying a background key instead of using the default
     * allows remembered state to be correctly moved between background and foreground.
     */
    Background,

    /**
     * The default content key to identify the content displayed by the content block
     * when isBackground == false. Specifying a background key instead of using the default
     * allows remembered state to be correctly moved between background and foreground.
     */
    Content
}

/**
 * States used as targets for the anchor points for swipe-to-dismiss.
 */
@Deprecated(
    "SwipeToDismiss has been migrated to androidx.wear.compose.foundation. " +
        "Please import SwipeToDismissValue from androidx.wear.compose.foundation instead.",
)
public enum class SwipeToDismissValue {
    /**
     * The state of the SwipeToDismissBox before the swipe started.
     */
    Default,

    /**
     * The state of the SwipeToDismissBox after the swipe passes the swipe-to-dismiss threshold.
     */
    Dismissed
}

/**
 * Limits swipe to dismiss to be active from the edge of the viewport only. Used when the center
 * of the screen needs to be able to handle horizontal paging, such as 2-d scrolling a Map
 * or swiping horizontally between pages. Swipe to the right is intercepted on the left
 * part of the viewport with width specified by [edgeWidth], with other touch events
 * ignored - vertical scroll, click, long click, etc.
 *
 * Currently Edge swipe, like swipe to dismiss, is only supported on the left part of the viewport
 * regardless of layout direction as content is swiped away from left to right.
 *
 * Requires that the element to which this modifier is applied exists within a
 * SwipeToDismissBox which is using the same [SwipeToDismissBoxState] instance.
 *
 * Example of a modifier usage with SwipeToDismiss
 * @sample androidx.wear.compose.material.samples.EdgeSwipeForSwipeToDismiss
 *
 * @param swipeToDismissBoxState A state of SwipeToDismissBox. Used to trigger swipe gestures
 * on SwipeToDismissBox
 * @param edgeWidth A width of edge, where swipe should be recognised
 */
@Suppress("DEPRECATION")
@Deprecated(
    "SwipeToDismiss has been migrated to androidx.wear.compose.foundation. " +
        "Please import Modifier.edgeSwipeToDismiss from androidx.wear.compose.foundation instead.",
    replaceWith = ReplaceWith(
        "androidx.wear.compose.foundation.edgeSwipeToDismiss(",
        "swipeToDismissBoxState, edgeWidth)"
    )
)
public fun Modifier.edgeSwipeToDismiss(
    swipeToDismissBoxState: SwipeToDismissBoxState,
    edgeWidth: Dp = SwipeToDismissBoxDefaults.EdgeWidth
): Modifier =
    foundationEdgeSwipeToDismiss(
        swipeToDismissBoxState = swipeToDismissBoxState.foundationState,
        edgeWidth = edgeWidth
    )

@Suppress("DEPRECATION")
private fun convertToFoundationSwipeToDismissValue(
    value: SwipeToDismissValue
) = when (value) {
    SwipeToDismissValue.Default ->
        androidx.wear.compose.foundation.SwipeToDismissValue.Default

    SwipeToDismissValue.Dismissed ->
        androidx.wear.compose.foundation.SwipeToDismissValue.Dismissed
}

@Suppress("DEPRECATION")
private fun convertFromFoundationSwipeToDismissValue(
    value: androidx.wear.compose.foundation.SwipeToDismissValue
) = when (value) {
    androidx.wear.compose.foundation.SwipeToDismissValue.Default ->
        SwipeToDismissValue.Default

    androidx.wear.compose.foundation.SwipeToDismissValue.Dismissed ->
        SwipeToDismissValue.Dismissed
}

private val SWIPE_TO_DISMISS_BOX_ANIMATION_SPEC =
    TweenSpec<Float>(200, 0, LinearOutSlowInEasing)