Merge "Request WindowInsets when they are first needed." into androidx-main
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsActivity.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsActivity.kt
index 41e064f..e9bf1a3 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsActivity.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsActivity.kt
@@ -19,6 +19,7 @@
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.annotation.RequiresApi
+import androidx.core.view.WindowCompat
import java.util.concurrent.CountDownLatch
class WindowInsetsActivity : ComponentActivity() {
@@ -26,7 +27,7 @@
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreate(savedInstanceState: Bundle?) {
- window.setDecorFitsSystemWindows(false)
+ WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
createdLatch.countDown()
}
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsDeviceTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsDeviceTest.kt
index a35618c..8d50d89 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsDeviceTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsDeviceTest.kt
@@ -17,7 +17,14 @@
import android.os.Build
import android.os.SystemClock
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@@ -28,25 +35,42 @@
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.graphics.Insets
+import androidx.core.view.OnApplyWindowInsetsListener
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
+import androidx.core.view.children
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@MediumTest
@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
class WindowInsetsDeviceTest {
@get:Rule
val rule = createAndroidComposeRule<WindowInsetsActivity>()
+ @Before
+ fun setup() {
+ rule.activity.createdLatch.await(1, TimeUnit.SECONDS)
+ }
+
@OptIn(ExperimentalLayoutApi::class)
@Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
fun disableConsumeDisablesAnimationConsumption() {
var imeInset1 = 0
var imeInset2 = 0
@@ -122,10 +146,194 @@
}
}
+ @RequiresApi(Build.VERSION_CODES.R)
private fun pokeIME() {
// This appears to be necessary for cuttlefish devices to show the keyboard
val controller = rule.activity.window.insetsController
controller?.show(android.view.WindowInsets.Type.ime())
controller?.hide(android.view.WindowInsets.Type.ime())
}
+
+ @Test
+ fun insetsUsedAfterInitialComposition() {
+ var useInsets by mutableStateOf(false)
+ var systemBarsInsets by mutableStateOf(Insets.NONE)
+
+ rule.setContent {
+ val view = LocalView.current
+ DisposableEffect(Unit) {
+ // Ensure that the system bars are shown
+ val window = rule.activity.window
+
+ @Suppress("RedundantNullableReturnType") // nullable on some versions
+ val controller: WindowInsetsControllerCompat? =
+ WindowCompat.getInsetsController(window, view)
+ controller?.show(WindowInsetsCompat.Type.systemBars())
+ onDispose { }
+ }
+ Box(Modifier.fillMaxSize()) {
+ if (useInsets) {
+ val systemBars = WindowInsets.systemBars
+ val density = LocalDensity.current
+ val left = systemBars.getLeft(density, LayoutDirection.Ltr)
+ val top = systemBars.getTop(density)
+ val right = systemBars.getRight(density, LayoutDirection.Ltr)
+ val bottom = systemBars.getBottom(density)
+ systemBarsInsets = Insets.of(left, top, right, bottom)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ useInsets = true
+ }
+
+ rule.runOnIdle {
+ assertThat(systemBarsInsets).isNotEqualTo(Insets.NONE)
+ }
+ }
+
+ @Test
+ fun insetsAfterStopWatching() {
+ var useInsets by mutableStateOf(true)
+ var hasStatusBarInsets = false
+
+ rule.setContent {
+ val view = LocalView.current
+ DisposableEffect(Unit) {
+ // Ensure that the status bars are shown
+ val window = rule.activity.window
+
+ @Suppress("RedundantNullableReturnType") // nullable on some versions
+ val controller: WindowInsetsControllerCompat? =
+ WindowCompat.getInsetsController(window, view)
+ controller?.hide(WindowInsetsCompat.Type.statusBars())
+ onDispose { }
+ }
+ Box(Modifier.fillMaxSize()) {
+ if (useInsets) {
+ val statusBars = WindowInsets.statusBars
+ val density = LocalDensity.current
+ val left = statusBars.getLeft(density, LayoutDirection.Ltr)
+ val top = statusBars.getTop(density)
+ val right = statusBars.getRight(density, LayoutDirection.Ltr)
+ val bottom = statusBars.getBottom(density)
+ hasStatusBarInsets = left != 0 || top != 0 || right != 0 || bottom != 0
+ }
+ }
+ }
+
+ rule.waitForIdle()
+
+ rule.waitUntil(1000) { !hasStatusBarInsets }
+
+ // disable watching the insets
+ rule.runOnIdle {
+ useInsets = false
+ }
+
+ val statusBarsWatcher = StatusBarsShowListener()
+
+ // show the insets while we're not watching
+ rule.runOnIdle {
+ ViewCompat.setOnApplyWindowInsetsListener(
+ rule.activity.window.decorView,
+ statusBarsWatcher
+ )
+ @Suppress("RedundantNullableReturnType")
+ val controller: WindowInsetsControllerCompat? = WindowCompat.getInsetsController(
+ rule.activity.window,
+ rule.activity.window.decorView
+ )
+ controller?.show(WindowInsetsCompat.Type.statusBars())
+ }
+
+ assertThat(statusBarsWatcher.latch.await(1, TimeUnit.SECONDS)).isTrue()
+
+ // Now look at the insets
+ rule.runOnIdle {
+ useInsets = true
+ }
+
+ rule.runOnIdle {
+ assertThat(hasStatusBarInsets).isTrue()
+ }
+ }
+
+ @Test
+ fun insetsAfterReattachingView() {
+ var hasStatusBarInsets = false
+
+ // hide the insets
+ rule.runOnUiThread {
+ @Suppress("RedundantNullableReturnType")
+ val controller: WindowInsetsControllerCompat? = WindowCompat.getInsetsController(
+ rule.activity.window,
+ rule.activity.window.decorView
+ )
+ controller?.hide(WindowInsetsCompat.Type.statusBars())
+ }
+
+ rule.setContent {
+ Box(Modifier.fillMaxSize()) {
+ val statusBars = WindowInsets.statusBars
+ val density = LocalDensity.current
+ val left = statusBars.getLeft(density, LayoutDirection.Ltr)
+ val top = statusBars.getTop(density)
+ val right = statusBars.getRight(density, LayoutDirection.Ltr)
+ val bottom = statusBars.getBottom(density)
+ hasStatusBarInsets = left != 0 || top != 0 || right != 0 || bottom != 0
+ }
+ }
+
+ rule.waitForIdle()
+
+ rule.waitUntil(1000) { !hasStatusBarInsets }
+
+ val contentView = rule.activity.findViewById<ViewGroup>(android.R.id.content)
+ val composeView = contentView.children.first()
+
+ // remove the view
+ rule.runOnUiThread {
+ contentView.removeView(composeView)
+ }
+
+ val statusBarsWatcher = StatusBarsShowListener()
+
+ // show the insets while we're not watching
+ rule.runOnUiThread {
+ ViewCompat.setOnApplyWindowInsetsListener(
+ rule.activity.window.decorView,
+ statusBarsWatcher
+ )
+ @Suppress("RedundantNullableReturnType")
+ val controller: WindowInsetsControllerCompat? = WindowCompat.getInsetsController(
+ rule.activity.window,
+ rule.activity.window.decorView
+ )
+ controller?.show(WindowInsetsCompat.Type.statusBars())
+ }
+
+ assertThat(statusBarsWatcher.latch.await(1, TimeUnit.SECONDS)).isTrue()
+
+ // Now add the view back again
+ rule.runOnUiThread {
+ contentView.addView(composeView)
+ }
+
+ rule.waitUntil(1000) { hasStatusBarInsets }
+ }
+
+ class StatusBarsShowListener : OnApplyWindowInsetsListener {
+ val latch = CountDownLatch(1)
+
+ override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
+ val statusBars = insets.getInsets(WindowInsetsCompat.Type.statusBars())
+ if (statusBars != Insets.NONE) {
+ latch.countDown()
+ ViewCompat.setOnApplyWindowInsetsListener(v, null)
+ }
+ return insets
+ }
+ }
}
diff --git a/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsets.android.kt b/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsets.android.kt
index eef1555..5f7031d 100644
--- a/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsets.android.kt
+++ b/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsets.android.kt
@@ -19,6 +19,7 @@
import androidx.core.graphics.Insets as AndroidXInsets
import android.os.Build
import android.view.View
+import android.view.View.OnAttachStateChangeListener
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
@@ -453,6 +454,11 @@
// add listeners
ViewCompat.setOnApplyWindowInsetsListener(view, insetsListener)
+ if (view.isAttachedToWindow) {
+ view.requestApplyInsets()
+ }
+ view.addOnAttachStateChangeListener(insetsListener)
+
// We don't need animation callbacks on earlier versions, so don't bother adding
// the listener. ViewCompat calls the animation callbacks superfluously.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
@@ -473,6 +479,7 @@
// remove listeners
ViewCompat.setOnApplyWindowInsetsListener(view, null)
ViewCompat.setWindowInsetsAnimationCallback(view, null)
+ view.removeOnAttachStateChangeListener(insetsListener)
}
}
@@ -616,9 +623,9 @@
private class InsetsListener(
val composeInsets: WindowInsetsHolder,
-) : Runnable, OnApplyWindowInsetsListener, WindowInsetsAnimationCompat.Callback(
+) : WindowInsetsAnimationCompat.Callback(
if (composeInsets.consumes) DISPATCH_MODE_STOP else DISPATCH_MODE_CONTINUE_ON_SUBTREE
-) {
+), Runnable, OnApplyWindowInsetsListener, OnAttachStateChangeListener {
/**
* When [android.view.WindowInsetsController.controlWindowInsetsAnimation] is called,
* the [onApplyWindowInsets] is called after [onPrepare] with the target size. We
@@ -697,4 +704,11 @@
}
}
}
+
+ override fun onViewAttachedToWindow(view: View) {
+ view.requestApplyInsets()
+ }
+
+ override fun onViewDetachedFromWindow(v: View) {
+ }
}