Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import androidx.compose.ui.test.junit4.DesktopScreenshotTestRule
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.time.ExperimentalTime
import kotlinx.coroutines.runBlocking
Expand Down Expand Up @@ -94,13 +95,17 @@ class ImageComposeSceneTest {

@Test
fun `run dialog in center`() {
val image = renderComposeScene(
val image = ImageComposeScene(
width = 80,
height = 40,
) {
Dialog(onDismissRequest = {}) {
Box(Modifier.size(20.dp).background(Color.Red))
}
}.use {
it.render()
// Skip animation for the Dialog appearance
it.render(500.milliseconds)
}
screenshotRule.assertImageAgainstGolden(image)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@
package androidx.compose.ui

import kotlin.jvm.JvmField
import kotlin.jvm.JvmName

internal object SkikoComposeUiFlags {
@Suppress("MutableBareField")
@JvmField
var useLegacyRenderNodeLayers: Boolean = false

@Suppress("MutableBareField")
@JvmField
var isDialogAnimationEnabled: Boolean = true
}

/**
Expand All @@ -33,3 +36,11 @@ internal object SkikoComposeUiFlags {
*/
@ExperimentalComposeUiApi
var ComposeUiFlags.useLegacyRenderNodeLayers by SkikoComposeUiFlags::useLegacyRenderNodeLayers

/**
* When enabled the [androidx.compose.ui.window.Dialog] appear and disappear with animation.
*
* Note that it's a temporary flag, it will be removed in the future.
*/
@ExperimentalComposeUiApi
var ComposeUiFlags.isDialogAnimationEnabled by SkikoComposeUiFlags::isDialogAnimationEnabled
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,22 @@ 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,
update: (Float) -> Unit
) {
update(0f)

var firstFrameTime = 0L
var firstFrameTime: Long? = null
var progressDuration = Duration.ZERO
while (progressDuration < duration) {
withFrameNanos { frameTime ->
if (firstFrameTime == 0L) {
if (firstFrameTime == null) {
firstFrameTime = frameTime
}
progressDuration = (frameTime - firstFrameTime).nanoseconds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,6 @@ internal fun rememberComposeSceneLayer(
layer.density = density
layer.layoutDirection = layoutDirection

DisposableEffect(Unit) {
onDispose {
layer.close()
}
}
return layer
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,28 @@ 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.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCompositionContext
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.ComposeUiFlags
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.isDialogAnimationEnabled
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalPlatformWindowInsets
import androidx.compose.ui.platform.LocalWindowInfo
Expand All @@ -44,12 +56,21 @@ 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.CoroutineStart
import kotlinx.coroutines.cancel
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.2f
private const val AnimatedLayerScale = 0.05f

/**
* Properties used to customize the behavior of a [Dialog].
Expand Down Expand Up @@ -166,13 +187,39 @@ private fun DialogLayout(
content: @Composable () -> Unit
) {
val currentContent by rememberUpdatedState(content)

val layer = rememberComposeSceneLayer(
focusable = true
)
layer.scrimColor = properties.scrimColor
val compositionContext = rememberCompositionContext()
val appearanceProgress = remember { mutableStateOf(0f) }
var graphicsLayerScopeUpdate by remember {
mutableStateOf(animationLayerTransform(appearanceProgress))
}
val layer = rememberComposeSceneLayer(focusable = true)
layer.setOutsidePointerEventListener(onOutsidePointerEvent)

fun updateScrimAlpha() {
val scrimAlpha =
AnimatedLayerInitialAlpha + appearanceProgress.value * (1f - AnimatedLayerInitialAlpha)
layer.scrimColor = properties.scrimColor.copy(properties.scrimColor.alpha * scrimAlpha)
}

val appearanceScope = CoroutineScope(rememberCoroutineScope().coroutineContext)
layer.Content {
LaunchedEffect(Unit) {
appearanceScope.launch(start = CoroutineStart.UNDISPATCHED) {
if (ComposeUiFlags.isDialogAnimationEnabled) {
withAnimationProgress(
duration = (durationScale() * 0.2).seconds,
timingFunction = ::easeOutTimingFunction
) { progress ->
appearanceProgress.value = progress
updateScrimAlpha()
}
}

graphicsLayerScopeUpdate = { alpha = 1f }
layer.scrimColor = properties.scrimColor
}
}

val platformInsets = properties.platformInsets
val containerSize = LocalWindowInfo.current.containerSize
val measurePolicy = rememberDialogMeasurePolicy(
Expand All @@ -188,13 +235,53 @@ private fun DialogLayout(
) {
Layout(
content = currentContent,
modifier = modifier,
modifier = Modifier.graphicsLayer(graphicsLayerScopeUpdate).then(modifier),
measurePolicy = measurePolicy
)
}
}

DisposableEffect(Unit) {
onDispose {
appearanceScope.cancel()
if (ComposeUiFlags.isDialogAnimationEnabled) {
CoroutineScope(compositionContext.effectCoroutineContext)
.launch(start = CoroutineStart.UNDISPATCHED) {
val initialProgress = appearanceProgress.value
graphicsLayerScopeUpdate = animationLayerTransform(appearanceProgress)

withAnimationProgress(
duration = (durationScale() * 0.1).seconds,
timingFunction = ::easeOutTimingFunction
) { progress ->
appearanceProgress.value = (1f - progress) * initialProgress
updateScrimAlpha()
}

layer.close()
}
} else {
layer.close()
}
}
}
}

private suspend fun durationScale(): Float {
return currentCoroutineContext()[MotionDurationScale]?.scaleFactor ?: 1f
}

private fun animationLayerTransform(progress: State<Float>): GraphicsLayerScope.() -> Unit =
{
this.alpha = AnimatedLayerInitialAlpha + (1f - AnimatedLayerInitialAlpha) * progress.value

val reversedProgress = 1f - progress.value
val scale = 1f - reversedProgress * AnimatedLayerScale
this.scaleX = scale
this.scaleY = scale
this.translationY = AnimatedLayerOffsetDp * reversedProgress * density
}

private val DialogProperties.platformInsets: PlatformInsets
@Composable get() {
val safeInsets = if (usePlatformInsets) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,12 @@ private fun PopupLayout(
)
}
}

DisposableEffect(Unit) {
onDispose {
layer.close()
}
}
}

private val PopupProperties.platformInsets: PlatformInsets
Expand Down
Loading