-
Notifications
You must be signed in to change notification settings - Fork 98
Implement enter end exit animation for Dialog #2374
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: jb-main
Are you sure you want to change the base?
Changes from all commits
c8093a1
50fac7b
c3d0949
a4c8084
c979384
6a9351b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,14 +17,23 @@ | |
package androidx.compose.ui.window | ||
|
||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.DisposableEffect | ||
import androidx.compose.runtime.Immutable | ||
import androidx.compose.runtime.LaunchedEffect | ||
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.ExperimentalComposeUiApi | ||
import androidx.compose.ui.Modifier | ||
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.key.Key | ||
import androidx.compose.ui.input.key.KeyEvent | ||
import androidx.compose.ui.input.key.KeyEventType | ||
|
@@ -46,12 +55,50 @@ 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.launch | ||
|
||
/** | ||
* The default scrim opacity. | ||
*/ | ||
private const val DefaultScrimOpacity = 0.6f | ||
private const val DefaultScrimOpacity = 0.78f | ||
private val DefaultScrimColor = Color.Black.copy(alpha = DefaultScrimOpacity) | ||
private const val AnimatedLayerAppearanceOffsetDp = 16f | ||
private const val AnimatedLayerDisappearanceOffsetDp = 8f | ||
private const val AnimatedLayerInitialAlphaProgress = 0.6f | ||
|
||
/** | ||
* 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. | ||
* | ||
* Note: This API is experimental and may change in the future. | ||
*/ | ||
@ExperimentalComposeUiApi | ||
@Immutable | ||
interface DialogAnimationScope { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it's a good idea to introduce such API not in We can still add another boolean flag to |
||
/** | ||
* 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 | ||
} | ||
|
||
/** | ||
* Properties used to customize the behavior of a [Dialog]. | ||
|
@@ -68,6 +115,8 @@ private val DefaultScrimColor = Color.Black.copy(alpha = DefaultScrimOpacity) | |
* @property useSoftwareKeyboardInset Whether the size of the dialog's content should be limited by | ||
* software keyboard inset. | ||
* @property scrimColor Color of background fill. | ||
* @property onAppearEffect The effect to be applied when the dialog appears. | ||
* @property onDisappearEffect The effect to be applied when the dialog disappears. | ||
*/ | ||
@Immutable | ||
actual class DialogProperties @ExperimentalComposeUiApi constructor( | ||
|
@@ -77,6 +126,12 @@ actual class DialogProperties @ExperimentalComposeUiApi constructor( | |
val usePlatformInsets: Boolean = true, | ||
val useSoftwareKeyboardInset: Boolean = true, | ||
val scrimColor: Color = DefaultScrimColor, | ||
@property:ExperimentalComposeUiApi | ||
val onAppearEffect: suspend DialogAnimationScope.() -> Unit = | ||
DialogAnimationScope::defaultDialogAppearEffect, | ||
@property:ExperimentalComposeUiApi | ||
val onDisappearEffect: suspend DialogAnimationScope.() -> Unit = | ||
DialogAnimationScope::defaultDialogDisappearEffect, | ||
) { | ||
actual constructor( | ||
dismissOnBackPress: Boolean, | ||
|
@@ -91,6 +146,25 @@ actual class DialogProperties @ExperimentalComposeUiApi constructor( | |
scrimColor = DefaultScrimColor, | ||
) | ||
|
||
@ExperimentalComposeUiApi | ||
constructor( | ||
dismissOnBackPress: Boolean = true, | ||
dismissOnClickOutside: Boolean = true, | ||
usePlatformDefaultWidth: Boolean = true, | ||
usePlatformInsets: Boolean = true, | ||
useSoftwareKeyboardInset: Boolean = true, | ||
scrimColor: Color = DefaultScrimColor, | ||
) : this( | ||
dismissOnBackPress = dismissOnBackPress, | ||
dismissOnClickOutside = dismissOnClickOutside, | ||
usePlatformDefaultWidth = usePlatformDefaultWidth, | ||
usePlatformInsets = usePlatformInsets, | ||
useSoftwareKeyboardInset = useSoftwareKeyboardInset, | ||
scrimColor = scrimColor, | ||
onAppearEffect = DialogAnimationScope::defaultDialogAppearEffect, | ||
onDisappearEffect = DialogAnimationScope::defaultDialogDisappearEffect, | ||
) | ||
|
||
override fun equals(other: Any?): Boolean { | ||
if (this === other) return true | ||
if (other !is DialogProperties) return false | ||
|
@@ -169,14 +243,30 @@ private fun DialogLayout( | |
content: @Composable () -> Unit | ||
) { | ||
val currentContent by rememberUpdatedState(content) | ||
|
||
val compositionContext = rememberCompositionContext() | ||
var graphicsLayerScopeUpdate by remember { mutableStateOf<GraphicsLayerScope.() -> Unit>({}) } | ||
val layer = rememberComposeSceneLayer( | ||
focusable = true | ||
) | ||
layer.scrimColor = properties.scrimColor | ||
layer.setKeyEventListener(onPreviewKeyEvent, onKeyEvent) | ||
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) { | ||
properties.onAppearEffect(dialogAnimationScope) | ||
graphicsLayerScopeUpdate = {} | ||
layer.scrimColor = properties.scrimColor | ||
} | ||
val platformInsets = properties.platformInsets | ||
val containerSize = LocalWindowInfo.current.containerSize | ||
val measurePolicy = rememberDialogMeasurePolicy( | ||
|
@@ -192,11 +282,45 @@ private fun DialogLayout( | |
) { | ||
Layout( | ||
content = currentContent, | ||
modifier = modifier, | ||
modifier = Modifier.graphicsLayer(graphicsLayerScopeUpdate).then(modifier), | ||
measurePolicy = measurePolicy | ||
) | ||
} | ||
} | ||
|
||
DisposableEffect(Unit) { | ||
onDispose { | ||
CoroutineScope(compositionContext.effectCoroutineContext).launch { | ||
properties.onDisappearEffect(dialogAnimationScope) | ||
layer.close() | ||
} | ||
} | ||
} | ||
} | ||
|
||
private suspend fun DialogAnimationScope.defaultDialogAppearEffect() { | ||
val initialScrimColor = this.scrimColor | ||
withAnimationProgress(0.15.seconds, timingFunction = ::easeOutTimingFunction) { progress -> | ||
val animatedAlpha = | ||
AnimatedLayerInitialAlphaProgress + progress * (1f - AnimatedLayerInitialAlphaProgress) | ||
this.scrimColor = initialScrimColor.copy(initialScrimColor.alpha * animatedAlpha) | ||
this.graphicsLayer { | ||
this.alpha = animatedAlpha | ||
this.translationY = (AnimatedLayerAppearanceOffsetDp * (1f - progress)) * density | ||
} | ||
} | ||
} | ||
|
||
private suspend fun DialogAnimationScope.defaultDialogDisappearEffect() { | ||
val initialScrimColor = this.scrimColor | ||
withAnimationProgress(0.10.seconds, timingFunction = ::easeOutTimingFunction) { progress -> | ||
val animatedAlpha = 1f - progress * AnimatedLayerInitialAlphaProgress | ||
this.scrimColor = initialScrimColor.copy(initialScrimColor.alpha * animatedAlpha) | ||
this.graphicsLayer { | ||
this.alpha = animatedAlpha | ||
this.translationY = -AnimatedLayerDisappearanceOffsetDp * progress * density | ||
} | ||
} | ||
} | ||
|
||
private val DialogProperties.platformInsets: PlatformInsets | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert default value here - it's picked from Android OS sources. It should be the same by default across the platforms