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 d093eca656017..ba1c0488221d7 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 @@ -471,10 +471,10 @@ private fun PopupLayout( content: @Composable () -> Unit ) { // Use a MutableState directly to avoid recomposing when the value changes - val parentBoundsInWindow: MutableState = remember { mutableStateOf(IntRect.Zero) } + val parentBoundsInWindow: MutableState = remember { mutableStateOf(null) } EmptyLayout(Modifier.onPlaced { childCoordinates -> - // This will be called before the popup measure policy is actually asked to calculate - // the popup's position, so it will never see the initial value of IntRect.Zero + // For a layer in the same scene, this runs before its popup measure policy calculates a + // position, so the policy observes the parent bounds in the first frame. childCoordinates.parentCoordinates?.let { // Nodes which read layout coordinates (including, e.g., positionInWindow) in // layout/placement get invalidated when these coordinates change @@ -541,7 +541,7 @@ private fun rememberPopupMeasurePolicy( containerSize: IntSize, platformInsets: PlatformInsets, layoutDirection: LayoutDirection, - parentBoundsInWindow: MutableState + parentBoundsInWindow: MutableState ) = remember( layer, popupPositionProvider, @@ -556,6 +556,11 @@ private fun rememberPopupMeasurePolicy( usePlatformDefaultWidth = properties.usePlatformDefaultWidth ) { contentSize -> val parentRectInWindow = parentBoundsInWindow.value + ?: run { + // Keep an unanchored layer out of the visible window while its content is measured. + layer.boundsInWindow = IntRect.Zero + return@ComposeSceneLayerMeasurePolicy IntOffset.Zero + } val positionWithInsets = positionWithInsets(platformInsets, containerSize) { sizeWithoutInsets -> // Position provider works in coordinates without insets. diff --git a/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/layers/LayersRenderingTest.kt b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/layers/LayersRenderingTest.kt index d8c82aa25e118..b634de35f816b 100644 --- a/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/layers/LayersRenderingTest.kt +++ b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/layers/LayersRenderingTest.kt @@ -18,13 +18,14 @@ package androidx.compose.ui.layers import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.background -import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.test.captureScreenshot import androidx.compose.ui.test.runUIKitInstrumentedTest import androidx.compose.ui.test.utils.forEachPixel @@ -33,38 +34,30 @@ import androidx.compose.ui.window.PopupProperties import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull -import platform.UIKit.UIImage -import platform.darwin.dispatch_async -import platform.darwin.dispatch_get_main_queue class LayersRenderingTest { @Test - fun testLayerContentOnFirstRender() = runUIKitInstrumentedTest { + fun testLayerContentAfterParentAnchorIsAvailable() = runUIKitInstrumentedTest { var showRed by mutableStateOf(false) var showGreen by mutableStateOf(false) - var onRender = {} - var frameImage: UIImage? = null - - fun prepareForCaptureNextFrame() { - frameImage = null - onRender = { - dispatch_async(dispatch_get_main_queue()) { - frameImage = captureScreenshot() - } - onRender = {} - } - } + var popupContentPlaced by mutableStateOf(false) setContent { - Box(Modifier.fillMaxSize().background(Color.Blue).drawBehind { - onRender() - }) + Box(Modifier.fillMaxSize().background(Color.Blue)) if (showRed) { Popup( onDismissRequest = {}, properties = PopupProperties(usePlatformInsets = false) ) { - Box(Modifier.fillMaxSize().background(Color.Red)) + DisposableEffect(Unit) { + onDispose { popupContentPlaced = false } + } + Box( + Modifier + .fillMaxSize() + .background(Color.Red) + .onPlaced { popupContentPlaced = true } + ) } } if (showGreen) { @@ -72,17 +65,22 @@ class LayersRenderingTest { onDismissRequest = {}, properties = PopupProperties(usePlatformInsets = false) ) { - Box(Modifier.fillMaxSize().background(Color.Green)) + DisposableEffect(Unit) { + onDispose { popupContentPlaced = false } + } + Box( + Modifier + .fillMaxSize() + .background(Color.Green) + .onPlaced { popupContentPlaced = true } + ) } } } - fun assertNextFrameColor(expectedColor: Color) { - prepareForCaptureNextFrame() + fun assertFrameColor(expectedColor: Color) { waitForIdle() - - assertNotNull(frameImage) - frameImage!!.forEachPixel(step = 4) { _, _, actualColor -> + assertNotNull(captureScreenshot()).forEachPixel(step = 4) { _, _, actualColor -> assertEquals( expectedColor, actualColor, @@ -91,16 +89,33 @@ class LayersRenderingTest { } } + fun awaitPopupContentPlacement() { + waitUntil("Popup content should be placed") { popupContentPlaced } + waitForIdle() + } + + fun awaitPopupContentDisposal() { + waitUntil("Popup content should be disposed") { !popupContentPlaced } + waitForIdle() + } + + // IosComposeSceneLayer owns a separate ComposeScene. Its first layout is not ordered + // after the parent scene's onPlaced callback, so this test only asserts the result once + // UIKit has processed both scenes, rather than requiring the popup in a particular frame. showRed = true - assertNextFrameColor(Color.Red) + awaitPopupContentPlacement() + assertFrameColor(Color.Red) showRed = false - assertNextFrameColor(Color.Blue) + awaitPopupContentDisposal() + assertFrameColor(Color.Blue) showGreen = true - assertNextFrameColor(Color.Green) + awaitPopupContentPlacement() + assertFrameColor(Color.Green) showGreen = false - assertNextFrameColor(Color.Blue) + awaitPopupContentDisposal() + assertFrameColor(Color.Blue) } }