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

commonMain.com.outsidesource.oskitcompose.popup.Modal.kt Maven / Gradle / Ivy

package com.outsidesource.oskitcompose.popup

import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.unit.*
import com.outsidesource.oskitcompose.modifier.OuterShadow
import com.outsidesource.oskitcompose.modifier.disablePointerInput
import com.outsidesource.oskitcompose.modifier.outerShadow
import com.outsidesource.oskitcompose.modifier.preventClickPropagationToParent

@Immutable
data class ModalStyles(
    val transitionDuration: Int = 200,
    val scrimColor: Color = Color.Black.copy(alpha = .5f),
    val shadow: OuterShadow = OuterShadow(
        blur = 11.dp,
        color = Color.Black.copy(alpha = .25f),
        shape = RoundedCornerShape(8.dp)
    ),
    val backgroundColor: Color = Color.White,
    val backgroundShape: Shape = RoundedCornerShape(8.dp),
    val windowPadding: PaddingValues = PaddingValues(20.dp),
    val contentPadding: PaddingValues = PaddingValues(16.dp),
) {
    companion object {
        /**
         * ModalStyles with all content set to unspecified to allow for custom user definition
         */
        val UserDefinedContent = ModalStyles(
            shadow = OuterShadow(blur = 0.dp, color = Color.Transparent),
            backgroundColor = Color.Transparent,
            backgroundShape = RectangleShape,
            windowPadding = PaddingValues(0.dp),
            contentPadding = PaddingValues(0.dp),
        )
    }
}


/**
 * Creates a fully customizable [Modal]
 *
 * @param isVisible Whether the modal is visible or not
 * @param onDismissRequest Executes when the user performs an action to dismiss the [Modal]
 * @param dismissOnExternalClick calls [onDismissRequest] when clicking on the scrim
 * @param dismissOnBackPress call [onDismissRequest] when pressing escape or back key
 * @param onPreviewKeyEvent Handles the onPreviewKey event
 * @param onKeyEvent Handles the onKeyEvent
 * @param isFullScreen Utilized in Android and iOS. Specifies whether to draw behind the system bars or not
 * @param styles Styles to modify the look of the [Modal]
 * @param content The content to be displayed inside the popup.
 */
@Composable
fun Modal(
    isVisible: Boolean,
    modifier: Modifier = Modifier,
    onDismissRequest: (() -> Unit)? = null,
    dismissOnExternalClick: Boolean = true,
    dismissOnBackPress: Boolean = true,
    onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
    onKeyEvent: (KeyEvent) -> Boolean = { false },
    isFullScreen: Boolean = true,
    styles: ModalStyles = remember { ModalStyles() },
    content: @Composable BoxScope.() -> Unit,
) {
    val density = LocalDensity.current
    val transition = updateTransition(isVisible)
    val alpha by transition.animateFloat(
        transitionSpec = { tween(styles.transitionDuration) },
        targetValueByState = { if (it) 1f else 0f },
        label = "AlphaAnimation"
    )
    val scale by transition.animateFloat(
        transitionSpec = { tween(styles.transitionDuration) },
        targetValueByState = { if (it) 1f else .95f },
        label = "ScaleAnimation"
    )
    val translate by transition.animateFloat(
        transitionSpec = { tween(styles.transitionDuration) },
        targetValueByState = { if (it) 0f else -10f * density.density },
        label = "TranslateAnimation"
    )

    if (transition.currentState || transition.targetState) {
        KMPPopup(
            popupPositionProvider = ModalPositionProvider,
            dismissOnBackPress = dismissOnBackPress,
            onDismissRequest = onDismissRequest,
            onPreviewKeyEvent = onPreviewKeyEvent,
            focusable = true,
            isFullScreen = isFullScreen,
            onKeyEvent = onKeyEvent,
        ) {
            Box(
                modifier = Modifier
                    .disablePointerInput(!LocalWindowInfo.current.isWindowFocused)
                    .clickable(remember { MutableInteractionSource() }, indication = null) {
                        if (dismissOnExternalClick) onDismissRequest?.invoke()
                    }
                    .fillMaxSize()
                    .graphicsLayer { this.alpha = alpha }
                    .background(styles.scrimColor),
                contentAlignment = Alignment.Center,
            ) {
                Box(
                    modifier = Modifier
                        .preventClickPropagationToParent()
                        .graphicsLayer {
                            this.scaleX = scale
                            this.scaleY = scale
                            this.translationY = translate
                        }
                        .padding(styles.windowPadding)
                        .outerShadow(
                            blur = styles.shadow.blur,
                            color = styles.shadow.color,
                            shape = styles.shadow.shape,
                            spread = styles.shadow.spread,
                            offset = styles.shadow.offset,
                        )
                        .background(
                            styles.backgroundColor,
                            styles.backgroundShape
                        )
                        .padding(styles.contentPadding)
                        .then(modifier)
                ) {
                    content()
                }
            }
        }
    }
}

private val ModalPositionProvider = object : PopupPositionProvider {
    override fun calculatePosition(
        anchorBounds: IntRect,
        windowSize: IntSize,
        layoutDirection: LayoutDirection,
        popupContentSize: IntSize
    ): IntOffset = IntOffset.Zero
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy