diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/animation/Animation.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/animation/Animation.skiko.kt index 15ed32f8a53a3..c3632c5a1cc1e 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/animation/Animation.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/animation/Animation.skiko.kt @@ -27,6 +27,10 @@ internal fun easeInOutTimingFunction(progress: Float): Float = if (progress < 0. (-2f * progress * progress) + (4f * progress) - 1f } +internal fun easeOutTimingFunction(progress: Float): Float { + return -progress * (progress - 2f) +} + internal suspend fun withAnimationProgress( duration: Duration, timingFunction: (Float) -> Float = ::easeInOutTimingFunction, diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/ComposeSceneLayer.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/ComposeSceneLayer.skiko.kt index deff13e81ff68..69608ef25e968 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/ComposeSceneLayer.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/ComposeSceneLayer.skiko.kt @@ -196,11 +196,6 @@ internal fun rememberComposeSceneLayer( layer.density = density layer.layoutDirection = layoutDirection - DisposableEffect(Unit) { - onDispose { - layer.close() - } - } return layer } diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/window/Dialog.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/window/Dialog.skiko.kt index bcf9413ea920f..63bc2b232ae25 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/window/Dialog.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/window/Dialog.skiko.kt @@ -19,14 +19,22 @@ package androidx.compose.ui.window import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Immutable -import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCompositionContext import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.MotionDurationScale +import androidx.compose.ui.animation.easeOutTimingFunction +import androidx.compose.ui.animation.withAnimationProgress import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.GraphicsLayerScope +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.PointerButton import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.layout.Layout @@ -44,12 +52,19 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.center +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.launch /** * The default scrim opacity. */ private const val DefaultScrimOpacity = 0.6f private val DefaultScrimColor = Color.Black.copy(alpha = DefaultScrimOpacity) +private const val AnimatedLayerOffsetDp = 10f +private const val AnimatedLayerInitialAlpha = 0.0f +private const val AnimatedLayerScale = 0.05f /** * Properties used to customize the behavior of a [Dialog]. @@ -166,13 +181,31 @@ private fun DialogLayout( content: @Composable () -> Unit ) { val currentContent by rememberUpdatedState(content) - + val compositionContext = rememberCompositionContext() + var graphicsLayerScopeUpdate by remember { + mutableStateOf Unit>({ alpha = 0f }) + } val layer = rememberComposeSceneLayer( focusable = true ) - layer.scrimColor = properties.scrimColor layer.setOutsidePointerEventListener(onOutsidePointerEvent) + val dialogAnimationScope = remember { + object : DialogAnimationScope { + override fun graphicsLayer(modify: GraphicsLayerScope.() -> Unit) { + graphicsLayerScopeUpdate = modify + } + + override var scrimColor: Color + get() = layer.scrimColor ?: properties.scrimColor + set(value) { layer.scrimColor = value } + } + } layer.Content { + LaunchedEffect(Unit) { + dialogAnimationScope.defaultDialogAppearEffect() + graphicsLayerScopeUpdate = { alpha = 1f } + layer.scrimColor = properties.scrimColor + } val platformInsets = properties.platformInsets val containerSize = LocalWindowInfo.current.containerSize val measurePolicy = rememberDialogMeasurePolicy( @@ -188,11 +221,90 @@ private fun DialogLayout( ) { Layout( content = currentContent, - modifier = modifier, + modifier = Modifier.graphicsLayer(graphicsLayerScopeUpdate).then(modifier), measurePolicy = measurePolicy ) } } + + DisposableEffect(Unit) { + onDispose { + CoroutineScope(compositionContext.effectCoroutineContext).launch { + dialogAnimationScope.defaultDialogDisappearEffect() + layer.close() + } + } + } +} + +/** + * Represents an animation scope for dialogs, allowing customization of dialog animations + * and associated visual properties. + * + * This interface provides methods to apply transformations and modify visual properties + * during dialog animations. + */ +@Immutable +private interface DialogAnimationScope { + /** + * Applies graphics layer transformations and customizations within the specified [GraphicsLayerScope]. + * This method can be used to modify visual properties like rotation, scale, translation, + * shadow, and other effects. + * + * @param modify A lambda receiver of [GraphicsLayerScope] allowing customization of graphics layer properties. + */ + fun graphicsLayer(modify: GraphicsLayerScope.() -> Unit) + + /** + * Defines the color of the scrim used during dialog animations. + * + * The scrim is a semi-transparent layer displayed behind the dialog to + * focus the user's attention on the foreground content. This property + * allows customization of the scrim's appearance to match desired visual + * aesthetics or themes. + */ + var scrimColor: Color +} + +private suspend fun durationScale(): Float { + return currentCoroutineContext()[MotionDurationScale]?.scaleFactor ?: 1f +} + +private suspend fun DialogAnimationScope.defaultDialogAppearEffect() { + val initialScrimColor = this.scrimColor + withAnimationProgress( + duration = (durationScale() * 0.2).seconds, + timingFunction = ::easeOutTimingFunction + ) { progress -> + val animatedAlpha = AnimatedLayerInitialAlpha + progress * (1f - AnimatedLayerInitialAlpha) + this.scrimColor = initialScrimColor.copy(initialScrimColor.alpha * animatedAlpha) + this.graphicsLayer { + this.alpha = animatedAlpha + val scale = 1f + (progress - 1f) * AnimatedLayerScale + this.scaleX = scale + this.scaleY = scale + this.translationY = (AnimatedLayerOffsetDp * (1f - progress)) * density + } + } +} + +private suspend fun DialogAnimationScope.defaultDialogDisappearEffect() { + val initialScrimColor = this.scrimColor + withAnimationProgress( + duration = (durationScale() * 0.15).seconds, + timingFunction = ::easeOutTimingFunction + ) { progress -> + val animatedAlpha = + AnimatedLayerInitialAlpha + (1f - progress) * (1f - AnimatedLayerInitialAlpha) + this.scrimColor = initialScrimColor.copy(initialScrimColor.alpha * animatedAlpha) + this.graphicsLayer { + this.alpha = animatedAlpha + val scale = 1f - progress * AnimatedLayerScale + this.scaleX = scale + this.scaleY = scale + this.translationY = AnimatedLayerOffsetDp * progress * density + } + } } private val DialogProperties.platformInsets: PlatformInsets diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/window/Popup.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/window/Popup.skiko.kt index 7ff6145a38404..43066c66f5f6d 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/window/Popup.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/window/Popup.skiko.kt @@ -472,6 +472,12 @@ private fun PopupLayout( ) } } + + DisposableEffect(Unit) { + onDispose { + layer.close() + } + } } private val PopupProperties.platformInsets: PlatformInsets