Merge "Pin savedState dependencies to alpha01" into androidx-main
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/navigation/routing/RoutingDemoModels.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/navigation/routing/RoutingDemoModels.java
index 248a8ea..71c7c36 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/navigation/routing/RoutingDemoModels.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/navigation/routing/RoutingDemoModels.java
@@ -23,8 +23,10 @@
import android.text.Spanned;
import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
import androidx.car.app.CarContext;
import androidx.car.app.CarToast;
+import androidx.car.app.annotations.ExperimentalCarApi;
import androidx.car.app.model.Action;
import androidx.car.app.model.ActionStrip;
import androidx.car.app.model.CarColor;
@@ -125,6 +127,7 @@
* Returns the action strip that contains a "bug report" button and "stop navigation" button.
*/
@NonNull
+ @OptIn(markerClass = ExperimentalCarApi.class)
public static ActionStrip getActionStrip(
@NonNull CarContext carContext, @NonNull OnClickListener onStopNavigation) {
return new ActionStrip.Builder()
@@ -147,6 +150,7 @@
new Action.Builder()
.setTitle("Stop")
.setOnClickListener(onStopNavigation)
+ .setFlags(Action.FLAG_IS_PERSISTENT)
.build())
.build();
}
diff --git a/car/app/app/api/public_plus_experimental_1.2.0-beta03.txt b/car/app/app/api/public_plus_experimental_1.2.0-beta03.txt
index bcf427c..c2b190a8 100644
--- a/car/app/app/api/public_plus_experimental_1.2.0-beta03.txt
+++ b/car/app/app/api/public_plus_experimental_1.2.0-beta03.txt
@@ -453,6 +453,7 @@
method public static String typeToString(int);
field public static final androidx.car.app.model.Action APP_ICON;
field public static final androidx.car.app.model.Action BACK;
+ field @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(5) public static final int FLAG_IS_PERSISTENT = 2; // 0x2
field @androidx.car.app.annotations.RequiresCarApi(4) public static final int FLAG_PRIMARY = 1; // 0x1
field public static final androidx.car.app.model.Action PAN;
field public static final int TYPE_APP_ICON = 65538; // 0x10002
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index bcf427c..c2b190a8 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -453,6 +453,7 @@
method public static String typeToString(int);
field public static final androidx.car.app.model.Action APP_ICON;
field public static final androidx.car.app.model.Action BACK;
+ field @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(5) public static final int FLAG_IS_PERSISTENT = 2; // 0x2
field @androidx.car.app.annotations.RequiresCarApi(4) public static final int FLAG_PRIMARY = 1; // 0x1
field public static final androidx.car.app.model.Action PAN;
field public static final int TYPE_APP_ICON = 65538; // 0x10002
diff --git a/car/app/app/src/main/java/androidx/car/app/model/Action.java b/car/app/app/src/main/java/androidx/car/app/model/Action.java
index 832ac5e..6f3b005 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/Action.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/Action.java
@@ -32,9 +32,11 @@
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RestrictTo;
import androidx.car.app.CarContext;
import androidx.car.app.annotations.CarProtocol;
+import androidx.car.app.annotations.ExperimentalCarApi;
import androidx.car.app.annotations.RequiresCarApi;
import androidx.car.app.model.constraints.CarIconConstraints;
import androidx.lifecycle.LifecycleOwner;
@@ -86,10 +88,12 @@
* @hide
*/
@RestrictTo(LIBRARY)
+ @OptIn(markerClass = ExperimentalCarApi.class)
@IntDef(
flag = true,
value = {
FLAG_PRIMARY,
+ FLAG_IS_PERSISTENT,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ActionFlag {
@@ -133,6 +137,13 @@
public static final int FLAG_PRIMARY = 1 << 0;
/**
+ * Indicates that this action will not fade in/out inside an {@link ActionStrip}.
+ */
+ @RequiresCarApi(5)
+ @ExperimentalCarApi
+ public static final int FLAG_IS_PERSISTENT = 1 << 1;
+
+ /**
* A standard action to show the app's icon.
*
* <p>This action is non-interactive.
diff --git a/compose/foundation/foundation-layout/api/current.txt b/compose/foundation/foundation-layout/api/current.txt
index a76c2c1..0e4b541 100644
--- a/compose/foundation/foundation-layout/api/current.txt
+++ b/compose/foundation/foundation-layout/api/current.txt
@@ -206,5 +206,67 @@
method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Spacer(androidx.compose.ui.Modifier modifier);
}
+ @androidx.compose.runtime.Stable public interface WindowInsets {
+ method public int getBottom(androidx.compose.ui.unit.Density density);
+ method public int getLeft(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection);
+ method public int getRight(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection);
+ method public int getTop(androidx.compose.ui.unit.Density density);
+ field public static final androidx.compose.foundation.layout.WindowInsets.Companion Companion;
+ }
+
+ public static final class WindowInsets.Companion {
+ }
+
+ public final class WindowInsetsKt {
+ method public static androidx.compose.foundation.layout.WindowInsets WindowInsets(optional int left, optional int top, optional int right, optional int bottom);
+ method public static androidx.compose.foundation.layout.WindowInsets WindowInsets(optional float left, optional float top, optional float right, optional float bottom);
+ method public static androidx.compose.foundation.layout.WindowInsets add(androidx.compose.foundation.layout.WindowInsets, androidx.compose.foundation.layout.WindowInsets insets);
+ method @androidx.compose.runtime.Composable public static androidx.compose.foundation.layout.PaddingValues asPaddingValues(androidx.compose.foundation.layout.WindowInsets);
+ method public static androidx.compose.foundation.layout.WindowInsets exclude(androidx.compose.foundation.layout.WindowInsets, androidx.compose.foundation.layout.WindowInsets insets);
+ method public static androidx.compose.foundation.layout.WindowInsets union(androidx.compose.foundation.layout.WindowInsets, androidx.compose.foundation.layout.WindowInsets insets);
+ }
+
+ public final class WindowInsetsPaddingKt {
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsPadding(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+ }
+
+ public final class WindowInsetsPadding_androidKt {
+ method public static androidx.compose.ui.Modifier captionBarPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier displayCutoutPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier imePadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier mandatorySystemGesturesPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier navigationBarsPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier safeContentPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier safeDrawingPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier safeGesturesPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier statusBarsPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier systemBarsPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier systemGesturesPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier waterfallPadding(androidx.compose.ui.Modifier);
+ }
+
+ public final class WindowInsetsSizeKt {
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsBottomHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsEndWidth(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsStartWidth(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsTopHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+ }
+
+ public final class WindowInsets_androidKt {
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getCaptionBar(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getDisplayCutout(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getIme(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getMandatorySystemGestures(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getNavigationBars(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSafeContent(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSafeDrawing(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSafeGestures(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getStatusBars(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSystemBars(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSystemGestures(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getTappableElement(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getWaterfall(androidx.compose.foundation.layout.WindowInsets.Companion);
+ }
+
}
diff --git a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
index 11582d2..359b8d2 100644
--- a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
@@ -209,5 +209,69 @@
method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Spacer(androidx.compose.ui.Modifier modifier);
}
+ @androidx.compose.runtime.Stable public interface WindowInsets {
+ method public int getBottom(androidx.compose.ui.unit.Density density);
+ method public int getLeft(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection);
+ method public int getRight(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection);
+ method public int getTop(androidx.compose.ui.unit.Density density);
+ field public static final androidx.compose.foundation.layout.WindowInsets.Companion Companion;
+ }
+
+ public static final class WindowInsets.Companion {
+ }
+
+ public final class WindowInsetsKt {
+ method public static androidx.compose.foundation.layout.WindowInsets WindowInsets(optional int left, optional int top, optional int right, optional int bottom);
+ method public static androidx.compose.foundation.layout.WindowInsets WindowInsets(optional float left, optional float top, optional float right, optional float bottom);
+ method public static androidx.compose.foundation.layout.WindowInsets add(androidx.compose.foundation.layout.WindowInsets, androidx.compose.foundation.layout.WindowInsets insets);
+ method @androidx.compose.runtime.Composable public static androidx.compose.foundation.layout.PaddingValues asPaddingValues(androidx.compose.foundation.layout.WindowInsets);
+ method public static androidx.compose.foundation.layout.WindowInsets exclude(androidx.compose.foundation.layout.WindowInsets, androidx.compose.foundation.layout.WindowInsets insets);
+ method public static androidx.compose.foundation.layout.WindowInsets union(androidx.compose.foundation.layout.WindowInsets, androidx.compose.foundation.layout.WindowInsets insets);
+ }
+
+ public final class WindowInsetsPaddingKt {
+ method @androidx.compose.foundation.layout.ExperimentalLayoutApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier consumedWindowInsets(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+ method @androidx.compose.foundation.layout.ExperimentalLayoutApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier consumedWindowInsets(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.PaddingValues paddingValues);
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsPadding(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+ }
+
+ public final class WindowInsetsPadding_androidKt {
+ method public static androidx.compose.ui.Modifier captionBarPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier displayCutoutPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier imePadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier mandatorySystemGesturesPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier navigationBarsPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier safeContentPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier safeDrawingPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier safeGesturesPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier statusBarsPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier systemBarsPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier systemGesturesPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier waterfallPadding(androidx.compose.ui.Modifier);
+ }
+
+ public final class WindowInsetsSizeKt {
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsBottomHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsEndWidth(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsStartWidth(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsTopHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+ }
+
+ public final class WindowInsets_androidKt {
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getCaptionBar(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getDisplayCutout(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getIme(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getMandatorySystemGestures(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getNavigationBars(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSafeContent(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSafeDrawing(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSafeGestures(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getStatusBars(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSystemBars(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSystemGestures(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getTappableElement(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getWaterfall(androidx.compose.foundation.layout.WindowInsets.Companion);
+ }
+
}
diff --git a/compose/foundation/foundation-layout/api/restricted_current.txt b/compose/foundation/foundation-layout/api/restricted_current.txt
index 9355fc6..6dc4d15 100644
--- a/compose/foundation/foundation-layout/api/restricted_current.txt
+++ b/compose/foundation/foundation-layout/api/restricted_current.txt
@@ -211,5 +211,67 @@
method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Spacer(androidx.compose.ui.Modifier modifier);
}
+ @androidx.compose.runtime.Stable public interface WindowInsets {
+ method public int getBottom(androidx.compose.ui.unit.Density density);
+ method public int getLeft(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection);
+ method public int getRight(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection);
+ method public int getTop(androidx.compose.ui.unit.Density density);
+ field public static final androidx.compose.foundation.layout.WindowInsets.Companion Companion;
+ }
+
+ public static final class WindowInsets.Companion {
+ }
+
+ public final class WindowInsetsKt {
+ method public static androidx.compose.foundation.layout.WindowInsets WindowInsets(optional int left, optional int top, optional int right, optional int bottom);
+ method public static androidx.compose.foundation.layout.WindowInsets WindowInsets(optional float left, optional float top, optional float right, optional float bottom);
+ method public static androidx.compose.foundation.layout.WindowInsets add(androidx.compose.foundation.layout.WindowInsets, androidx.compose.foundation.layout.WindowInsets insets);
+ method @androidx.compose.runtime.Composable public static androidx.compose.foundation.layout.PaddingValues asPaddingValues(androidx.compose.foundation.layout.WindowInsets);
+ method public static androidx.compose.foundation.layout.WindowInsets exclude(androidx.compose.foundation.layout.WindowInsets, androidx.compose.foundation.layout.WindowInsets insets);
+ method public static androidx.compose.foundation.layout.WindowInsets union(androidx.compose.foundation.layout.WindowInsets, androidx.compose.foundation.layout.WindowInsets insets);
+ }
+
+ public final class WindowInsetsPaddingKt {
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsPadding(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+ }
+
+ public final class WindowInsetsPadding_androidKt {
+ method public static androidx.compose.ui.Modifier captionBarPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier displayCutoutPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier imePadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier mandatorySystemGesturesPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier navigationBarsPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier safeContentPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier safeDrawingPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier safeGesturesPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier statusBarsPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier systemBarsPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier systemGesturesPadding(androidx.compose.ui.Modifier);
+ method public static androidx.compose.ui.Modifier waterfallPadding(androidx.compose.ui.Modifier);
+ }
+
+ public final class WindowInsetsSizeKt {
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsBottomHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsEndWidth(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsStartWidth(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+ method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier windowInsetsTopHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.WindowInsets insets);
+ }
+
+ public final class WindowInsets_androidKt {
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getCaptionBar(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getDisplayCutout(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getIme(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getMandatorySystemGestures(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getNavigationBars(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSafeContent(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSafeDrawing(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSafeGestures(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getStatusBars(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSystemBars(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getSystemGestures(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getTappableElement(androidx.compose.foundation.layout.WindowInsets.Companion);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static androidx.compose.foundation.layout.WindowInsets getWaterfall(androidx.compose.foundation.layout.WindowInsets.Companion);
+ }
+
}
diff --git a/compose/foundation/foundation-layout/build.gradle b/compose/foundation/foundation-layout/build.gradle
index 45de895..7a0b162 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -36,11 +36,12 @@
*/
api("androidx.annotation:annotation:1.1.0")
- api("androidx.compose.ui:ui:1.1.0-rc01")
+ api(project(":compose:ui:ui"))
api("androidx.compose.ui:ui-unit:1.1.0-rc01")
implementation("androidx.compose.runtime:runtime:1.1.0-rc01")
implementation("androidx.compose.ui:ui-util:1.0.0")
+ implementation("androidx.core:core:1.7.0")
implementation(libs.kotlinStdlibCommon)
testImplementation(libs.testRules)
@@ -88,6 +89,7 @@
androidMain.dependencies {
api("androidx.annotation:annotation:1.1.0")
+ implementation("androidx.core:core:1.7.0")
}
desktopMain.dependencies {
diff --git a/compose/foundation/foundation-layout/samples/build.gradle b/compose/foundation/foundation-layout/samples/build.gradle
index 2fcf57d..02cf445 100644
--- a/compose/foundation/foundation-layout/samples/build.gradle
+++ b/compose/foundation/foundation-layout/samples/build.gradle
@@ -36,6 +36,8 @@
implementation(project(":compose:runtime:runtime"))
implementation("androidx.compose.ui:ui:1.0.0")
implementation("androidx.compose.ui:ui-text:1.0.0")
+ implementation("androidx.core:core-ktx:1.7.0")
+ implementation("androidx.activity:activity-compose:1.4.0")
}
androidx {
diff --git a/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/WindowInsetsPaddingSample.kt b/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/WindowInsetsPaddingSample.kt
new file mode 100644
index 0000000..0b323e8
--- /dev/null
+++ b/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/WindowInsetsPaddingSample.kt
@@ -0,0 +1,358 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.layout.samples
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.captionBarPadding
+import androidx.compose.foundation.layout.consumedWindowInsets
+import androidx.compose.foundation.layout.displayCutoutPadding
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.ime
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.layout.mandatorySystemGesturesPadding
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeContentPadding
+import androidx.compose.foundation.layout.safeDrawingPadding
+import androidx.compose.foundation.layout.safeGesturesPadding
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.foundation.layout.systemGesturesPadding
+import androidx.compose.foundation.layout.union
+import androidx.compose.foundation.layout.waterfallPadding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.dp
+import androidx.core.view.WindowCompat
+
+@Sampled
+fun captionBarPaddingSample() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ Box(Modifier.captionBarPadding()) {
+ // app content
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+fun systemBarsPaddingSample() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ Box(Modifier.systemBarsPadding()) {
+ // app content
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+fun displayCutoutPaddingSample() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ Box(Modifier.background(Color.Blue).statusBarsPadding()) {
+ Box(Modifier.background(Color.Yellow).displayCutoutPadding()) {
+ // app content
+ }
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+fun statusBarsAndNavigationBarsPaddingSample() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ Box(Modifier.background(Color.Blue).statusBarsPadding()) {
+ Box(Modifier.background(Color.Green).navigationBarsPadding()) {
+ // app content
+ }
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+fun imePaddingSample() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ Box(Modifier.background(Color.Blue).systemBarsPadding()) {
+ Box(Modifier.imePadding()) {
+ // app content
+ }
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+fun waterfallPaddingSample() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ Box(Modifier.background(Color.Blue).systemBarsPadding()) {
+ // The app content shouldn't spill over the edges. They will be green.
+ Box(Modifier.background(Color.Green).waterfallPadding()) {
+ // app content
+ }
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+fun systemGesturesPaddingSample() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ Box(Modifier.background(Color.Blue).systemBarsPadding()) {
+ // The app content won't interfere with the system gestures area.
+ // It will just be white.
+ Box(Modifier.background(Color.White).systemGesturesPadding()) {
+ // app content
+ }
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+fun mandatorySystemGesturesPaddingSample() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ Box(Modifier.background(Color.Blue).systemBarsPadding()) {
+ // The app content won't interfere with the mandatory system gestures area.
+ // It will just be white.
+ Box(Modifier.background(Color.White).mandatorySystemGesturesPadding()) {
+ // app content
+ }
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+fun safeDrawingPaddingSample() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ Box(Modifier.background(Color.Black).systemBarsPadding()) {
+ // The app content won't have anything drawing over it, but all the
+ // background not in the status bars will be white.
+ Box(Modifier.background(Color.White).safeDrawingPadding()) {
+ // app content
+ }
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+fun safeGesturesPaddingSample() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ Box(Modifier.background(Color.Black).systemBarsPadding()) {
+ // The app content will only be drawn where there is no possible
+ // gesture confusion. The rest will be plain white
+ Box(Modifier.background(Color.White).safeGesturesPadding()) {
+ // app content
+ }
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+fun safeContentPaddingSample() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ Box(Modifier.background(Color.Black).systemBarsPadding()) {
+ // The app content will only be drawn where there is no possible
+ // gesture confusion and content will not be drawn over.
+ // The rest will be plain white
+ Box(Modifier.background(Color.White).safeContentPadding()) {
+ // app content
+ }
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+fun insetsPaddingSample() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ val insets = WindowInsets.systemBars.union(WindowInsets.ime)
+ Box(Modifier.background(Color.White).fillMaxSize().windowInsetsPadding(insets)) {
+ // app content
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalLayoutApi::class)
+@Sampled
+fun consumedInsetsPaddingSample() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ with(LocalDensity.current) {
+ val paddingValues = PaddingValues(horizontal = 20.dp)
+ Box(
+ Modifier
+ .padding(paddingValues)
+ .consumedWindowInsets(paddingValues)
+ ) {
+ // app content
+ }
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+fun insetsInt() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ // Make sure we are at least 10 pixels away from the top.
+ val insets = WindowInsets.statusBars.union(WindowInsets(top = 10))
+ Box(Modifier.windowInsetsPadding(insets)) {
+ // app content
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+fun insetsDp() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ // Make sure we are at least 10 DP away from the top.
+ val insets = WindowInsets.statusBars.union(WindowInsets(top = 10.dp))
+ Box(Modifier.windowInsetsPadding(insets)) {
+ // app content
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+fun paddingValuesSample() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ LazyColumn(
+ contentPadding = WindowInsets.navigationBars.asPaddingValues()
+ ) {
+ // items
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalLayoutApi::class)
+@Sampled
+fun consumedInsetsSample() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ Box(Modifier.padding(WindowInsets.navigationBars.asPaddingValues())) {
+ Box(Modifier.consumedWindowInsets(WindowInsets.navigationBars)) {
+ // app content
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/WindowInsetsSizeSample.kt b/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/WindowInsetsSizeSample.kt
new file mode 100644
index 0000000..6b875a8
--- /dev/null
+++ b/compose/foundation/foundation-layout/samples/src/main/java/androidx/compose/foundation/layout/samples/WindowInsetsSizeSample.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.layout.samples
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.annotation.Sampled
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.layout.windowInsetsEndWidth
+import androidx.compose.foundation.layout.windowInsetsStartWidth
+import androidx.compose.foundation.layout.windowInsetsTopHeight
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.core.view.WindowCompat
+
+@Sampled
+fun insetsStartWidthSample() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ Box(Modifier.fillMaxSize()) {
+ // Background for navigation bar at the start
+ Box(Modifier.windowInsetsStartWidth(WindowInsets.navigationBars)
+ .fillMaxHeight()
+ .align(Alignment.CenterStart)
+ .background(Color.Red)
+ )
+ // app content
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+fun insetsTopHeightSample() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ Box(Modifier.fillMaxSize()) {
+ // Background for status bar at the top
+ Box(Modifier.windowInsetsTopHeight(WindowInsets.statusBars)
+ .fillMaxWidth()
+ .align(Alignment.TopCenter)
+ .background(Color.Red)
+ )
+ // app content
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+fun insetsEndWidthSample() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ Box(Modifier.fillMaxSize()) {
+ // Background for navigation bar at the end
+ Box(Modifier.windowInsetsEndWidth(WindowInsets.navigationBars)
+ .fillMaxHeight()
+ .align(Alignment.CenterEnd)
+ .background(Color.Red)
+ )
+ // app content
+ }
+ }
+ }
+ }
+}
+
+@Sampled
+fun insetsBottomHeightSample() {
+ class SampleActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ super.onCreate(savedInstanceState)
+ setContent {
+ Box(Modifier.fillMaxSize()) {
+ // Background for navigation bar at the bottom
+ Box(Modifier.windowInsetsTopHeight(WindowInsets.navigationBars)
+ .fillMaxWidth()
+ .align(Alignment.BottomCenter)
+ .background(Color.Red)
+ )
+ // app content
+ }
+ }
+ }
+ }
+}
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsPaddingTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsPaddingTest.kt
new file mode 100644
index 0000000..02fc73a
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsPaddingTest.kt
@@ -0,0 +1,872 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.layout
+
+import android.graphics.Insets as FrameworkInsets
+import android.graphics.Rect as AndroidRect
+import android.view.WindowInsets as AndroidWindowInsets
+import android.content.Context
+import android.os.Build
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowInsetsAnimation
+import android.view.animation.LinearInterpolator
+import android.widget.FrameLayout
+import androidx.activity.ComponentActivity
+import androidx.annotation.RequiresApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.boundsInRoot
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.ViewRootForTest
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.round
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.graphics.Insets as AndroidXInsets
+import androidx.core.view.DisplayCutoutCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.forEach
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+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 kotlin.math.roundToInt
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class WindowInsetsPaddingTest {
+ @get:Rule
+ val rule = createAndroidComposeRule<ComponentActivity>()
+
+ private lateinit var insetsView: InsetsView
+
+ @Before
+ fun setup() {
+ WindowInsetsHolder.setUseTestInsets(true)
+ }
+
+ @After
+ fun teardown() {
+ WindowInsetsHolder.setUseTestInsets(false)
+ }
+
+ @Test
+ fun systemBarsPadding() {
+ testInsetsPadding(
+ WindowInsetsCompat.Type.systemBars(),
+ Modifier.systemBarsPadding()
+ )
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+ @Test
+ fun displayCutoutPadding() {
+ val coordinates = setInsetContent {
+ Modifier.displayCutoutPadding()
+ }
+
+ val (width, height) = rule.runOnIdle {
+ coordinates.boundsInRoot().bottomRight.round()
+ }
+
+ val insets = sendDisplayCutoutInsets(width, height)
+ insets.assertIsConsumed(WindowInsetsCompat.Type.displayCutout())
+
+ rule.runOnIdle {
+ val expectedRect = Rect(10f, 11f, width - 12f, height - 13f)
+ assertThat(coordinates.boundsInRoot()).isEqualTo(expectedRect)
+ }
+ }
+
+ private fun sendDisplayCutoutInsets(width: Int, height: Int): WindowInsetsCompat {
+ val centerWidth = width / 2
+ val centerHeight = height / 2
+
+ val left = AndroidRect(0, centerHeight, 10, centerHeight + 2)
+ val top = AndroidRect(centerWidth, 0, centerWidth + 2, 11)
+ val right = AndroidRect(width - 12, centerHeight, width, centerHeight + 2)
+ val bottom = AndroidRect(centerWidth, height - 13, centerWidth + 2, height)
+ val safeInsets = AndroidXInsets.of(10, 11, 12, 13)
+ val windowInsets = WindowInsetsCompat.Builder()
+ .setInsets(WindowInsetsCompat.Type.statusBars(), AndroidXInsets.of(0, 11, 0, 0))
+ .setInsets(WindowInsetsCompat.Type.displayCutout(), safeInsets)
+ .setDisplayCutout(
+ DisplayCutoutCompat(
+ safeInsets,
+ left,
+ top,
+ right,
+ bottom,
+ AndroidXInsets.of(1, 2, 3, 4)
+ )
+ )
+ .build()
+ return dispatchApplyWindowInsets(windowInsets)
+ }
+
+ @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.Q)
+ @Test
+ fun statusBarsPaddingApi21() {
+ testInsetsPadding(
+ WindowInsetsCompat.Type.statusBars(),
+ Modifier.statusBarsPadding()
+ ) { width, height ->
+ Rect(0f, 11f, width.toFloat(), height.toFloat())
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun statusBarsPaddingApi30() {
+ testInsetsPadding(
+ WindowInsetsCompat.Type.statusBars(),
+ Modifier.statusBarsPadding()
+ )
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun captionBarPadding() {
+ testInsetsPadding(
+ WindowInsetsCompat.Type.captionBar(),
+ Modifier.captionBarPadding()
+ )
+ }
+
+ @Test
+ fun navigationBarsPaddingLeft() {
+ testInsetsPadding(
+ WindowInsetsCompat.Type.navigationBars(),
+ Modifier.navigationBarsPadding(),
+ sentInsets = AndroidXInsets.of(10, 0, 0, 0)
+ ) { width, height ->
+ Rect(10f, 0f, width.toFloat(), height.toFloat())
+ }
+ }
+
+ @Test
+ fun navigationBarsPaddingRight() {
+ testInsetsPadding(
+ WindowInsetsCompat.Type.navigationBars(),
+ Modifier.navigationBarsPadding(),
+ sentInsets = AndroidXInsets.of(0, 0, 12, 0)
+ ) { width, height ->
+ Rect(0f, 0f, width - 12f, height.toFloat())
+ }
+ }
+
+ @Test
+ fun navigationBarsPaddingBottom() {
+ testInsetsPadding(
+ WindowInsetsCompat.Type.navigationBars(),
+ Modifier.navigationBarsPadding(),
+ sentInsets = AndroidXInsets.of(0, 0, 0, 13)
+ ) { width, height ->
+ Rect(0f, 0f, width.toFloat(), height - 13f)
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun navigationBarsPaddingApi30() {
+ testInsetsPadding(
+ WindowInsetsCompat.Type.navigationBars(),
+ Modifier.navigationBarsPadding()
+ )
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun insetsPaddingIme() = testInsetsPadding(WindowInsetsCompat.Type.ime()) {
+ Modifier.windowInsetsPadding(WindowInsets.ime)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun insetsPaddingDisplayCutout() = testInsetsPadding(WindowInsetsCompat.Type.displayCutout()) {
+ Modifier.windowInsetsPadding(WindowInsets.displayCutout)
+ }
+
+ @Test
+ fun insetsPaddingStatusBarsTop() = testInsetsPadding(
+ WindowInsetsCompat.Type.statusBars(),
+ sentInsets = AndroidXInsets.of(0, 10, 0, 0),
+ expected = { w, h -> Rect(0f, 10f, w.toFloat(), h.toFloat()) }
+ ) { Modifier.windowInsetsPadding(WindowInsets.statusBars) }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun insetsPaddingStatusBarsApi30() = testInsetsPadding(WindowInsetsCompat.Type.statusBars()) {
+ Modifier.windowInsetsPadding(WindowInsets.statusBars)
+ }
+
+ @Test
+ fun insetsPaddingSystemBars() = testInsetsPadding(WindowInsetsCompat.Type.systemBars()) {
+ Modifier.windowInsetsPadding(WindowInsets.systemBars)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+ @Test
+ fun insetsPaddingTappableElement() =
+ testInsetsPadding(WindowInsetsCompat.Type.tappableElement()) {
+ Modifier.windowInsetsPadding(WindowInsets.tappableElement)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun insetsPaddingCaptionBar() = testInsetsPadding(WindowInsetsCompat.Type.captionBar()) {
+ Modifier.windowInsetsPadding(WindowInsets.captionBar)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+ @Test
+ fun insetsPaddingMandatorySystemGestures() =
+ testInsetsPadding(WindowInsetsCompat.Type.mandatorySystemGestures()) {
+ Modifier.windowInsetsPadding(WindowInsets.mandatorySystemGestures)
+ }
+
+ @Test
+ fun insetsPaddingNavigationBarsLeft() =
+ testInsetsPadding(
+ WindowInsetsCompat.Type.navigationBars(),
+ sentInsets = AndroidXInsets.of(10, 0, 0, 0),
+ expected = { width, height -> Rect(10f, 0f, width.toFloat(), height.toFloat()) }
+ ) {
+ Modifier.windowInsetsPadding(WindowInsets.navigationBars)
+ }
+
+ @Test
+ fun insetsPaddingNavigationBarsRight() =
+ testInsetsPadding(
+ WindowInsetsCompat.Type.navigationBars(),
+ sentInsets = AndroidXInsets.of(0, 0, 10, 0),
+ expected = { width, height -> Rect(0f, 0f, width - 10f, height.toFloat()) }
+ ) {
+ Modifier.windowInsetsPadding(WindowInsets.navigationBars)
+ }
+
+ @Test
+ fun insetsPaddingNavigationBarsBottom() =
+ testInsetsPadding(
+ WindowInsetsCompat.Type.navigationBars(),
+ sentInsets = AndroidXInsets.of(0, 0, 0, 10),
+ expected = { width, height -> Rect(0f, 0f, width.toFloat(), height - 10f) }
+ ) {
+ Modifier.windowInsetsPadding(WindowInsets.navigationBars)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun insetsPaddingNavigationBarsApi30() =
+ testInsetsPadding(WindowInsetsCompat.Type.navigationBars()) {
+ Modifier.windowInsetsPadding(WindowInsets.navigationBars)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun insetsPaddingWaterfall() {
+ val coordinates = setInsetContent {
+ Modifier.windowInsetsPadding(WindowInsets.waterfall)
+ }
+
+ val (width, height) = rule.runOnIdle {
+ coordinates.boundsInRoot().bottomRight.round()
+ }
+
+ val insets = sendDisplayCutoutInsets(width, height)
+ insets.assertIsConsumed(WindowInsetsCompat.Type.displayCutout())
+
+ rule.runOnIdle {
+ val expectedRect = Rect(1f, 2f, width - 3f, height - 4f)
+ assertThat(coordinates.boundsInRoot()).isEqualTo(expectedRect)
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+ @Test
+ fun insetsPaddingSystemGestures() =
+ testInsetsPadding(WindowInsetsCompat.Type.systemGestures()) {
+ Modifier.windowInsetsPadding(WindowInsets.systemGestures)
+ }
+
+ @Test
+ fun mixedInsetsPadding() {
+ val coordinates = setInsetContent {
+ val windowInsets = WindowInsets
+ val insets =
+ windowInsets.navigationBars.union(windowInsets.statusBars).union(windowInsets.ime)
+ Modifier.windowInsetsPadding(insets)
+ }
+
+ val insets = WindowInsetsCompat.Builder()
+ .setInsets(WindowInsetsCompat.Type.navigationBars(), AndroidXInsets.of(0, 0, 0, 15))
+ .setInsets(WindowInsetsCompat.Type.statusBars(), AndroidXInsets.of(0, 10, 0, 0))
+ .setInsets(WindowInsetsCompat.Type.ime(), AndroidXInsets.of(0, 0, 0, 5))
+ .build()
+
+ dispatchApplyWindowInsets(insets)
+
+ rule.runOnIdle {
+ val view = insetsView.findComposeView()
+ val width = view.width
+ val height = view.height
+ assertThat(coordinates.boundsInRoot())
+ .isEqualTo(Rect(0f, 10f, width.toFloat(), height - 15f))
+ }
+ }
+
+ @OptIn(ExperimentalLayoutApi::class)
+ @Test
+ fun consumedInsets() {
+ lateinit var coordinates: LayoutCoordinates
+
+ setContent {
+ with(LocalDensity.current) {
+ CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
+ Box(
+ Modifier.fillMaxSize().padding(5.toDp(), 4.toDp(), 3.toDp(), 2.toDp())
+ .consumedWindowInsets(WindowInsets(5, 4, 3, 2))
+ ) {
+ Box(Modifier.fillMaxSize().systemBarsPadding()) {
+ Box(Modifier.fillMaxSize().onGloballyPositioned { coordinates = it })
+ }
+ }
+ }
+ }
+ }
+
+ val insets = WindowInsetsCompat.Builder()
+ .setInsets(WindowInsetsCompat.Type.systemBars(), AndroidXInsets.of(10, 11, 12, 13))
+ .build()
+
+ dispatchApplyWindowInsets(insets)
+
+ rule.runOnIdle {
+ val view = insetsView.findComposeView()
+ val width = view.width
+ val height = view.height
+ assertThat(coordinates.boundsInRoot())
+ .isEqualTo(Rect(10f, 11f, width - 12f, height - 13f))
+ }
+ }
+
+ @Test
+ fun consumedPadding() {
+ lateinit var coordinates: LayoutCoordinates
+
+ setContent {
+ CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
+ Box(Modifier.statusBarsPadding()) {
+ Box(Modifier.systemBarsPadding()) {
+ Box(Modifier.fillMaxSize().onGloballyPositioned { coordinates = it })
+ }
+ }
+ }
+ }
+
+ // wait for layout
+ rule.waitForIdle()
+
+ val insets = WindowInsetsCompat.Builder()
+ .setInsets(WindowInsetsCompat.Type.statusBars(), AndroidXInsets.of(0, 5, 0, 0))
+ .setInsets(WindowInsetsCompat.Type.systemBars(), AndroidXInsets.of(10, 11, 12, 13))
+ .build()
+
+ dispatchApplyWindowInsets(insets)
+
+ rule.runOnIdle {
+ val view = insetsView.findComposeView()
+ val width = view.width
+ val height = view.height
+ assertThat(coordinates.boundsInRoot())
+ .isEqualTo(Rect(10f, 11f, width - 12f, height - 13f))
+ }
+ }
+
+ private fun testInsetsPadding(
+ type: Int,
+ modifier: Modifier,
+ sentInsets: AndroidXInsets = AndroidXInsets.of(10, 11, 12, 13),
+ expected: (Int, Int) -> Rect = { width, height ->
+ Rect(10f, 11f, width - 12f, height - 13f)
+ }
+ ) {
+ testInsetsPadding(type, sentInsets, expected) { modifier }
+ }
+
+ private fun testInsetsPadding(
+ type: Int,
+ sentInsets: AndroidXInsets = AndroidXInsets.of(10, 11, 12, 13),
+ expected: (Int, Int) -> Rect = { width, height ->
+ Rect(10f, 11f, width - 12f, height - 13f)
+ },
+ modifier: @Composable () -> Modifier,
+ ) {
+ val coordinates = setInsetContent(modifier)
+
+ val insets = sendInsets(type, sentInsets)
+ insets.assertIsConsumed(type)
+
+ rule.runOnIdle {
+ val view = insetsView.findComposeView()
+ val width = view.width
+ val height = view.height
+ val expectedRect = expected(width, height)
+ assertThat(coordinates.boundsInRoot()).isEqualTo(expectedRect)
+ }
+ }
+
+ // Removing the last Modifier handling insets should stop insets from being consumed
+ @Test
+ fun removeLastInsetsPadding() {
+ var useStatusBarInsets by mutableStateOf(true)
+ var useNavigationBarInsets by mutableStateOf(true)
+ val coordinates = setInsetContent {
+ (if (useStatusBarInsets) Modifier.statusBarsPadding() else Modifier).then(
+ if (useNavigationBarInsets) Modifier.navigationBarsPadding() else Modifier
+ )
+ }
+
+ rule.runOnIdle {
+ useStatusBarInsets = false
+ }
+
+ sendInsets(WindowInsetsCompat.Type.systemBars())
+ .assertIsConsumed(WindowInsetsCompat.Type.systemBars())
+
+ rule.runOnIdle {
+ useNavigationBarInsets = false
+ }
+
+ sendInsets(WindowInsetsCompat.Type.systemBars())
+ .assertIsNotConsumed(WindowInsetsCompat.Type.systemBars())
+
+ rule.runOnIdle {
+ val view = insetsView.findComposeView()
+ val width = view.width.toFloat()
+ val height = view.height.toFloat()
+ assertThat(coordinates.boundsInRoot()).isEqualTo(Rect(0f, 0f, width, height))
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun animateImeInsets() {
+ with(Api30Methods(rule)) {
+ val coordinates = setInsetContent { Modifier.systemBarsPadding().imePadding() }
+
+ sendInsets(WindowInsetsCompat.Type.systemBars())
+
+ val view = insetsView.findComposeView()
+ val animation = sendImeStart(view)
+
+ val width = view.width
+ val height = view.height
+
+ animation.sendImeProgress(view, 0f)
+
+ rule.runOnIdle {
+ assertThat(coordinates.boundsInRoot())
+ .isEqualTo(Rect(10f, 11f, width - 12f, height - 13f))
+ }
+
+ animation.sendImeProgress(view, 0.75f)
+
+ rule.runOnIdle {
+ assertThat(coordinates.boundsInRoot())
+ .isEqualTo(Rect(10f, 11f, width - 12f, height - 15f))
+ }
+
+ animation.sendImeProgress(view, 1f)
+
+ rule.runOnIdle {
+ assertThat(coordinates.boundsInRoot())
+ .isEqualTo(Rect(10f, 11f, width - 12f, height - 20f))
+ }
+
+ animation.sendImeEnd(view)
+
+ rule.runOnIdle {
+ assertThat(coordinates.boundsInRoot())
+ .isEqualTo(Rect(10f, 11f, width - 12f, height - 20f))
+ }
+ }
+ }
+
+ @Test
+ fun paddingValues() {
+ lateinit var coordinates: LayoutCoordinates
+
+ setContent {
+ val padding = WindowInsets.systemBars.asPaddingValues()
+ Box(Modifier.fillMaxSize().padding(padding)) {
+ Box(Modifier.fillMaxSize().onGloballyPositioned { coordinates = it })
+ }
+ }
+
+ // wait for layout
+ rule.waitForIdle()
+
+ val insets = sendInsets(WindowInsetsCompat.Type.systemBars())
+ insets.assertIsConsumed(WindowInsetsCompat.Type.systemBars())
+
+ rule.runOnIdle {
+ val view = insetsView.findComposeView()
+ val width = view.width
+ val height = view.height
+ val expectedRect = Rect(10f, 11f, width - 12f, height - 13f)
+ assertThat(coordinates.boundsInRoot()).isEqualTo(expectedRect)
+ }
+ }
+
+ // Each level of the padding should consume some parts of the insets
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun consumeAtEachDepth() {
+ lateinit var statusBar: LayoutCoordinates
+ lateinit var navigationBar: LayoutCoordinates
+ lateinit var ime: LayoutCoordinates
+ setContent {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .statusBarsPadding()
+ .onGloballyPositioned { statusBar = it }
+ ) {
+ Box(Modifier.navigationBarsPadding().onGloballyPositioned { navigationBar = it }) {
+ Box(Modifier.imePadding().fillMaxSize().onGloballyPositioned { ime = it })
+ }
+ }
+ }
+ // wait for layout
+ rule.waitForIdle()
+
+ val insets = WindowInsetsCompat.Builder()
+ .setInsets(WindowInsetsCompat.Type.statusBars(), AndroidXInsets.of(0, 10, 0, 0))
+ .setInsets(WindowInsetsCompat.Type.navigationBars(), AndroidXInsets.of(0, 0, 0, 11))
+ .setInsets(WindowInsetsCompat.Type.ime(), AndroidXInsets.of(0, 10, 0, 20))
+ .build()
+
+ dispatchApplyWindowInsets(insets)
+
+ rule.runOnIdle {
+ val height = insetsView.findComposeView().height
+ assertThat(statusBar.size.height).isEqualTo(height - 10)
+ assertThat(navigationBar.size.height).isEqualTo(height - 21)
+ assertThat(ime.size.height).isEqualTo(height - 30)
+ }
+ }
+
+ // The consumedPaddingInsets() should remove the insets values so that they aren't consumed
+ // further down the hierarchy.
+ @OptIn(ExperimentalLayoutApi::class)
+ @Test
+ fun consumedInsetsPadding() {
+ lateinit var outer: LayoutCoordinates
+ lateinit var middle: LayoutCoordinates
+ lateinit var inner: LayoutCoordinates
+ setContent {
+ with(LocalDensity.current) {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .consumedWindowInsets(PaddingValues(top = 1.toDp()))
+ .windowInsetsPadding(WindowInsets(top = 10))
+ .onGloballyPositioned { outer = it }
+ ) {
+ Box(Modifier
+ .consumedWindowInsets(PaddingValues(top = 1.toDp()))
+ .windowInsetsPadding(WindowInsets(top = 20))
+ .onGloballyPositioned { middle = it }
+ ) {
+ Box(
+ Modifier
+ .consumedWindowInsets(PaddingValues(top = 1.toDp()))
+ .windowInsetsPadding(WindowInsets(top = 30))
+ .fillMaxSize()
+ .onGloballyPositioned { inner = it }
+ )
+ }
+ }
+ }
+ }
+ // wait for layout
+ rule.waitForIdle()
+
+ val insets = WindowInsetsCompat.Builder()
+ .setInsets(WindowInsetsCompat.Type.statusBars(), AndroidXInsets.of(0, 35, 0, 0))
+ .build()
+
+ dispatchApplyWindowInsets(insets)
+
+ rule.runOnIdle {
+ val height = insetsView.findComposeView().height
+ assertThat(outer.size.height).isEqualTo(height - 9)
+ assertThat(middle.size.height).isEqualTo(height - 18)
+ assertThat(inner.size.height).isEqualTo(height - 27)
+ }
+ }
+
+ // The consumedInsets() should remove only values that haven't been consumed.
+ @OptIn(ExperimentalLayoutApi::class)
+ @Test
+ fun consumedInsetsLimitedConsumption() {
+ lateinit var outer: LayoutCoordinates
+ lateinit var middle: LayoutCoordinates
+ lateinit var inner: LayoutCoordinates
+ setContent {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .consumedWindowInsets(WindowInsets(top = 1))
+ .windowInsetsPadding(WindowInsets(top = 10))
+ .onGloballyPositioned { outer = it }
+ ) {
+ Box(Modifier
+ .consumedWindowInsets(WindowInsets(top = 10))
+ .windowInsetsPadding(WindowInsets(top = 20))
+ .onGloballyPositioned { middle = it }
+ ) {
+ Box(
+ Modifier
+ .consumedWindowInsets(WindowInsets(top = 20))
+ .windowInsetsPadding(WindowInsets(top = 30))
+ .fillMaxSize()
+ .onGloballyPositioned { inner = it }
+ )
+ }
+ }
+ }
+ // wait for layout
+ rule.waitForIdle()
+
+ val insets = WindowInsetsCompat.Builder()
+ .setInsets(WindowInsetsCompat.Type.statusBars(), AndroidXInsets.of(0, 35, 0, 0))
+ .build()
+
+ dispatchApplyWindowInsets(insets)
+
+ rule.runOnIdle {
+ val height = insetsView.findComposeView().height
+ assertThat(outer.size.height).isEqualTo(height - 9)
+ assertThat(middle.size.height).isEqualTo(height - 19)
+ assertThat(inner.size.height).isEqualTo(height - 29)
+ }
+ }
+
+ // When the insets change, the layout should be redrawn.
+ @OptIn(ExperimentalLayoutApi::class)
+ @Test
+ fun newInsetsCausesLayout() {
+ lateinit var coordinates: LayoutCoordinates
+ var useMiddleInsets by mutableStateOf(true)
+
+ setContent {
+ Box(Modifier.fillMaxSize()) {
+ val modifier = if (useMiddleInsets) {
+ Modifier.consumedWindowInsets(WindowInsets(top = 1))
+ } else {
+ Modifier.consumedWindowInsets(WindowInsets(top = 2))
+ }
+ with(LocalDensity.current) {
+ Box(modifier.size(50.toDp())) {
+ Box(
+ Modifier
+ .windowInsetsPadding(WindowInsets(top = 10))
+ .fillMaxSize()
+ .onGloballyPositioned { coordinates = it }
+ )
+ }
+ }
+ }
+ }
+
+ // wait for layout
+ rule.waitForIdle()
+
+ sendInsets(WindowInsetsCompat.Type.statusBars(), AndroidXInsets.of(0, 20, 0, 0))
+
+ rule.runOnIdle {
+ assertThat(coordinates.size.height).isEqualTo(41)
+ useMiddleInsets = false
+ }
+
+ rule.runOnIdle {
+ assertThat(coordinates.size.height).isEqualTo(42)
+ }
+ }
+
+ private fun sendInsets(
+ type: Int,
+ sentInsets: AndroidXInsets = AndroidXInsets.of(10, 11, 12, 13)
+ ): WindowInsetsCompat {
+ val insets = WindowInsetsCompat.Builder()
+ .setInsets(type, sentInsets)
+ .build()
+ return dispatchApplyWindowInsets(insets)
+ }
+
+ private fun dispatchApplyWindowInsets(insets: WindowInsetsCompat): WindowInsetsCompat {
+ return rule.runOnIdle {
+ val windowInsets = insets.toWindowInsets()!!
+ val view = insetsView
+ insetsView.myInsets = windowInsets
+ val returnedInsets = view.findComposeView().dispatchApplyWindowInsets(windowInsets)
+ WindowInsetsCompat.toWindowInsetsCompat(returnedInsets, view)
+ }
+ }
+
+ private fun setInsetContent(
+ insetsModifier: @Composable () -> Modifier
+ ): LayoutCoordinates {
+ lateinit var coordinates: LayoutCoordinates
+
+ setContent {
+ Box(Modifier.fillMaxSize().background(Color.Blue).then(insetsModifier())) {
+ Box(Modifier.fillMaxSize().onGloballyPositioned {
+ coordinates = it
+ })
+ }
+ }
+
+ // wait for layout
+ rule.waitForIdle()
+ return coordinates
+ }
+
+ private fun setContent(content: @Composable () -> Unit) {
+ rule.setContent {
+ AndroidView(factory = { context ->
+ val view = InsetsView(context)
+ insetsView = view
+ val composeView = ComposeView(rule.activity)
+ view.addView(
+ composeView,
+ ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ )
+ composeView.setContent(content)
+ view
+ }, modifier = Modifier.fillMaxSize())
+ }
+ }
+
+ private fun WindowInsetsCompat.assertIsConsumed(type: Int) {
+ val insets = getInsets(type)
+ assertThat(insets).isEqualTo(AndroidXInsets.of(0, 0, 0, 0))
+ }
+
+ private fun WindowInsetsCompat.assertIsNotConsumed(type: Int) {
+ val insets = getInsets(type)
+ assertThat(insets).isNotEqualTo(AndroidXInsets.of(0, 0, 0, 0))
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.R)
+private class Api30Methods(
+ val rule: AndroidComposeTestRule<ActivityScenarioRule<ComponentActivity>, ComponentActivity>
+) {
+ fun sendImeStart(view: View): WindowInsetsAnimation {
+ return rule.runOnIdle {
+ val animation =
+ WindowInsetsAnimation(AndroidWindowInsets.Type.ime(), LinearInterpolator(), 100L)
+ view.dispatchWindowInsetsAnimationPrepare(animation)
+
+ val imeInsets = FrameworkInsets.of(0, 0, 0, 20)
+ val bounds = WindowInsetsAnimation.Bounds(
+ FrameworkInsets.NONE,
+ imeInsets
+ )
+ view.dispatchWindowInsetsAnimationStart(animation, bounds)
+ animation
+ }
+ }
+
+ fun WindowInsetsAnimation.sendImeProgress(view: View, progress: Float) {
+ return rule.runOnIdle {
+ val bottom = (20 * progress).roundToInt()
+ val imeInsets = FrameworkInsets.of(0, 0, 0, bottom)
+ val systemBarsInsets = FrameworkInsets.of(10, 11, 12, 13)
+ val animatedInsets = AndroidWindowInsets.Builder()
+ .setInsets(AndroidWindowInsets.Type.systemBars(), systemBarsInsets)
+ .setInsets(AndroidWindowInsets.Type.ime(), imeInsets)
+ .build()
+
+ val progressInsets =
+ view.dispatchWindowInsetsAnimationProgress(animatedInsets, listOf(this))
+ assertThat(progressInsets.isConsumed).isTrue()
+ }
+ }
+
+ fun WindowInsetsAnimation.sendImeEnd(view: View) {
+ rule.runOnIdle {
+ view.dispatchWindowInsetsAnimationEnd(this)
+ }
+ }
+}
+
+/**
+ * A View below the compose View that overrides the insets sent by the system. The
+ * compat onApplyWindowInsets listener calls requestApplyInsets(), which results in
+ * the insets being sent again. If we don't override the insets then the system insets
+ * (which are likely 0) will override the insets that we set in the test.
+ */
+internal class InsetsView(context: Context) : FrameLayout(context) {
+ var myInsets: AndroidWindowInsets? = null
+
+ override fun dispatchApplyWindowInsets(insets: AndroidWindowInsets): AndroidWindowInsets {
+ return super.dispatchApplyWindowInsets(myInsets ?: insets)
+ }
+
+ fun findComposeView(): View = findComposeView(this)!!
+
+ private companion object {
+ fun findComposeView(view: View): View? {
+ if (view is ViewRootForTest) {
+ return view
+ } else if (view is ViewGroup) {
+ view.forEach { child ->
+ val composeView = findComposeView(child)
+ if (composeView != null) {
+ return composeView
+ }
+ }
+ }
+ return null
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsSizeTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsSizeTest.kt
new file mode 100644
index 0000000..e987593
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsSizeTest.kt
@@ -0,0 +1,328 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.layout
+
+import android.graphics.Rect as AndroidRect
+import android.view.WindowInsets as AndroidWindowInsets
+import android.os.Build
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.ComponentActivity
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.graphics.Insets as AndroidXInsets
+import androidx.core.view.DisplayCutoutCompat
+import androidx.core.view.WindowInsetsCompat
+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 org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class WindowInsetsSizeTest {
+ @get:Rule
+ val rule = createAndroidComposeRule<ComponentActivity>()
+
+ private lateinit var insetsView: InsetsView
+
+ @Before
+ fun setup() {
+ WindowInsetsHolder.setUseTestInsets(true)
+ }
+
+ @After
+ fun teardown() {
+ WindowInsetsHolder.setUseTestInsets(false)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun insetsStartWidthIme() {
+ testInsetsSize(
+ WindowInsetsCompat.Type.ime(),
+ { Modifier.windowInsetsStartWidth(WindowInsets.ime).fillMaxHeight() },
+ AndroidXInsets.of(10, 0, 0, 0),
+ LayoutDirection.Ltr
+ ) { size -> IntSize(10, size.height) }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun insetsStartWidthImeRtl() {
+ testInsetsSize(
+ WindowInsetsCompat.Type.ime(),
+ { Modifier.windowInsetsStartWidth(WindowInsets.ime).fillMaxHeight() },
+ AndroidXInsets.of(0, 0, 10, 0),
+ LayoutDirection.Rtl
+ ) { size -> IntSize(10, size.height) }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun insetsEndWidthIme() {
+ testInsetsSize(
+ WindowInsetsCompat.Type.ime(),
+ { Modifier.windowInsetsEndWidth(WindowInsets.ime).fillMaxHeight() },
+ AndroidXInsets.of(0, 0, 10, 0),
+ LayoutDirection.Ltr
+ ) { size -> IntSize(10, size.height) }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun insetsTopHeightIme() {
+ testInsetsSize(
+ WindowInsetsCompat.Type.ime(),
+ { Modifier.windowInsetsTopHeight(WindowInsets.ime).fillMaxWidth() },
+ AndroidXInsets.of(0, 10, 0, 0),
+ LayoutDirection.Ltr
+ ) { size -> IntSize(size.width, 10) }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
+ @Test
+ fun insetsBottomHeightIme() {
+ testInsetsSize(
+ WindowInsetsCompat.Type.ime(),
+ { Modifier.windowInsetsBottomHeight(WindowInsets.ime).fillMaxWidth() },
+ AndroidXInsets.of(0, 0, 0, 10),
+ LayoutDirection.Ltr
+ ) { size -> IntSize(size.width, 10) }
+ }
+
+ @Test
+ fun insetsStartWidthNavigationBars() {
+ testInsetsSize(
+ WindowInsetsCompat.Type.navigationBars(),
+ { Modifier.windowInsetsStartWidth(WindowInsets.navigationBars).fillMaxHeight() },
+ AndroidXInsets.of(10, 0, 0, 0),
+ LayoutDirection.Ltr
+ ) { size -> IntSize(10, size.height) }
+ }
+
+ @Test
+ fun insetsStartWidthNavigationBarsRtl() {
+ testInsetsSize(
+ WindowInsetsCompat.Type.navigationBars(),
+ { Modifier.windowInsetsStartWidth(WindowInsets.navigationBars).fillMaxHeight() },
+ AndroidXInsets.of(0, 0, 10, 0),
+ LayoutDirection.Rtl
+ ) { size -> IntSize(10, size.height) }
+ }
+
+ @Test
+ fun insetsEndWidthNavigationBars() {
+ testInsetsSize(
+ WindowInsetsCompat.Type.navigationBars(),
+ { Modifier.windowInsetsEndWidth(WindowInsets.navigationBars).fillMaxHeight() },
+ AndroidXInsets.of(0, 0, 10, 0),
+ LayoutDirection.Ltr
+ ) { size -> IntSize(10, size.height) }
+ }
+
+ @Test
+ fun insetsTopHeightStatusBars() {
+ testInsetsSize(
+ WindowInsetsCompat.Type.statusBars(),
+ { Modifier.windowInsetsTopHeight(WindowInsets.statusBars).fillMaxWidth() },
+ AndroidXInsets.of(0, 10, 0, 0),
+ LayoutDirection.Ltr
+ ) { size -> IntSize(size.width, 10) }
+ }
+
+ @Test
+ fun insetsTopHeightMixed() {
+ val coordinates = setInsetContent(
+ {
+ val insets = WindowInsets
+ Modifier.windowInsetsTopHeight(insets.navigationBars.union(insets.systemBars))
+ .fillMaxWidth()
+ },
+ LayoutDirection.Ltr
+ )
+ val insets = WindowInsetsCompat.Builder()
+ .setInsets(WindowInsetsCompat.Type.navigationBars(), AndroidXInsets.of(0, 3, 0, 0))
+ .setInsets(WindowInsetsCompat.Type.systemBars(), AndroidXInsets.of(0, 10, 0, 0))
+ .build()
+
+ val view = findComposeView()
+ rule.runOnIdle {
+ insetsView.myInsets = insets.toWindowInsets()
+ view.dispatchApplyWindowInsets(insets.toWindowInsets())
+ }
+
+ rule.runOnIdle {
+ assertThat(coordinates.size).isEqualTo(IntSize(view.width, 10))
+ }
+ }
+
+ @Test
+ fun topHeightModifiersAreEqual() {
+ rule.setContent {
+ val modifier1 = Modifier.windowInsetsTopHeight(WindowInsets.statusBars)
+ val modifier2 = Modifier.windowInsetsTopHeight(WindowInsets.statusBars)
+ assertThat(modifier1).isEqualTo(modifier2)
+ }
+ }
+
+ @Test
+ fun bottomHeightModifiersAreEqual() {
+ rule.setContent {
+ val modifier1 = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)
+ val modifier2 = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)
+ assertThat(modifier1).isEqualTo(modifier2)
+ }
+ }
+
+ @Test
+ fun startWidthModifiersAreEqual() {
+ rule.setContent {
+ val modifier1 = Modifier.windowInsetsStartWidth(WindowInsets.navigationBars)
+ val modifier2 = Modifier.windowInsetsStartWidth(WindowInsets.navigationBars)
+ assertThat(modifier1).isEqualTo(modifier2)
+ }
+ }
+
+ @Test
+ fun endWidthModifiersAreEqual() {
+ rule.setContent {
+ val modifier1 = Modifier.windowInsetsEndWidth(WindowInsets.navigationBars)
+ val modifier2 = Modifier.windowInsetsEndWidth(WindowInsets.navigationBars)
+ assertThat(modifier1).isEqualTo(modifier2)
+ }
+ }
+
+ private fun testInsetsSize(
+ type: Int,
+ modifier: @Composable () -> Modifier,
+ sentInsets: AndroidXInsets,
+ layoutDirection: LayoutDirection,
+ expected: (IntSize) -> IntSize
+ ) {
+ val coordinates = setInsetContent(modifier, layoutDirection)
+
+ val insets = sendInsets(type, sentInsets)
+ assertThat(insets.isConsumed)
+
+ rule.runOnIdle {
+ val view = findComposeView()
+ val width = view.width
+ val height = view.height
+ val expectedSize = expected(IntSize(width, height))
+ assertThat(coordinates.size).isEqualTo(expectedSize)
+ }
+ }
+
+ private fun sendInsets(
+ type: Int,
+ sentInsets: AndroidXInsets = AndroidXInsets.of(10, 11, 12, 13)
+ ): AndroidWindowInsets {
+ val builder = WindowInsetsCompat.Builder()
+ .setInsets(type, sentInsets)
+ if (type == WindowInsetsCompat.Type.displayCutout()) {
+ val view = findComposeView()
+ val width = view.width
+ val height = view.height
+ val safeRect = AndroidRect(0, 0, width, height)
+ val cutoutRect =
+ AndroidRect(width / 2 - 5, height / 2 - 5, width / 2 + 5, height / 2 + 5)
+ when {
+ sentInsets.left > 0 -> {
+ safeRect.left = sentInsets.left
+ cutoutRect.left = 0
+ cutoutRect.right = safeRect.left
+ }
+ sentInsets.top > 0 -> {
+ safeRect.top = sentInsets.top
+ cutoutRect.top = 0
+ cutoutRect.bottom = safeRect.top
+ }
+ sentInsets.right > 0 -> {
+ safeRect.right = width - sentInsets.right
+ cutoutRect.right = width
+ cutoutRect.left = width - safeRect.right
+ }
+ sentInsets.bottom > 0 -> {
+ safeRect.bottom = sentInsets.bottom
+ cutoutRect.bottom = height
+ cutoutRect.top = height - safeRect.bottom
+ }
+ }
+ builder.setDisplayCutout(DisplayCutoutCompat(safeRect, listOf(cutoutRect)))
+ }
+ val insets = WindowInsetsCompat.Builder()
+ .setInsets(type, sentInsets)
+ .build()
+ insetsView.myInsets = insets.toWindowInsets()
+ return rule.runOnIdle {
+ AndroidWindowInsets(
+ findComposeView().dispatchApplyWindowInsets(insets.toWindowInsets())
+ )
+ }
+ }
+
+ private fun setInsetContent(
+ sizeModifier: @Composable () -> Modifier,
+ layoutDirection: LayoutDirection
+ ): LayoutCoordinates {
+ lateinit var coordinates: LayoutCoordinates
+
+ rule.setContent {
+ AndroidView(factory = { context ->
+ val view = InsetsView(context)
+ insetsView = view
+ val composeView = ComposeView(rule.activity)
+ view.addView(
+ composeView,
+ ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ )
+ composeView.setContent {
+ CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+ Box(Modifier.wrapContentSize().onGloballyPositioned { coordinates = it }) {
+ Box(sizeModifier())
+ }
+ }
+ }
+ view
+ }, modifier = Modifier.fillMaxSize())
+ }
+
+ // wait for layout
+ rule.waitForIdle()
+ return coordinates
+ }
+
+ private fun findComposeView(): View = insetsView.findComposeView()
+}
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
new file mode 100644
index 0000000..bfc442d
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsets.android.kt
@@ -0,0 +1,370 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.layout
+
+import androidx.core.graphics.Insets as AndroidXInsets
+import android.os.Build
+import android.view.View
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.NonRestartableComposable
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.OnApplyWindowInsetsListener
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsAnimationCompat
+import androidx.core.view.WindowInsetsCompat
+import java.util.WeakHashMap
+import org.jetbrains.annotations.TestOnly
+
+internal fun AndroidXInsets.toInsetsValues(): InsetsValues =
+ InsetsValues(left, top, right, bottom)
+
+internal fun ValueInsets(insets: AndroidXInsets, name: String): ValueInsets =
+ ValueInsets(insets.toInsetsValues(), name)
+
+/**
+ * For the [WindowInsetsCompat.Type.captionBar].
+ */
+val WindowInsets.Companion.captionBar: WindowInsets
+ @Composable
+ @NonRestartableComposable
+ get() = WindowInsetsHolder.current().captionBar
+
+/**
+ * For the [WindowInsetsCompat.Type.displayCutout]. This insets represents the area that the
+ * display cutout (e.g. for camera) is and important content should be excluded from.
+ */
+val WindowInsets.Companion.displayCutout: WindowInsets
+ @Composable
+ @NonRestartableComposable
+ get() = WindowInsetsHolder.current().displayCutout
+
+/**
+ * For the [WindowInsetsCompat.Type.ime]. On [Build.VERSION_CODES.R] and above, the
+ * soft keyboard can be detected and [ime] will animate when it shows.
+ *
+ * Developers should set `android:windowSoftInputMode="adjustResize"` in their
+ * `AndroidManifest.xml` file and call `WindowCompat.setDecorFitsSystemWindows(window, false)`
+ * in their [android.app.Activity.onCreate].
+ */
+val WindowInsets.Companion.ime: WindowInsets
+ @Composable
+ @NonRestartableComposable
+ get() = WindowInsetsHolder.current().ime
+
+/**
+ * For the [WindowInsetsCompat.Type.mandatorySystemGestures]. These insets represents the
+ * space where system gestures have priority over application gestures.
+ */
+val WindowInsets.Companion.mandatorySystemGestures: WindowInsets
+ @Composable
+ @NonRestartableComposable
+ get() = WindowInsetsHolder.current().mandatorySystemGestures
+
+/**
+ * For the [WindowInsetsCompat.Type.navigationBars]. These insets represent where
+ * system UI places navigation bars. Interactive UI should avoid the navigation bars
+ * area.
+ */
+val WindowInsets.Companion.navigationBars: WindowInsets
+ @Composable
+ @NonRestartableComposable
+ get() = WindowInsetsHolder.current().navigationBars
+
+/**
+ * For the [WindowInsetsCompat.Type.statusBars].
+ */
+val WindowInsets.Companion.statusBars: WindowInsets
+ @Composable
+ @NonRestartableComposable
+ get() = WindowInsetsHolder.current().statusBars
+
+/**
+ * For the [WindowInsetsCompat.Type.systemBars].
+ */
+val WindowInsets.Companion.systemBars: WindowInsets
+ @Composable
+ @NonRestartableComposable
+ get() = WindowInsetsHolder.current().systemBars
+
+/**
+ * For the [WindowInsetsCompat.Type.systemGestures].
+ */
+val WindowInsets.Companion.systemGestures: WindowInsets
+ @Composable
+ @NonRestartableComposable
+ get() = WindowInsetsHolder.current().systemGestures
+
+/**
+ * For the [WindowInsetsCompat.Type.tappableElement].
+ */
+val WindowInsets.Companion.tappableElement: WindowInsets
+ @Composable
+ @NonRestartableComposable
+ get() = WindowInsetsHolder.current().tappableElement
+
+/**
+ * The insets for the curved areas in a waterfall display.
+ */
+val WindowInsets.Companion.waterfall: WindowInsets
+ @Composable
+ @NonRestartableComposable
+ get() = WindowInsetsHolder.current().waterfall
+
+/**
+ * The insets that include areas where content may be covered by other drawn content.
+ * This includes all [system bars][systemBars], [display cutout][displayCutout], and
+ * [soft keyboard][ime].
+ */
+val WindowInsets.Companion.safeDrawing: WindowInsets
+ @Composable
+ @NonRestartableComposable
+ get() = WindowInsetsHolder.current().safeDrawing
+
+/**
+ * The insets that include areas where gestures may be confused with other input,
+ * including [system gestures][systemGestures],
+ * [mandatory system gestures][mandatorySystemGestures],
+ * [rounded display areas][waterfall], and [tappable areas][tappableElement].
+ */
+val WindowInsets.Companion.safeGestures: WindowInsets
+ @Composable
+ @NonRestartableComposable
+ get() = WindowInsetsHolder.current().safeGestures
+
+/**
+ * The insets that include all areas that may be drawn over or have gesture confusion,
+ * including everything in [safeDrawing] and [safeGestures].
+ */
+val WindowInsets.Companion.safeContent: WindowInsets
+ @Composable
+ @NonRestartableComposable
+ get() = WindowInsetsHolder.current().safeContent
+
+/**
+ * The insets for various values in the current window.
+ */
+internal class WindowInsetsHolder private constructor(insets: WindowInsetsCompat?) {
+ val captionBar =
+ valueInsets(insets, WindowInsetsCompat.Type.captionBar(), "captionBar")
+ val displayCutout =
+ valueInsets(insets, WindowInsetsCompat.Type.displayCutout(), "displayCutout")
+ val ime = valueInsets(insets, WindowInsetsCompat.Type.ime(), "ime")
+ val mandatorySystemGestures = valueInsets(
+ insets,
+ WindowInsetsCompat.Type.mandatorySystemGestures(),
+ "mandatorySystemGestures"
+ )
+ val navigationBars =
+ valueInsets(insets, WindowInsetsCompat.Type.navigationBars(), "navigationBars")
+ val statusBars =
+ valueInsets(insets, WindowInsetsCompat.Type.statusBars(), "statusBars")
+ val systemBars =
+ valueInsets(insets, WindowInsetsCompat.Type.systemBars(), "systemBars")
+ val systemGestures =
+ valueInsets(insets, WindowInsetsCompat.Type.systemGestures(), "systemGestures")
+ val tappableElement =
+ valueInsets(insets, WindowInsetsCompat.Type.tappableElement(), "tappableElement")
+ val waterfall =
+ ValueInsets(insets?.displayCutout?.waterfallInsets ?: AndroidXInsets.NONE, "waterfall")
+ val safeDrawing =
+ systemBars.union(ime).union(displayCutout)
+ val safeGestures: WindowInsets =
+ tappableElement.union(mandatorySystemGestures).union(systemGestures).union(waterfall)
+ val safeContent: WindowInsets = safeDrawing.union(safeGestures)
+
+ /**
+ * The number of accesses to [WindowInsetsHolder]. When this reaches
+ * zero, the listeners are removed. When it increases to 1, the listeners are added.
+ */
+ private var consumers = 0
+
+ private val insetsListener = InsetsListener(this)
+
+ /**
+ * A usage of [WindowInsetsHolder.current] was added. We must track so that when the
+ * first one is added, listeners are set and when the last is removed, the listeners
+ * are removed.
+ */
+ fun incrementConsumers(view: View) {
+ if (consumers == 0) {
+ // add listeners
+ ViewCompat.setOnApplyWindowInsetsListener(view, 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) {
+ ViewCompat.setWindowInsetsAnimationCallback(view, insetsListener)
+ }
+ }
+ consumers++
+ }
+
+ /**
+ * A usage of [WindowInsetsHolder.current] was removed. We must track so that when the
+ * first one is added, listeners are set and when the last is removed, the listeners
+ * are removed.
+ */
+ fun decrementConsumers(view: View) {
+ consumers--
+ if (consumers == 0) {
+ // remove listeners
+ ViewCompat.setOnApplyWindowInsetsListener(view, null)
+ ViewCompat.setWindowInsetsAnimationCallback(view, null)
+ }
+ }
+
+ /**
+ * Updates the WindowInsets values and notifies changes.
+ */
+ fun update(windowInsets: WindowInsetsCompat) {
+ Snapshot.withMutableSnapshot {
+ val insets = if (testInsets) {
+ // WindowInsetsCompat erases insets that aren't part of the device.
+ // For example, if there is no navigation bar because of hardware keys,
+ // the bottom navigation bar will be removed. By using the constructor
+ // that doesn't accept a View, it doesn't remove the insets that aren't
+ // possible. This is important for testing on arbitrary hardware.
+ WindowInsetsCompat.toWindowInsetsCompat(windowInsets.toWindowInsets()!!)
+ } else {
+ windowInsets
+ }
+ captionBar.value =
+ insets.getInsets(WindowInsetsCompat.Type.captionBar()).toInsetsValues()
+ ime.value =
+ insets.getInsets(WindowInsetsCompat.Type.ime()).toInsetsValues()
+ displayCutout.value =
+ insets.getInsets(WindowInsetsCompat.Type.displayCutout()).toInsetsValues()
+ navigationBars.value =
+ insets.getInsets(WindowInsetsCompat.Type.navigationBars()).toInsetsValues()
+ statusBars.value =
+ insets.getInsets(WindowInsetsCompat.Type.statusBars()).toInsetsValues()
+ systemBars.value =
+ insets.getInsets(WindowInsetsCompat.Type.systemBars()).toInsetsValues()
+ systemGestures.value =
+ insets.getInsets(WindowInsetsCompat.Type.systemGestures()).toInsetsValues()
+ tappableElement.value =
+ insets.getInsets(WindowInsetsCompat.Type.tappableElement()).toInsetsValues()
+ mandatorySystemGestures.value =
+ insets.getInsets(WindowInsetsCompat.Type.mandatorySystemGestures()).toInsetsValues()
+
+ val cutout = insets.displayCutout
+ if (cutout != null) {
+ val waterfallInsets = cutout.waterfallInsets
+ waterfall.value = waterfallInsets.toInsetsValues()
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * A mapping of AndroidComposeView to ComposeWindowInsets. Normally a tag is a great
+ * way to do this mapping, but off-UI thread and multithreaded composition don't
+ * allow using the tag.
+ */
+ private val viewMap = WeakHashMap<View, WindowInsetsHolder>()
+
+ private var testInsets = false
+
+ /**
+ * Testing Window Insets is difficult, so we have this to help eliminate device-specifics
+ * from the WindowInsets. This is indirect because `@TestOnly` cannot be applied to a
+ * property with a backing field.
+ */
+ @TestOnly
+ fun setUseTestInsets(testInsets: Boolean) {
+ this.testInsets = testInsets
+ }
+
+ @Composable
+ fun current(): WindowInsetsHolder {
+ val view = LocalView.current
+ val insets = getOrCreateFor(view)
+
+ DisposableEffect(insets) {
+ insets.incrementConsumers(view)
+ onDispose {
+ insets.decrementConsumers(view)
+ }
+ }
+ return insets
+ }
+
+ /**
+ * Returns the [WindowInsetsHolder] associated with [view] or creates one and associates
+ * it.
+ */
+ private fun getOrCreateFor(view: View): WindowInsetsHolder {
+ return synchronized(viewMap) {
+ viewMap.getOrPut(view) {
+ val insets = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ RootWindowInsetsApi23.rootWindowInsets(view)
+ } else {
+ null
+ }
+ WindowInsetsHolder(insets)
+ }
+ }
+ }
+
+ /**
+ * Creates a [ValueInsets] using the value from [windowInsets] if it isn't `null`
+ */
+ private fun valueInsets(
+ windowInsets: WindowInsetsCompat?,
+ type: Int,
+ name: String
+ ): ValueInsets {
+ val initial = windowInsets?.getInsets(type) ?: AndroidXInsets.NONE
+ return ValueInsets(initial, name)
+ }
+ }
+}
+
+/**
+ * Used to get the [View.getRootWindowInsets] only on M and above
+ */
+@RequiresApi(Build.VERSION_CODES.M)
+private object RootWindowInsetsApi23 {
+ @DoNotInline
+ fun rootWindowInsets(view: View): WindowInsetsCompat? {
+ return view.rootWindowInsets?.let {
+ WindowInsetsCompat.toWindowInsetsCompat(it, view)
+ }
+ }
+}
+
+private class InsetsListener(
+ val composeInsets: WindowInsetsHolder,
+) : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP), OnApplyWindowInsetsListener {
+
+ override fun onProgress(
+ insets: WindowInsetsCompat,
+ runningAnimations: MutableList<WindowInsetsAnimationCompat>
+ ): WindowInsetsCompat {
+ composeInsets.update(insets)
+ return WindowInsetsCompat.CONSUMED
+ }
+
+ override fun onApplyWindowInsets(view: View, insets: WindowInsetsCompat): WindowInsetsCompat {
+ composeInsets.update(insets)
+ return WindowInsetsCompat.CONSUMED
+ }
+}
diff --git a/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.android.kt b/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.android.kt
new file mode 100644
index 0000000..244bf86
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/androidMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.android.kt
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.layout
+
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+
+/**
+ * Adds padding to accommodate the [safe drawing][WindowInsets.Companion.safeDrawing] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.safeDrawing] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [statusBarsPadding], the area that the parent
+ * pads for the status bars will not be padded again by this [safeDrawingPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.safeDrawingPaddingSample
+ */
+fun Modifier.safeDrawingPadding() =
+ windowInsetsPadding(debugInspectorInfo { name = "safeDrawingPadding" }) { safeDrawing }
+
+/**
+ * Adds padding to accommodate the [safe gestures][WindowInsets.Companion.safeGestures] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.safeGestures] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [navigationBarsPadding],
+ * the area that the parent layout pads for the status bars will not be padded again by this
+ * [safeGesturesPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.safeGesturesPaddingSample
+ */
+fun Modifier.safeGesturesPadding() =
+ windowInsetsPadding(debugInspectorInfo { name = "safeGesturesPadding" }) { safeGestures }
+
+/**
+ * Adds padding to accommodate the [safe content][WindowInsets.Companion.safeContent] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.safeContent] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [navigationBarsPadding],
+ * the area that the parent layout pads for the status bars will not be padded again by this
+ * [safeContentPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.safeContentPaddingSample
+ */
+fun Modifier.safeContentPadding() =
+ windowInsetsPadding(debugInspectorInfo { name = "safeContentPadding" }) { safeContent }
+
+/**
+ * Adds padding to accommodate the [system bars][WindowInsets.Companion.systemBars] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.systemBars] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [statusBarsPadding], the
+ * area that the parent layout pads for the status bars will not be padded again by this
+ * [systemBarsPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.systemBarsPaddingSample
+ */
+fun Modifier.systemBarsPadding() =
+ windowInsetsPadding(debugInspectorInfo { name = "systemBarsPadding" }) { systemBars }
+
+/**
+ * Adds padding to accommodate the [display cutout][WindowInsets.Companion.displayCutout].
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.displayCutout] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [statusBarsPadding], the
+ * area that the parent layout pads for the status bars will not be padded again by this
+ * [displayCutoutPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.displayCutoutPaddingSample
+ */
+fun Modifier.displayCutoutPadding() =
+ windowInsetsPadding(debugInspectorInfo { name = "displayCutoutPadding" }) { displayCutout }
+
+/**
+ * Adds padding to accommodate the [status bars][WindowInsets.Companion.statusBars] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.statusBars] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [displayCutoutPadding], the
+ * area that the parent layout pads for the status bars will not be padded again by this
+ * [statusBarsPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.statusBarsAndNavigationBarsPaddingSample
+ */
+fun Modifier.statusBarsPadding() =
+ windowInsetsPadding(debugInspectorInfo { name = "statusBarsPadding" }) { statusBars }
+
+/**
+ * Adds padding to accommodate the [ime][WindowInsets.Companion.ime] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.ime] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [navigationBarsPadding],
+ * the area that the parent layout pads for the status bars will not be padded again by this
+ * [imePadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.imePaddingSample
+ */
+fun Modifier.imePadding() =
+ windowInsetsPadding(debugInspectorInfo { name = "imePadding" }) { ime }
+
+/**
+ * Adds padding to accommodate the [navigation bars][WindowInsets.Companion.navigationBars] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.navigationBars] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [systemBarsPadding], the
+ * area that the parent layout pads for the status bars will not be padded again by this
+ * [navigationBarsPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.statusBarsAndNavigationBarsPaddingSample
+ */
+fun Modifier.navigationBarsPadding() =
+ windowInsetsPadding(debugInspectorInfo { name = "navigationBarsPadding" }) { navigationBars }
+
+/**
+ * Adds padding to accommodate the [caption bar][WindowInsets.Companion.captionBar] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.captionBar] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [displayCutoutPadding], the
+ * area that the parent layout pads for the status bars will not be padded again by this
+ * [captionBarPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.captionBarPaddingSample
+ */
+fun Modifier.captionBarPadding() =
+ windowInsetsPadding(debugInspectorInfo { name = "captionBarPadding" }) { captionBar }
+
+/**
+ * Adds padding to accommodate the [waterfall][WindowInsets.Companion.waterfall] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.waterfall] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [systemGesturesPadding],
+ * the area that the parent layout pads for the status bars will not be padded again by this
+ * [waterfallPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.waterfallPaddingSample
+ */
+fun Modifier.waterfallPadding() =
+ windowInsetsPadding(debugInspectorInfo { name = "waterfallPadding" }) { waterfall }
+
+/**
+ * Adds padding to accommodate the [system gestures][WindowInsets.Companion.systemGestures] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.systemGestures] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [waterfallPadding], the
+ * area that the parent layout pads for the status bars will not be padded again by this
+ * [systemGesturesPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.systemGesturesPaddingSample
+ */
+fun Modifier.systemGesturesPadding() =
+ windowInsetsPadding(debugInspectorInfo { name = "systemGesturesPadding" }) { systemGestures }
+
+/**
+ * Adds padding to accommodate the
+ * [mandatory system gestures][WindowInsets.Companion.mandatorySystemGestures] insets.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent layout
+ * will be excluded from the padding. [WindowInsets.Companion.mandatorySystemGestures] will be
+ * [consumed][consumedWindowInsets] for child layouts as well.
+ *
+ * For example, if a parent layout uses [navigationBarsPadding],
+ * the area that the parent layout pads for the status bars will not be padded again by this
+ * [mandatorySystemGesturesPadding] modifier.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.mandatorySystemGesturesPaddingSample
+ */
+fun Modifier.mandatorySystemGesturesPadding() =
+ windowInsetsPadding(debugInspectorInfo { name = "mandatorySystemGesturesPadding" }) {
+ mandatorySystemGestures
+ }
+
+@Suppress("NOTHING_TO_INLINE", "ModifierInspectorInfo")
+@Stable
+private inline fun Modifier.windowInsetsPadding(
+ noinline inspectorInfo: InspectorInfo.() -> Unit,
+ crossinline insetsCalculation: WindowInsetsHolder.() -> WindowInsets
+): Modifier = composed(inspectorInfo) {
+ val composeInsets = WindowInsetsHolder.current()
+ remember(composeInsets) {
+ val insets = composeInsets.insetsCalculation()
+ InsetsPaddingModifier(insets)
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsets.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsets.kt
new file mode 100644
index 0000000..627e0f5
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsets.kt
@@ -0,0 +1,457 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.layout
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+
+/**
+ * A representation of window insets that tracks access to enable recomposition,
+ * relayout, and redrawing when values change. These values should not be read during composition
+ * to avoid doing composition for every frame of an animation. Use methods like
+ * [Modifier.windowInsetsPadding], [Modifier.systemBarsPadding], and
+ * [Modifier.windowInsetsTopHeight] for Modifiers that will not cause recomposition when values
+ * change.
+ *
+ * Use the [WindowInsets.Companion] extensions to retrieve [WindowInsets] for the current
+ * window.
+ */
+@Stable
+interface WindowInsets {
+ /**
+ * The space, in pixels, at the left of the window that the inset represents.
+ */
+ fun getLeft(density: Density, layoutDirection: LayoutDirection): Int
+
+ /**
+ * The space, in pixels, at the top of the window that the inset represents.
+ */
+ fun getTop(density: Density): Int
+
+ /**
+ * The space, in pixels, at the right of the window that the inset represents.
+ */
+ fun getRight(density: Density, layoutDirection: LayoutDirection): Int
+
+ /**
+ * The space, in pixels, at the bottom of the window that the inset represents.
+ */
+ fun getBottom(density: Density): Int
+
+ companion object
+}
+
+/**
+ * Returns an [WindowInsets] that has the maximum values of this [WindowInsets] and [insets].
+ */
+fun WindowInsets.union(insets: WindowInsets): WindowInsets = UnionInsets(this, insets)
+
+/**
+ * Returns the values in this [WindowInsets] that are not also in [insets]. For example, if this
+ * [WindowInsets] has a [WindowInsets.getTop] value of `10` and [insets] has a
+ * [WindowInsets.getTop] value of `8`, the returned [WindowInsets] will have a
+ * [WindowInsets.getTop] value of `2`.
+ *
+ * Negative values are never returned. For example if [insets] has a [WindowInsets.getTop] of `10`
+ * and this has a [WindowInsets.getTop] of `0`, the returned [WindowInsets] will have a
+ * [WindowInsets.getTop] value of `0`.
+ */
+fun WindowInsets.exclude(insets: WindowInsets): WindowInsets = ExcludeInsets(this, insets)
+
+/**
+ * Returns the an [WindowInsets] that has values of this, added to the values of [insets].
+ * For example, if this has a top of 10 and insets has a top of 5, the returned [WindowInsets]
+ * will have a top of 15.
+ */
+fun WindowInsets.add(insets: WindowInsets): WindowInsets = AddedInsets(this, insets)
+
+/**
+ * Convert an [WindowInsets] to a [PaddingValues] and uses [LocalDensity] for DP to pixel conversion.
+ * [PaddingValues] can be passed to some containers to pad internal content so that it doesn't
+ * overlap the insets when fully scrolled. Ensure that the insets are [consumed][consumedWindowInsets]
+ * after the padding is applied if insets are to be used further down the hierarchy.
+ *
+ * @sample androidx.compose.foundation.layout.samples.paddingValuesSample
+ */
+@Composable
+fun WindowInsets.asPaddingValues(): PaddingValues = InsetsPaddingValues(this, LocalDensity.current)
+
+/**
+ * Convert a [PaddingValues] to an [WindowInsets].
+ */
+internal fun PaddingValues.asInsets(): WindowInsets = PaddingValuesInsets(this)
+
+/**
+ * Create an [WindowInsets] with fixed dimensions.
+ *
+ * @sample androidx.compose.foundation.layout.samples.insetsInt
+ */
+fun WindowInsets(left: Int = 0, top: Int = 0, right: Int = 0, bottom: Int = 0): WindowInsets =
+ FixedIntInsets(left, top, right, bottom)
+
+/**
+ * Create an [WindowInsets] with fixed dimensions, using [Dp] values.
+ *
+ * @sample androidx.compose.foundation.layout.samples.insetsDp
+ */
+fun WindowInsets(
+ left: Dp = 0.dp,
+ top: Dp = 0.dp,
+ right: Dp = 0.dp,
+ bottom: Dp = 0.dp
+): WindowInsets = FixedDpInsets(left, top, right, bottom)
+
+@Immutable
+private class FixedIntInsets(
+ private val leftVal: Int,
+ private val topVal: Int,
+ private val rightVal: Int,
+ private val bottomVal: Int
+) : WindowInsets {
+ override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int = leftVal
+ override fun getTop(density: Density): Int = topVal
+ override fun getRight(density: Density, layoutDirection: LayoutDirection): Int = rightVal
+ override fun getBottom(density: Density): Int = bottomVal
+
+ override fun toString(): String {
+ return "Insets(left=$leftVal, top=$topVal, right=$rightVal, bottom=$bottomVal)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+ if (other !is FixedIntInsets) {
+ return false
+ }
+
+ return leftVal == other.leftVal && topVal == other.topVal &&
+ rightVal == other.rightVal && bottomVal == other.bottomVal
+ }
+
+ override fun hashCode(): Int {
+ var result = leftVal
+ result = 31 * result + topVal
+ result = 31 * result + rightVal
+ result = 31 * result + bottomVal
+ return result
+ }
+}
+
+@Immutable
+private class FixedDpInsets(
+ private val leftDp: Dp,
+ private val topDp: Dp,
+ private val rightDp: Dp,
+ private val bottomDp: Dp
+) : WindowInsets {
+ override fun getLeft(density: Density, layoutDirection: LayoutDirection) =
+ with(density) { leftDp.roundToPx() }
+
+ override fun getTop(density: Density) = with(density) { topDp.roundToPx() }
+ override fun getRight(density: Density, layoutDirection: LayoutDirection) =
+ with(density) { rightDp.roundToPx() }
+ override fun getBottom(density: Density) = with(density) { bottomDp.roundToPx() }
+
+ override fun toString(): String {
+ return "Insets(left=$leftDp, top=$topDp, right=$rightDp, bottom=$bottomDp)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+ if (other !is FixedDpInsets) {
+ return false
+ }
+
+ return leftDp == other.leftDp && topDp == other.topDp &&
+ rightDp == other.rightDp && bottomDp == other.bottomDp
+ }
+
+ override fun hashCode(): Int {
+ var result = leftDp.hashCode()
+ result = 31 * result + topDp.hashCode()
+ result = 31 * result + rightDp.hashCode()
+ result = 31 * result + bottomDp.hashCode()
+ return result
+ }
+}
+
+/**
+ * An [WindowInsets] that comes straight from [androidx.core.graphics.Insets], whose value can
+ * be updated.
+ */
+@Stable
+internal class ValueInsets(val insets: InsetsValues, val name: String) : WindowInsets {
+ internal var value by mutableStateOf(insets)
+
+ override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int = value.left
+ override fun getTop(density: Density) = value.top
+ override fun getRight(density: Density, layoutDirection: LayoutDirection) = value.right
+ override fun getBottom(density: Density) = value.bottom
+
+ override fun equals(other: Any?): Boolean {
+ if (other === this) {
+ return true
+ }
+ if (other !is ValueInsets) {
+ return false
+ }
+ return value == other.value
+ }
+
+ override fun hashCode(): Int {
+ return name.hashCode()
+ }
+
+ override fun toString(): String {
+ return "$name(left=${insets.left}, top=${insets.top}, " +
+ "right=${insets.right}, bottom=${insets.bottom})"
+ }
+}
+
+@Immutable
+internal class InsetsValues(val left: Int, val top: Int, val right: Int, val bottom: Int) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+ if (other !is InsetsValues) {
+ return false
+ }
+
+ return left == other.left &&
+ top == other.top &&
+ right == other.right &&
+ bottom == other.bottom
+ }
+
+ override fun hashCode(): Int {
+ var result = left
+ result = 31 * result + top
+ result = 31 * result + right
+ result = 31 * result + bottom
+ return result
+ }
+}
+
+/**
+ * An [WindowInsets] that includes the maximum value of [first] and [second] as returned from
+ * [WindowInsets.union].
+ */
+@Stable
+private class UnionInsets(
+ private val first: WindowInsets,
+ private val second: WindowInsets
+) : WindowInsets {
+ override fun getLeft(density: Density, layoutDirection: LayoutDirection) =
+ maxOf(first.getLeft(density, layoutDirection), second.getLeft(density, layoutDirection))
+
+ override fun getTop(density: Density) =
+ maxOf(first.getTop(density), second.getTop(density))
+
+ override fun getRight(density: Density, layoutDirection: LayoutDirection) =
+ maxOf(first.getRight(density, layoutDirection), second.getRight(density, layoutDirection))
+
+ override fun getBottom(density: Density) =
+ maxOf(first.getBottom(density), second.getBottom(density))
+
+ override fun hashCode(): Int = first.hashCode() + second.hashCode() * 31
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+ if (other !is UnionInsets) {
+ return false
+ }
+ return other.first == first && other.second == second
+ }
+
+ override fun toString(): String = "($first ∪ $second)"
+}
+
+/**
+ * An [WindowInsets] that includes the added value of [first] to [second].
+ */
+@Stable
+private class AddedInsets(
+ private val first: WindowInsets,
+ private val second: WindowInsets
+) : WindowInsets {
+ override fun getLeft(density: Density, layoutDirection: LayoutDirection) =
+ first.getLeft(density, layoutDirection) + second.getLeft(density, layoutDirection)
+
+ override fun getTop(density: Density) =
+ first.getTop(density) + second.getTop(density)
+
+ override fun getRight(density: Density, layoutDirection: LayoutDirection) =
+ first.getRight(density, layoutDirection) + second.getRight(density, layoutDirection)
+
+ override fun getBottom(density: Density) =
+ first.getBottom(density) + second.getBottom(density)
+
+ override fun hashCode(): Int = first.hashCode() + second.hashCode() * 31
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+ if (other !is AddedInsets) {
+ return false
+ }
+ return other.first == first && other.second == second
+ }
+
+ override fun toString(): String = "($first + $second)"
+}
+
+/**
+ * An [WindowInsets] that includes the value of [included] that is not included in [excluded] as
+ * returned from [WindowInsets.exclude].
+ */
+@Stable
+private class ExcludeInsets(
+ private val included: WindowInsets,
+ private val excluded: WindowInsets
+) : WindowInsets {
+ override fun getLeft(density: Density, layoutDirection: LayoutDirection) =
+ (included.getLeft(density, layoutDirection) - excluded.getLeft(density, layoutDirection))
+ .coerceAtLeast(0)
+
+ override fun getTop(density: Density) =
+ (included.getTop(density) - excluded.getTop(density)).coerceAtLeast(0)
+
+ override fun getRight(density: Density, layoutDirection: LayoutDirection) =
+ (included.getRight(density, layoutDirection) - excluded.getRight(density, layoutDirection))
+ .coerceAtLeast(0)
+
+ override fun getBottom(density: Density) =
+ (included.getBottom(density) - excluded.getBottom(density)).coerceAtLeast(0)
+
+ override fun toString(): String = "($included - $excluded)"
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+ if (other !is ExcludeInsets) {
+ return false
+ }
+
+ return (other.included == included && other.excluded == excluded)
+ }
+
+ override fun hashCode(): Int = 31 * included.hashCode() + excluded.hashCode()
+}
+
+/**
+ * An [WindowInsets] calculated from [paddingValues].
+ */
+@Stable
+private class PaddingValuesInsets(private val paddingValues: PaddingValues) : WindowInsets {
+ override fun getLeft(density: Density, layoutDirection: LayoutDirection) = with(density) {
+ paddingValues.calculateLeftPadding(layoutDirection).roundToPx()
+ }
+
+ override fun getTop(density: Density) = with(density) {
+ paddingValues.calculateTopPadding().roundToPx()
+ }
+
+ override fun getRight(density: Density, layoutDirection: LayoutDirection) = with(density) {
+ paddingValues.calculateRightPadding(layoutDirection).roundToPx()
+ }
+
+ override fun getBottom(density: Density) = with(density) {
+ paddingValues.calculateBottomPadding().roundToPx()
+ }
+
+ override fun toString(): String {
+ val layoutDirection = LayoutDirection.Ltr
+ val start = paddingValues.calculateLeftPadding(layoutDirection)
+ val top = paddingValues.calculateTopPadding()
+ val end = paddingValues.calculateRightPadding(layoutDirection)
+ val bottom = paddingValues.calculateBottomPadding()
+ return "PaddingValues($start, $top, $end, $bottom)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+ if (other !is PaddingValuesInsets) {
+ return false
+ }
+
+ return other.paddingValues == paddingValues
+ }
+
+ override fun hashCode(): Int = paddingValues.hashCode()
+}
+
+@Stable
+private class InsetsPaddingValues(
+ val insets: WindowInsets,
+ private val density: Density
+) : PaddingValues {
+ override fun calculateLeftPadding(layoutDirection: LayoutDirection) = with(density) {
+ insets.getLeft(this, layoutDirection).toDp()
+ }
+
+ override fun calculateTopPadding() = with(density) {
+ insets.getTop(this).toDp()
+ }
+
+ override fun calculateRightPadding(layoutDirection: LayoutDirection) = with(density) {
+ insets.getRight(this, layoutDirection).toDp()
+ }
+
+ override fun calculateBottomPadding() = with(density) {
+ insets.getBottom(this).toDp()
+ }
+
+ override fun toString(): String {
+ return "InsetsPaddingValues(insets=$insets, density=$density)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+ if (other !is InsetsPaddingValues) {
+ return false
+ }
+ return insets == other.insets && density == other.density
+ }
+
+ override fun hashCode(): Int {
+ var result = insets.hashCode()
+ result = 31 * result + density.hashCode()
+ return result
+ }
+}
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.kt
new file mode 100644
index 0000000..5fbb82fc
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsPadding.kt
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.layout
+
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.modifier.ModifierLocalConsumer
+import androidx.compose.ui.modifier.ModifierLocalProvider
+import androidx.compose.ui.modifier.ModifierLocalReadScope
+import androidx.compose.ui.modifier.ProvidableModifierLocal
+import androidx.compose.ui.modifier.modifierLocalOf
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.unit.offset
+
+/**
+ * Adds padding so that the content doesn't enter [insets] space.
+ *
+ * Any insets consumed by other insets padding modifiers or [consumedWindowInsets] on a parent
+ * layout will be excluded from [insets]. [insets] will be [consumed][consumedWindowInsets] for
+ * child layouts as well.
+ *
+ * For example, if an ancestor uses [statusBarsPadding] and this modifier uses
+ * [WindowInsets.Companion.systemBars], the portion of the system bars that the status bars uses
+ * will not be padded again by this modifier.
+ *
+ * @sample androidx.compose.foundation.layout.samples.insetsPaddingSample
+ * @see WindowInsets
+ */
+@Stable
+fun Modifier.windowInsetsPadding(insets: WindowInsets): Modifier = this.then(
+ InsetsPaddingModifier(insets, debugInspectorInfo {
+ name = "windowInsetsPadding"
+ properties["insets"] = insets
+ })
+)
+
+/**
+ * Consume insets that haven't been consumed yet by other insets Modifiers similar to
+ * [windowInsetsPadding] without adding any padding.
+ *
+ * This can be useful when content offsets are provided by [WindowInsets.asPaddingValues].
+ * This should be used further down the hierarchy than the [PaddingValues] is used so
+ * that the values aren't consumed before the padding is added.
+ *
+ * @sample androidx.compose.foundation.layout.samples.consumedInsetsSample
+ */
+@ExperimentalLayoutApi
+@Stable
+fun Modifier.consumedWindowInsets(insets: WindowInsets): Modifier = this.then(
+ UnionInsetsConsumingModifier(insets, debugInspectorInfo {
+ name = "consumedWindowInsets"
+ properties["insets"] = insets
+ })
+)
+
+/**
+ * Consume [paddingValues] as insets as if the padding was added irrespective of insets.
+ * Layouts further down the hierarchy that use [windowInsetsPadding], [safeContentPadding],
+ * and other insets padding Modifiers won't pad for the values that [paddingValues] provides.
+ * This can be useful when content offsets are provided by layout rather than [windowInsetsPadding]
+ * modifiers.
+ *
+ * This method consumes all of [paddingValues] in addition to whatever has been
+ * consumed by other [windowInsetsPadding] modifiers by ancestors. [consumedWindowInsets]
+ * accepting a [WindowInsets] argument ensures that its insets are consumed and doesn't
+ * consume more if they have already been consumed by ancestors.
+ *
+ * @sample androidx.compose.foundation.layout.samples.consumedInsetsPaddingSample
+ */
+@ExperimentalLayoutApi
+@Stable
+fun Modifier.consumedWindowInsets(paddingValues: PaddingValues): Modifier = this.then(
+ PaddingValuesConsumingModifier(paddingValues, debugInspectorInfo {
+ name = "consumedWindowInsets"
+ properties["paddingValues"] = paddingValues
+ })
+)
+
+internal val ModifierLocalConsumedWindowInsets = modifierLocalOf {
+ WindowInsets(0, 0, 0, 0)
+}
+
+internal class InsetsPaddingModifier(
+ private val insets: WindowInsets,
+ inspectorInfo: InspectorInfo.() -> Unit = debugInspectorInfo {
+ name = "InsetsPaddingModifier"
+ properties["insets"] = insets
+ }
+) : InspectorValueInfo(inspectorInfo), LayoutModifier,
+ ModifierLocalConsumer, ModifierLocalProvider<WindowInsets> {
+ private var unconsumedInsets: WindowInsets by mutableStateOf(insets)
+ private var consumedInsets: WindowInsets by mutableStateOf(insets)
+
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val left = unconsumedInsets.getLeft(this, layoutDirection)
+ val top = unconsumedInsets.getTop(this)
+ val right = unconsumedInsets.getRight(this, layoutDirection)
+ val bottom = unconsumedInsets.getBottom(this)
+
+ val horizontal = left + right
+ val vertical = top + bottom
+
+ val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
+
+ val width = constraints.constrainWidth(placeable.width + horizontal)
+ val height = constraints.constrainHeight(placeable.height + vertical)
+ return layout(width, height) {
+ placeable.place(left, top)
+ }
+ }
+
+ override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
+ with(scope) {
+ val consumed = ModifierLocalConsumedWindowInsets.current
+ unconsumedInsets = insets.exclude(consumed)
+ consumedInsets = consumed.union(insets)
+ }
+ }
+
+ override val key: ProvidableModifierLocal<WindowInsets>
+ get() = ModifierLocalConsumedWindowInsets
+
+ override val value: WindowInsets
+ get() = consumedInsets
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+ if (other !is InsetsPaddingModifier) {
+ return false
+ }
+
+ return other.insets == insets
+ }
+
+ override fun hashCode(): Int = insets.hashCode()
+}
+
+/**
+ * Base class for arbitrary insets consumption modifiers.
+ */
+@Stable
+private sealed class InsetsConsumingModifier(
+ inspectorInfo: InspectorInfo.() -> Unit
+) : InspectorValueInfo(inspectorInfo), ModifierLocalConsumer, ModifierLocalProvider<WindowInsets> {
+ private var consumedInsets: WindowInsets by mutableStateOf(WindowInsets(0, 0, 0, 0))
+
+ abstract fun calculateInsets(modifierLocalInsets: WindowInsets): WindowInsets
+
+ override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
+ with(scope) {
+ val current = ModifierLocalConsumedWindowInsets.current
+ consumedInsets = calculateInsets(current)
+ }
+ }
+
+ override val key: ProvidableModifierLocal<WindowInsets>
+ get() = ModifierLocalConsumedWindowInsets
+
+ override val value: WindowInsets
+ get() = consumedInsets
+}
+
+@Stable
+private class PaddingValuesConsumingModifier(
+ private val paddingValues: PaddingValues,
+ inspectorInfo: InspectorInfo.() -> Unit
+) : InsetsConsumingModifier(inspectorInfo) {
+ override fun calculateInsets(modifierLocalInsets: WindowInsets): WindowInsets =
+ paddingValues.asInsets().add(modifierLocalInsets)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+ if (other !is PaddingValuesConsumingModifier) {
+ return false
+ }
+
+ return other.paddingValues == paddingValues
+ }
+
+ override fun hashCode(): Int = paddingValues.hashCode()
+}
+
+@Stable
+private class UnionInsetsConsumingModifier(
+ private val insets: WindowInsets,
+ inspectorInfo: InspectorInfo.() -> Unit
+) : InsetsConsumingModifier(inspectorInfo) {
+ override fun calculateInsets(modifierLocalInsets: WindowInsets): WindowInsets =
+ insets.union(modifierLocalInsets)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+ if (other !is UnionInsetsConsumingModifier) {
+ return false
+ }
+
+ return other.insets == insets
+ }
+
+ override fun hashCode(): Int = insets.hashCode()
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsSize.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsSize.kt
new file mode 100644
index 0000000..59cd2a6
--- /dev/null
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/WindowInsetsSize.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.layout
+
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+
+/**
+ * Sets the width to that of [insets] at the [start][androidx.compose.ui.Alignment.Start]
+ * of the screen, using either [left][WindowInsets.getLeft] or [right][WindowInsets.getRight],
+ * depending on the [LayoutDirection].
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.insetsStartWidthSample
+ */
+@Suppress("ModifierInspectorInfo")
+@Stable
+fun Modifier.windowInsetsStartWidth(insets: WindowInsets) = this.then(
+ DerivedWidthModifier(insets, debugInspectorInfo {
+ name = "insetsStartWidth"
+ properties["insets"] = insets
+ }) { layoutDirection, density ->
+ if (layoutDirection == LayoutDirection.Ltr) {
+ getLeft(density, layoutDirection)
+ } else {
+ getRight(density, layoutDirection)
+ }
+ }
+)
+
+/**
+ * Sets the width to that of [insets] at the [end][androidx.compose.ui.Alignment.End]
+ * of the screen, using either [left][WindowInsets.getLeft] or [right][WindowInsets.getRight],
+ * depending on the [LayoutDirection].
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.insetsEndWidthSample
+ */
+@Suppress("ModifierInspectorInfo")
+@Stable
+fun Modifier.windowInsetsEndWidth(insets: WindowInsets) = this.then(
+ DerivedWidthModifier(insets, debugInspectorInfo {
+ name = "insetsEndWidth"
+ properties["insets"] = insets
+ }) { layoutDirection, density ->
+ if (layoutDirection == LayoutDirection.Rtl) {
+ getLeft(density, layoutDirection)
+ } else {
+ getRight(density, layoutDirection)
+ }
+ }
+)
+
+/**
+ * Sets the height to that of [insets] at the [top][WindowInsets.getTop] of the screen.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.insetsTopHeightSample
+ */
+@Suppress("ModifierInspectorInfo")
+@Stable
+fun Modifier.windowInsetsTopHeight(insets: WindowInsets) = this.then(
+ DerivedHeightModifier(insets, debugInspectorInfo {
+ name = "insetsTopHeight"
+ properties["insets"] = insets
+ }) {
+ getTop(it)
+ }
+)
+
+/**
+ * Sets the height to that of [insets] at the [bottom][WindowInsets.getBottom] of the screen.
+ *
+ * When used, the [WindowInsets][android.view.WindowInsets] will be consumed.
+ *
+ * @sample androidx.compose.foundation.layout.samples.insetsBottomHeightSample
+ */
+@Suppress("ModifierInspectorInfo")
+@Stable
+fun Modifier.windowInsetsBottomHeight(insets: WindowInsets) = this.then(
+ DerivedHeightModifier(insets, debugInspectorInfo {
+ name = "insetsBottomHeight"
+ properties["insets"] = insets
+ }) {
+ getBottom(it)
+ }
+)
+
+/**
+ * Sets the width based on [widthCalc]. If the width is 0, the height will also always be 0
+ * and the content will not be placed.
+ */
+@Stable
+private class DerivedWidthModifier(
+ private val insets: WindowInsets,
+ inspectorInfo: InspectorInfo.() -> Unit,
+ private val widthCalc: WindowInsets.(LayoutDirection, Density) -> Int
+) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val width = insets.widthCalc(layoutDirection, this)
+ if (width == 0) {
+ return layout(0, 0) { }
+ }
+ // check for height first
+ val childConstraints = constraints.copy(minWidth = width, maxWidth = width)
+ val placeable = measurable.measure(childConstraints)
+ return layout(width, placeable.height) {
+ placeable.placeRelative(0, 0)
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+ if (other !is DerivedWidthModifier) {
+ return false
+ }
+ return insets == other.insets && widthCalc == other.widthCalc
+ }
+
+ override fun hashCode(): Int = 31 * insets.hashCode() + widthCalc.hashCode()
+}
+
+/**
+ * Sets the height based on [heightCalc]. If the height is 0, the width will also always be 0
+ * and the content will not be placed.
+ */
+@Stable
+private class DerivedHeightModifier(
+ private val insets: WindowInsets,
+ inspectorInfo: InspectorInfo.() -> Unit,
+ private val heightCalc: WindowInsets.(Density) -> Int
+) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val height = insets.heightCalc(this)
+ if (height == 0) {
+ return layout(0, 0) { }
+ }
+ // check for height first
+ val childConstraints = constraints.copy(minHeight = height, maxHeight = height)
+ val placeable = measurable.measure(childConstraints)
+ return layout(placeable.width, height) {
+ placeable.placeRelative(0, 0)
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+ if (other !is DerivedHeightModifier) {
+ return false
+ }
+ return insets == other.insets && heightCalc == other.heightCalc
+ }
+
+ override fun hashCode(): Int = 31 * insets.hashCode() + heightCalc.hashCode()
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextTestCase.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextTestCase.kt
index cb891d7..308b085 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextTestCase.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextTestCase.kt
@@ -30,7 +30,6 @@
import androidx.compose.testutils.LayeredComposeTestCase
import androidx.compose.testutils.ToggleableTestCase
import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalTextInputService
import androidx.compose.ui.text.benchmark.RandomTextGenerator
@@ -101,6 +100,5 @@
override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) {
/*do nothing*/
}
- override fun notifyFocusedRect(rect: Rect) { /*do nothing*/ }
}
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index 9526465..f82b997 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -50,6 +50,7 @@
ComposableDemo("Inside Dialog") { onNavigateUp ->
DialogInputFieldDemo(onNavigateUp)
},
+ ComposableDemo("Inside scrollable") { TextFieldsInScrollableDemo() }
)
),
ComposableDemo("Text Accessibility") { TextAccessibilityDemo() }
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldsInScrollableDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldsInScrollableDemo.kt
new file mode 100644
index 0000000..20deaab
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldsInScrollableDemo.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.foundation.demos.text
+
+import android.app.Activity
+import android.content.ContextWrapper
+import android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
+import android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Switch
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+
+private enum class ScrollableType {
+ ScrollableColumn,
+ LazyColumn
+}
+
+@Composable
+fun TextFieldsInScrollableDemo() {
+ var adjustResize by remember { mutableStateOf(false) }
+ var scrollableType by remember { mutableStateOf(ScrollableType.values().first()) }
+
+ @Suppress("DEPRECATION")
+ SoftInputMode(if (adjustResize) SOFT_INPUT_ADJUST_RESIZE else SOFT_INPUT_ADJUST_PAN)
+
+ Column {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text("ADJUST_PAN")
+ Switch(adjustResize, onCheckedChange = { adjustResize = it })
+ Text("ADJUST_RESIZE")
+ }
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text("Scrollable column")
+ Switch(
+ checked = scrollableType == ScrollableType.LazyColumn,
+ onCheckedChange = {
+ scrollableType = if (it) {
+ ScrollableType.LazyColumn
+ } else {
+ ScrollableType.ScrollableColumn
+ }
+ })
+ Text("LazyColumn")
+ }
+
+ when (scrollableType) {
+ ScrollableType.ScrollableColumn -> TextFieldInScrollableColumn()
+ ScrollableType.LazyColumn -> TextFieldInLazyColumn()
+ }
+ }
+}
+
+@Composable
+fun TextFieldInScrollableColumn() {
+ Column(
+ Modifier.verticalScroll(rememberScrollState())
+ ) {
+ repeat(100) { index ->
+ DemoTextField(index)
+ }
+ }
+}
+
+@Composable
+fun TextFieldInLazyColumn() {
+ LazyColumn {
+ items(100) { index ->
+ DemoTextField(index)
+ }
+ }
+}
+
+@Composable
+private fun DemoTextField(index: Int) {
+ var text by rememberSaveable { mutableStateOf("") }
+ TextField(
+ value = text,
+ onValueChange = { text = it },
+ leadingIcon = { Text(index.toString()) },
+ modifier = Modifier
+ .padding(4.dp)
+ .border(1.dp, Color.Black)
+ .fillMaxWidth()
+ )
+}
+
+/**
+ * Sets the window's [softInputMode][android.view.Window.setSoftInputMode] to [mode] as long as this
+ * function is composed.
+ */
+@Composable
+private fun SoftInputMode(mode: Int) {
+ val context = LocalContext.current
+ DisposableEffect(context, mode) {
+ val activity = generateSequence(context) { (context as? ContextWrapper)?.baseContext }
+ .filterIsInstance<Activity>()
+ .firstOrNull()
+ ?: return@DisposableEffect onDispose {}
+ val originalMode = activity.window.attributes.softInputMode
+ activity.window.setSoftInputMode(mode)
+ onDispose {
+ activity.window.setSoftInputMode(originalMode)
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt
new file mode 100644
index 0000000..421bdef
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import android.app.Activity
+import android.content.Context
+import android.content.ContextWrapper
+import android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
+import android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.CoreTextFieldKeyboardScrollableInteractionTest.ScrollableType.LazyList
+import androidx.compose.foundation.text.CoreTextFieldKeyboardScrollableInteractionTest.ScrollableType.ScrollableColumn
+import androidx.compose.foundation.text.CoreTextFieldKeyboardScrollableInteractionTest.SoftInputMode.AdjustPan
+import androidx.compose.foundation.text.CoreTextFieldKeyboardScrollableInteractionTest.SoftInputMode.AdjustResize
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.click
+import androidx.compose.ui.test.isFocused
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.dp
+import org.junit.Assume.assumeTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
+
+@RunWith(Parameterized::class)
+class CoreTextFieldKeyboardScrollableInteractionTest(
+ private val scrollableType: ScrollableType,
+ private val softInputMode: SoftInputMode,
+ private val withDecorationPadding: Boolean,
+) {
+ enum class ScrollableType {
+ ScrollableColumn,
+ LazyList
+ }
+
+ enum class SoftInputMode {
+ AdjustResize,
+ AdjustPan
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "scrollableType={0}, softInputMode={1}, withDecorationPadding={2}")
+ fun parameters(): Iterable<Array<*>> = crossProductOf(
+ ScrollableType.values(),
+ SoftInputMode.values(),
+ arrayOf(false, true), // withDecorationPadding
+ )
+ }
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private val ListTag = "list"
+ private val keyboardHelper = KeyboardHelper(rule)
+
+ @Test
+ fun test() {
+ // TODO(b/192043120) This is known broken when soft input mode is Resize.
+ assumeTrue(softInputMode != AdjustResize)
+
+ rule.setContent {
+ keyboardHelper.view = LocalView.current
+ TestContent()
+ }
+
+ // This test is all about the keyboard going from hidden to shown, so hide it to start.
+ keyboardHelper.hideKeyboardIfShown()
+
+ rule.onNodeWithTag(ListTag)
+ .performTouchInput {
+ // Click one pixel above the bottom of the list.
+ click(bottomCenter - Offset(0f, 1f))
+ }
+ keyboardHelper.waitForKeyboardVisibility(visible = true)
+
+ rule.onNode(isFocused()).assertIsDisplayed()
+ }
+
+ @Composable
+ fun TestContent() {
+ @Suppress("DEPRECATION")
+ SoftInputMode(
+ when (softInputMode) {
+ AdjustResize -> SOFT_INPUT_ADJUST_RESIZE
+ AdjustPan -> SOFT_INPUT_ADJUST_PAN
+ }
+ )
+
+ val itemCount = 100
+ when (scrollableType) {
+ ScrollableColumn -> {
+ Column(
+ Modifier
+ .testTag(ListTag)
+ .verticalScroll(rememberScrollState())
+ ) {
+ repeat(itemCount) { index ->
+ TestTextField(index)
+ }
+ }
+ }
+ LazyList -> {
+ LazyColumn(Modifier.testTag(ListTag)) {
+ items(itemCount) { index ->
+ TestTextField(index)
+ }
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun TestTextField(index: Int) {
+ var isFocused by remember { mutableStateOf(false) }
+ CoreTextField(
+ value = TextFieldValue(text = index.toString()),
+ onValueChange = {},
+ modifier = Modifier
+ .fillMaxWidth()
+ .drawWithContent {
+ drawContent()
+ if (isFocused) {
+ drawRect(Color.Blue, style = Stroke(2.dp.toPx()))
+ }
+ }
+ .onFocusChanged { isFocused = it.isFocused }
+ .testTag(index.toString()),
+ decorationBox = { inner ->
+ if (withDecorationPadding) {
+ Box(Modifier.padding(vertical = 24.dp)) {
+ inner()
+ }
+ } else {
+ inner()
+ }
+ }
+ )
+ }
+
+ @Composable
+ private fun SoftInputMode(mode: Int) {
+ val context = LocalContext.current
+ DisposableEffect(mode) {
+ val activity = context.findActivityOrNull() ?: return@DisposableEffect onDispose {}
+ val originalMode = activity.window.attributes.softInputMode
+ activity.window.setSoftInputMode(mode)
+ onDispose {
+ activity.window.setSoftInputMode(originalMode)
+ }
+ }
+ }
+
+ private tailrec fun Context.findActivityOrNull(): Activity? {
+ return (this as? Activity)
+ ?: (this as? ContextWrapper)?.baseContext?.findActivityOrNull()
+ }
+}
+
+private fun crossProductOf(vararg values: Array<*>): List<Array<*>> =
+ crossProductOf(values.map { it.asSequence() })
+ .map { it.toList().toTypedArray() }
+ .toList()
+
+private fun crossProductOf(values: List<Sequence<*>>): Sequence<Sequence<*>> =
+ when (values.size) {
+ 0 -> emptySequence()
+ 1 -> values[0].map { sequenceOf(it) }
+ else -> sequence {
+ for (subProduct in crossProductOf(values.subList(1, values.size)))
+ for (firstValue in values[0]) {
+ yield(sequenceOf(firstValue) + subProduct)
+ }
+ }
+ }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
index ae3ed31..6cdc2c7 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
@@ -17,10 +17,6 @@
package androidx.compose.foundation.text
import android.os.Build
-import android.view.View
-import android.view.WindowInsets
-import android.view.WindowInsetsAnimation
-import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
@@ -39,12 +35,9 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
-import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
@LargeTest
@RunWith(AndroidJUnit4::class)
@@ -52,9 +45,9 @@
@get:Rule
val rule = createComposeRule()
- private lateinit var view: View
private lateinit var focusManager: FocusManager
private val timeout = 15_000L
+ private val keyboardHelper = KeyboardHelper(rule, timeout)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
@Test
@@ -72,7 +65,7 @@
rule.onNodeWithTag("TextField1").performClick()
// Assert.
- view.waitUntil(timeout) { view.isSoftwareKeyboardShown() }
+ keyboardHelper.waitForKeyboardVisibility(visible = true)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
@@ -92,7 +85,7 @@
rule.runOnIdle { focusRequester.requestFocus() }
// Assert.
- view.waitUntil(timeout) { view.isSoftwareKeyboardShown() }
+ keyboardHelper.waitForKeyboardVisibility(visible = true)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
@@ -109,13 +102,13 @@
}
// Request focus and wait for keyboard.
rule.runOnIdle { focusRequester.requestFocus() }
- view.waitUntil(timeout) { view.isSoftwareKeyboardShown() }
+ keyboardHelper.waitForKeyboardVisibility(visible = true)
// Act.
rule.runOnIdle { focusManager.clearFocus() }
// Assert.
- view.waitUntil(timeout) { !view.isSoftwareKeyboardShown() }
+ keyboardHelper.waitForKeyboardVisibility(visible = false)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
@@ -123,7 +116,6 @@
fun keyboardShownAfterDismissingKeyboardAndClickingAgain() {
// Arrange.
rule.setContentForTest {
- view = LocalView.current
CoreTextField(
value = TextFieldValue("Hello"),
onValueChange = {},
@@ -131,15 +123,15 @@
)
}
rule.onNodeWithTag("TextField1").performClick()
- view.waitUntil(timeout) { view.isSoftwareKeyboardShown() }
+ keyboardHelper.waitForKeyboardVisibility(visible = true)
// Act.
- rule.runOnIdle { view.hideKeyboard() }
- view.waitUntil(timeout) { !view.isSoftwareKeyboardShown() }
+ rule.runOnIdle { keyboardHelper.hideKeyboard() }
+ keyboardHelper.waitForKeyboardVisibility(visible = false)
rule.onNodeWithTag("TextField1").performClick()
// Assert.
- view.waitUntil(timeout) { view.isSoftwareKeyboardShown() }
+ keyboardHelper.waitForKeyboardVisibility(visible = true)
}
@OptIn(ExperimentalComposeUiApi::class)
@@ -163,64 +155,23 @@
}
}
rule.runOnIdle { focusRequester1.requestFocus() }
- view.waitUntil(timeout) { view.isSoftwareKeyboardShown() }
+ keyboardHelper.waitForKeyboardVisibility(visible = true)
// Act.
rule.runOnIdle { focusRequester2.requestFocus() }
// Assert.
- view.waitUntil(timeout) { !view.isSoftwareKeyboardShown() }
+ keyboardHelper.waitForKeyboardVisibility(visible = false)
}
private fun ComposeContentTestRule.setContentForTest(composable: @Composable () -> Unit) {
setContent {
- view = LocalView.current
+ keyboardHelper.view = LocalView.current
focusManager = LocalFocusManager.current
composable()
}
// We experienced some flakiness in tests if the keyboard was visible at the start of the
// test. So we make sure that the keyboard is hidden at the start of every test.
- runOnIdle {
- if (view.isSoftwareKeyboardShown()) {
- view.hideKeyboard()
- view.waitUntil(timeout) { !view.isSoftwareKeyboardShown() }
- }
- }
+ keyboardHelper.hideKeyboardIfShown()
}
}
-
-private fun View.waitUntil(timeoutMillis: Long, condition: () -> Boolean) {
- val latch = CountDownLatch(1)
- rootView.setWindowInsetsAnimationCallback(
- InsetAnimationCallback {
- if (condition()) { latch.countDown() }
- }
- )
- val conditionMet = latch.await(timeoutMillis, TimeUnit.MILLISECONDS)
- assertThat(conditionMet).isTrue()
-}
-
-@RequiresApi(Build.VERSION_CODES.R)
-private class InsetAnimationCallback(val block: () -> Unit) :
- WindowInsetsAnimation.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
-
- override fun onProgress(
- insets: WindowInsets,
- runningAnimations: MutableList<WindowInsetsAnimation>
- ) = insets
-
- override fun onEnd(animation: WindowInsetsAnimation) {
- block()
- super.onEnd(animation)
- }
-}
-
-@RequiresApi(Build.VERSION_CODES.R)
-private fun View.isSoftwareKeyboardShown(): Boolean {
- return rootWindowInsets != null && rootWindowInsets.isVisible(WindowInsets.Type.ime())
-}
-
-@RequiresApi(Build.VERSION_CODES.R)
-private fun View.hideKeyboard() {
- windowInsetsController?.hide(WindowInsets.Type.ime())
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardHelper.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardHelper.kt
new file mode 100644
index 0000000..87fe0e8
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardHelper.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import android.content.Context
+import android.os.Build
+import android.view.View
+import android.view.WindowInsets
+import android.view.WindowInsetsAnimation
+import android.view.inputmethod.InputMethodManager
+import androidx.annotation.RequiresApi
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+/**
+ * Helper methods for hiding and showing the keyboard in tests.
+ * Must set [view] before calling any methods on this class.
+ */
+class KeyboardHelper(
+ private val composeRule: ComposeTestRule,
+ private val timeout: Long = 15_000L
+) {
+ /**
+ * The [View] hosting the compose rule's content. Must be set before calling any methods on this
+ * class.
+ */
+ lateinit var view: View
+ private val imm by lazy {
+ view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ }
+
+ /**
+ * Requests the keyboard to be hidden without waiting for it.
+ * Should be called from the main thread.
+ */
+ fun hideKeyboard() {
+ if (Build.VERSION.SDK_INT >= 30) {
+ hideKeyboardWithInsets()
+ } else {
+ hideKeyboardWithImm()
+ }
+ }
+
+ /**
+ * Blocks until the [timeout] or the keyboard's visibility matches [visible].
+ * May be called from the test thread or the main thread.
+ */
+ fun waitForKeyboardVisibility(visible: Boolean) {
+ waitUntil(timeout) {
+ isSoftwareKeyboardShown() == visible
+ }
+ }
+
+ fun hideKeyboardIfShown() {
+ composeRule.runOnIdle {
+ if (isSoftwareKeyboardShown()) {
+ hideKeyboard()
+ waitForKeyboardVisibility(visible = false)
+ }
+ }
+ }
+
+ private fun isSoftwareKeyboardShown(): Boolean {
+ return if (Build.VERSION.SDK_INT >= 30) {
+ isSoftwareKeyboardShownWithInsets()
+ } else {
+ isSoftwareKeyboardShownWithImm()
+ }
+ }
+
+ @RequiresApi(30)
+ private fun isSoftwareKeyboardShownWithInsets(): Boolean {
+ return view.rootWindowInsets != null &&
+ view.rootWindowInsets.isVisible(WindowInsets.Type.ime())
+ }
+
+ private fun isSoftwareKeyboardShownWithImm(): Boolean {
+ // TODO(b/163742556): This is just a proxy for software keyboard visibility. Find a better
+ // way to check if the software keyboard is shown.
+ return imm.isAcceptingText
+ }
+
+ private fun hideKeyboardWithImm() {
+ imm.hideSoftInputFromWindow(view.windowToken, 0)
+ }
+
+ @RequiresApi(30)
+ private fun hideKeyboardWithInsets() {
+ view.windowInsetsController?.hide(WindowInsets.Type.ime())
+ }
+
+ private fun waitUntil(timeout: Long, condition: () -> Boolean) {
+ if (Build.VERSION.SDK_INT >= 30) {
+ view.waitUntil(timeout, condition)
+ } else {
+ composeRule.waitUntil(timeout, condition)
+ }
+ }
+}
+
+@RequiresApi(30)
+fun View.waitUntil(timeoutMillis: Long, condition: () -> Boolean) {
+ val latch = CountDownLatch(1)
+ rootView.setWindowInsetsAnimationCallback(
+ InsetAnimationCallback {
+ if (condition()) {
+ latch.countDown()
+ }
+ }
+ )
+ val conditionMet = latch.await(timeoutMillis, TimeUnit.MILLISECONDS)
+ assertThat(conditionMet).isTrue()
+}
+
+@RequiresApi(30)
+private class InsetAnimationCallback(val block: () -> Unit) :
+ WindowInsetsAnimation.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
+
+ override fun onProgress(
+ insets: WindowInsets,
+ runningAnimations: MutableList<WindowInsetsAnimation>
+ ) = insets
+
+ override fun onEnd(animation: WindowInsetsAnimation) {
+ block()
+ super.onEnd(animation)
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
index 67e42d6..ff69b33 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
@@ -62,6 +62,17 @@
val scope = rememberCoroutineScope()
val focusedInteraction = remember { mutableStateOf<FocusInteraction.Focus?>(null) }
var isFocused by remember { mutableStateOf(false) }
+
+ // Focusables have a few different cases where they need to make sure they stay visible:
+ //
+ // 1. Focusable node newly receives focus – always bring entire node into view. That's what this
+ // BringIntoViewRequester does.
+ // 2. Scrollable parent resizes and the currently-focused item is now hidden – bring entire node
+ // into view if it was also in view before the resize. This handles the case of
+ // `softInputMode=ADJUST_RESIZE`. See b/216842427.
+ // 3. Entire window is panned due to `softInputMode=ADJUST_PAN` – report the correct focused
+ // rect to the view system, and the view system itself will keep the focused area in view.
+ // See aosp/1964580.
@OptIn(ExperimentalFoundationApi::class)
val bringIntoViewRequester = remember { BringIntoViewRequester() }
DisposableEffect(interactionSource) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 348fa20..aabc044 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -16,10 +16,13 @@
package androidx.compose.foundation.text
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.relocation.BringIntoViewRequester
+import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.SelectionHandleInfo
import androidx.compose.foundation.text.selection.SelectionHandleInfoKey
@@ -35,6 +38,7 @@
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
@@ -42,6 +46,7 @@
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Paint
@@ -100,6 +105,7 @@
import androidx.compose.ui.unit.Density
import kotlin.math.max
import kotlin.math.roundToInt
+import kotlinx.coroutines.launch
/**
* Base composable that enables users to edit text via hardware or software keyboard.
@@ -163,7 +169,7 @@
* innerTextField exactly once.
*/
@Composable
-@OptIn(InternalFoundationTextApi::class)
+@OptIn(InternalFoundationTextApi::class, ExperimentalFoundationApi::class)
internal fun CoreTextField(
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
@@ -256,6 +262,9 @@
manager.focusRequester = focusRequester
manager.editable = !readOnly
+ val coroutineScope = rememberCoroutineScope()
+ val bringIntoViewRequester = remember { BringIntoViewRequester() }
+
// Focus
val focusModifier = Modifier.textFieldFocusModifier(
enabled = enabled,
@@ -272,9 +281,28 @@
textInputService,
state,
value,
- imeOptions,
- offsetMapping
+ imeOptions
)
+
+ // The focusable modifier itself will request the entire focusable be brought into view
+ // when it gains focus – in this case, that's the decoration box. However, since text
+ // fields may have their own internal scrolling, and the decoration box can do anything,
+ // we also need to specifically request that the cursor itself be brought into view.
+ // TODO(b/216790855) If this request happens after the focusable's request, the field
+ // will only be scrolled far enough to show the cursor, _not_ the entire decoration
+ // box.
+ if (it.isFocused) {
+ state.layoutResult?.let { layoutResult ->
+ coroutineScope.launch {
+ bringIntoViewRequester.bringSelectionEndIntoView(
+ value,
+ state.textDelegate,
+ layoutResult.value,
+ offsetMapping
+ )
+ }
+ }
+ }
}
if (!it.isFocused) manager.deselect()
}
@@ -342,19 +370,6 @@
state.showCursorHandle =
manager.isSelectionHandleInVisibleBound(isStartHandle = true)
}
- state.layoutResult?.let { layoutResult ->
- state.inputSession?.let { inputSession ->
- TextFieldDelegate.notifyFocusedRect(
- value,
- state.textDelegate,
- layoutResult.value,
- it,
- inputSession,
- state.hasFocus,
- offsetMapping
- )
- }
- }
}
state.layoutResult?.innerTextFieldCoordinates = it
}
@@ -520,6 +535,7 @@
.textFieldMinSize(textStyle)
.then(onPositionedModifier)
.then(magnifierModifier)
+ .bringIntoViewRequester(bringIntoViewRequester)
SimpleLayout(coreTextFieldModifier) {
Layout(
@@ -789,13 +805,12 @@
}
}
-@OptIn(InternalFoundationTextApi::class)
+@OptIn(InternalFoundationTextApi::class, ExperimentalFoundationApi::class)
private fun notifyTextInputServiceOnFocusChange(
textInputService: TextInputService,
state: TextFieldState,
value: TextFieldValue,
- imeOptions: ImeOptions,
- offsetMapping: OffsetMapping
+ imeOptions: ImeOptions
) {
if (state.hasFocus) {
state.inputSession = TextFieldDelegate.onFocus(
@@ -805,21 +820,7 @@
imeOptions,
state.onValueChange,
state.onImeActionPerformed
- ).also { newSession ->
- state.layoutCoordinates?.let { coords ->
- state.layoutResult?.let { layoutResult ->
- TextFieldDelegate.notifyFocusedRect(
- value,
- state.textDelegate,
- layoutResult.value,
- coords,
- newSession,
- state.hasFocus,
- offsetMapping
- )
- }
- }
- }
+ )
} else {
state.inputSession?.let { session ->
TextFieldDelegate.onBlur(session, state.processor, state.onValueChange)
@@ -828,6 +829,54 @@
}
}
+/**
+ * Calculates the location of the end of the current selection and requests that it be brought into
+ * view using [bringIntoView][BringIntoViewRequester.bringIntoView].
+ *
+ * Text fields have a lot of different edge cases where they need to make sure they stay visible:
+ *
+ * 1. Focusable node newly receives focus – always bring entire node into view.
+ * 2. Unfocused text field is tapped – always bring cursor area into view (conflicts with above, see
+ * b/216790855).
+ * 3. Focused text field is tapped – always bring cursor area into view.
+ * 4. Text input occurs – always bring cursor area into view.
+ * 5. Scrollable parent resizes and the currently-focused item is now hidden – bring entire node
+ * into view if it was also in view before the resize. This handles the case of
+ * `softInputMode=ADJUST_RESIZE`. See b/216842427.
+ * 6. Entire window is panned due to `softInputMode=ADJUST_PAN` – report the correct focused rect to
+ * the view system, and the view system itself will keep the focused area in view.
+ * See aosp/1964580.
+ *
+ * This function is used to handle 2, 3, and 4, and the others are automatically handled by the
+ * focus system.
+ */
+@OptIn(ExperimentalFoundationApi::class, InternalFoundationTextApi::class)
+internal suspend fun BringIntoViewRequester.bringSelectionEndIntoView(
+ value: TextFieldValue,
+ textDelegate: TextDelegate,
+ textLayoutResult: TextLayoutResult,
+ offsetMapping: OffsetMapping
+) {
+ val selectionEndInTransformed = offsetMapping.originalToTransformed(value.selection.max)
+ val selectionEndBounds = when {
+ selectionEndInTransformed < textLayoutResult.layoutInput.text.length -> {
+ textLayoutResult.getBoundingBox(selectionEndInTransformed)
+ }
+ selectionEndInTransformed != 0 -> {
+ textLayoutResult.getBoundingBox(selectionEndInTransformed - 1)
+ }
+ else -> { // empty text.
+ val defaultSize = computeSizeForDefaultText(
+ textDelegate.style,
+ textDelegate.density,
+ textDelegate.fontFamilyResolver
+ )
+ Rect(0f, 0f, 1.0f, defaultSize.height.toFloat())
+ }
+ }
+ bringIntoView(selectionEndBounds)
+}
+
@Composable
private fun SelectionToolbarAndHandles(manager: TextFieldSelectionManager, show: Boolean) {
if (show) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt
index 2f001a7..c892fcc 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt
@@ -17,11 +17,8 @@
package androidx.compose.foundation.text
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Paint
-import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.Paragraph
import androidx.compose.ui.text.SpanStyle
@@ -134,55 +131,6 @@
}
/**
- * Notify system that focused input area.
- *
- * System is typically scrolled up not to be covered by keyboard.
- *
- * @param value The editor model
- * @param textDelegate The text delegate
- * @param layoutCoordinates The layout coordinates
- * @param textInputSession The current input session.
- * @param hasFocus True if focus is gained.
- * @param offsetMapping The mapper from/to editing buffer to/from visible text.
- */
- @JvmStatic
- internal fun notifyFocusedRect(
- value: TextFieldValue,
- textDelegate: TextDelegate,
- textLayoutResult: TextLayoutResult,
- layoutCoordinates: LayoutCoordinates,
- textInputSession: TextInputSession,
- hasFocus: Boolean,
- offsetMapping: OffsetMapping
- ) {
- if (!hasFocus) {
- return
- }
- val focusOffsetInTransformed = offsetMapping.originalToTransformed(value.selection.max)
- val bbox = when {
- focusOffsetInTransformed < textLayoutResult.layoutInput.text.length -> {
- textLayoutResult.getBoundingBox(focusOffsetInTransformed)
- }
- focusOffsetInTransformed != 0 -> {
- textLayoutResult.getBoundingBox(focusOffsetInTransformed - 1)
- }
- else -> { // empty text.
- val defaultSize = computeSizeForDefaultText(
- textDelegate.style,
- textDelegate.density,
- textDelegate.fontFamilyResolver
- )
- Rect(0f, 0f, 1.0f, defaultSize.height.toFloat())
- }
- }
- val globalLT = layoutCoordinates.localToRoot(Offset(bbox.left, bbox.top))
-
- textInputSession.notifyFocusedRect(
- Rect(Offset(globalLT.x, globalLT.y), Size(bbox.width, bbox.height))
- )
- }
-
- /**
* Called when edit operations are passed from TextInputService
*
* @param ops A list of edit operations.
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldBringIntoViewTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldBringIntoViewTest.kt
new file mode 100644
index 0000000..9296223
--- /dev/null
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldBringIntoViewTest.kt
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.relocation.BringIntoViewRequester
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.AlignmentLine
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextLayoutInput
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.OffsetMapping
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import com.nhaarman.mockitokotlin2.any
+import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.reset
+import com.nhaarman.mockitokotlin2.verify
+import com.nhaarman.mockitokotlin2.verifyBlocking
+import com.nhaarman.mockitokotlin2.whenever
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(InternalFoundationTextApi::class, ExperimentalFoundationApi::class)
+@RunWith(JUnit4::class)
+class TextFieldBringIntoViewTest {
+
+ private val delegate: TextDelegate = mock()
+ private var layoutCoordinates: LayoutCoordinates = mock()
+ private val textLayoutResultProxy: TextLayoutResultProxy = mock()
+ private val textLayoutResult: TextLayoutResult = mock()
+ private val bringIntoViewRequester: BringIntoViewRequester = mock()
+
+ /**
+ * Test implementation of offset map which doubles the offset in transformed text.
+ */
+ private val skippingOffsetMap = object : OffsetMapping {
+ override fun originalToTransformed(offset: Int): Int = offset * 2
+ override fun transformedToOriginal(offset: Int): Int = offset / 2
+ }
+
+ @Before
+ fun setup() {
+ whenever(textLayoutResultProxy.value).thenReturn(textLayoutResult)
+ }
+
+ @Test
+ fun notify_focused_rect() {
+ val rect = Rect(0f, 1f, 2f, 3f)
+ whenever(textLayoutResult.getBoundingBox(any())).thenReturn(rect)
+ val point = Offset(5f, 6f)
+ layoutCoordinates = MockCoordinates(
+ rootOffset = point
+ )
+ val editorState = TextFieldValue(text = "Hello, World", selection = TextRange(1))
+
+ val input = TextLayoutInput(
+ text = AnnotatedString(editorState.text),
+ style = TextStyle(),
+ placeholders = listOf(),
+ maxLines = Int.MAX_VALUE,
+ softWrap = true,
+ overflow = TextOverflow.Clip,
+ density = Density(1.0f),
+ layoutDirection = LayoutDirection.Ltr,
+ fontFamilyResolver = mock(),
+ constraints = mock()
+ )
+ whenever(textLayoutResult.layoutInput).thenReturn(input)
+
+ runBlocking {
+ bringIntoViewRequester.bringSelectionEndIntoView(
+ editorState,
+ delegate,
+ textLayoutResult,
+ OffsetMapping.Identity
+ )
+ }
+ verifyBlocking(bringIntoViewRequester) { bringIntoView(rect) }
+ }
+
+ @Test
+ fun notify_rect_tail() {
+ val rect = Rect(0f, 1f, 2f, 3f)
+ whenever(textLayoutResult.getBoundingBox(any())).thenReturn(rect)
+ val point = Offset(5f, 6f)
+ layoutCoordinates = MockCoordinates(
+ rootOffset = point
+ )
+ val editorState = TextFieldValue(text = "Hello, World", selection = TextRange(12))
+ val input = TextLayoutInput(
+ text = AnnotatedString(editorState.text),
+ style = TextStyle(),
+ placeholders = listOf(),
+ maxLines = Int.MAX_VALUE,
+ softWrap = true,
+ overflow = TextOverflow.Clip,
+ density = Density(1.0f),
+ layoutDirection = LayoutDirection.Ltr,
+ fontFamilyResolver = mock(),
+ constraints = mock()
+ )
+ whenever(textLayoutResult.layoutInput).thenReturn(input)
+
+ runBlocking {
+ bringIntoViewRequester.bringSelectionEndIntoView(
+ editorState,
+ delegate,
+ textLayoutResult,
+ OffsetMapping.Identity
+ )
+ }
+ verifyBlocking(bringIntoViewRequester) { bringIntoView(rect) }
+ }
+
+ @Test
+ fun check_notify_rect_uses_offset_map() {
+ val rect = Rect(0f, 1f, 2f, 3f)
+ val point = Offset(5f, 6f)
+ val editorState = TextFieldValue(text = "Hello, World", selection = TextRange(1, 3))
+
+ whenever(textLayoutResult.getBoundingBox(any())).thenReturn(rect)
+ val input = TextLayoutInput(
+ text = AnnotatedString(editorState.text),
+ style = TextStyle(),
+ placeholders = listOf(),
+ maxLines = Int.MAX_VALUE,
+ softWrap = true,
+ overflow = TextOverflow.Clip,
+ density = Density(1.0f),
+ layoutDirection = LayoutDirection.Ltr,
+ fontFamilyResolver = mock(),
+ constraints = mock()
+ )
+ whenever(textLayoutResult.layoutInput).thenReturn(input)
+ layoutCoordinates = MockCoordinates(
+ rootOffset = point
+ )
+
+ runBlocking {
+ bringIntoViewRequester.bringSelectionEndIntoView(
+ editorState,
+ delegate,
+ textLayoutResult,
+ skippingOffsetMap
+ )
+ }
+ verify(textLayoutResult).getBoundingBox(6)
+ verifyBlocking(bringIntoViewRequester) { bringIntoView(rect) }
+ }
+
+ @Test
+ fun notify_transformed_text() {
+ val rect = Rect(0f, 1f, 2f, 3f)
+ whenever(textLayoutResult.getBoundingBox(any())).thenReturn(rect)
+ val point = Offset(5f, 6f)
+ layoutCoordinates = MockCoordinates(
+ rootOffset = point
+ )
+
+ val input = TextLayoutInput(
+ // In this test case, transform the text into double characters text.
+ text = AnnotatedString("HHeelllloo,, WWoorrlldd"),
+ style = TextStyle(),
+ placeholders = listOf(),
+ maxLines = Int.MAX_VALUE,
+ softWrap = true,
+ overflow = TextOverflow.Clip,
+ density = Density(1.0f),
+ layoutDirection = LayoutDirection.Ltr,
+ fontFamilyResolver = mock(),
+ constraints = mock()
+ )
+ whenever(textLayoutResult.layoutInput).thenReturn(input)
+
+ val offsetMapping = object : OffsetMapping {
+ override fun originalToTransformed(offset: Int): Int = offset * 2
+ override fun transformedToOriginal(offset: Int): Int = offset / 2
+ }
+
+ // The beginning of the text.
+ runBlocking {
+ bringIntoViewRequester.bringSelectionEndIntoView(
+ TextFieldValue(text = "Hello, World", selection = TextRange(0)),
+ delegate,
+ textLayoutResult,
+ offsetMapping
+ )
+ }
+ verifyBlocking(bringIntoViewRequester) { bringIntoView(rect) }
+
+ // The tail of the transformed text.
+ reset(bringIntoViewRequester)
+ runBlocking {
+ bringIntoViewRequester.bringSelectionEndIntoView(
+ TextFieldValue(text = "Hello, World", selection = TextRange(24)),
+ delegate,
+ textLayoutResult,
+ offsetMapping
+ )
+ }
+ verifyBlocking(bringIntoViewRequester) { bringIntoView(rect) }
+
+ // Beyond the tail of the transformed text.
+ reset(bringIntoViewRequester)
+ runBlocking {
+ bringIntoViewRequester.bringSelectionEndIntoView(
+ TextFieldValue(text = "Hello, World", selection = TextRange(25)),
+ delegate,
+ textLayoutResult,
+ offsetMapping
+ )
+ }
+ verifyBlocking(bringIntoViewRequester) { bringIntoView(rect) }
+ }
+
+ private class MockCoordinates(
+ override val size: IntSize = IntSize.Zero,
+ val localOffset: Offset = Offset.Zero,
+ val globalOffset: Offset = Offset.Zero,
+ val rootOffset: Offset = Offset.Zero
+ ) : LayoutCoordinates {
+ override val providedAlignmentLines: Set<AlignmentLine>
+ get() = emptySet()
+ override val parentLayoutCoordinates: LayoutCoordinates?
+ get() = null
+ override val parentCoordinates: LayoutCoordinates?
+ get() = null
+ override val isAttached: Boolean
+ get() = true
+
+ override fun windowToLocal(relativeToWindow: Offset): Offset = localOffset
+
+ override fun localToWindow(relativeToLocal: Offset): Offset = globalOffset
+
+ override fun localToRoot(relativeToLocal: Offset): Offset = rootOffset
+ override fun localPositionOf(
+ sourceCoordinates: LayoutCoordinates,
+ relativeToSource: Offset
+ ): Offset = Offset.Zero
+
+ override fun localBoundingBoxOf(
+ sourceCoordinates: LayoutCoordinates,
+ clipBounds: Boolean
+ ): Rect = Rect.Zero
+
+ override fun get(alignmentLine: AlignmentLine): Int = 0
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldDelegateTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldDelegateTest.kt
index 6ec1a8a..d586f8f 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldDelegateTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldDelegateTest.kt
@@ -17,18 +17,14 @@
package androidx.compose.foundation.text
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.MultiParagraphIntrinsics
import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.TextLayoutInput
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.EditProcessor
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.ImeOptions
@@ -41,17 +37,11 @@
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextDecoration
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.inOrder
import com.nhaarman.mockitokotlin2.mock
-import com.nhaarman.mockitokotlin2.never
-import com.nhaarman.mockitokotlin2.reset
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
@@ -184,133 +174,6 @@
}
@Test
- fun notify_focused_rect() {
- val rect = Rect(0f, 1f, 2f, 3f)
- whenever(textLayoutResult.getBoundingBox(any())).thenReturn(rect)
- val point = Offset(5f, 6f)
- layoutCoordinates = MockCoordinates(
- rootOffset = point
- )
- val editorState = TextFieldValue(text = "Hello, World", selection = TextRange(1))
- val textInputSession: TextInputSession = mock()
-
- val input = TextLayoutInput(
- text = AnnotatedString(editorState.text),
- style = TextStyle(),
- placeholders = listOf(),
- maxLines = Int.MAX_VALUE,
- softWrap = true,
- overflow = TextOverflow.Clip,
- density = Density(1.0f),
- layoutDirection = LayoutDirection.Ltr,
- fontFamilyResolver = mock(),
- constraints = mock()
- )
- whenever(textLayoutResult.layoutInput).thenReturn(input)
-
- TextFieldDelegate.notifyFocusedRect(
- editorState,
- mDelegate,
- textLayoutResult,
- layoutCoordinates,
- textInputSession,
- true /* hasFocus */,
- OffsetMapping.Identity
- )
- verify(textInputSession).notifyFocusedRect(any())
- }
-
- @Test
- fun notify_focused_rect_without_focus() {
- val editorState = TextFieldValue(text = "Hello, World", selection = TextRange(1))
- val textInputSession: TextInputSession = mock()
- TextFieldDelegate.notifyFocusedRect(
- editorState,
- mDelegate,
- textLayoutResult,
- layoutCoordinates,
- textInputSession,
- false /* hasFocus */,
- OffsetMapping.Identity
- )
- verify(textInputSession, never()).notifyFocusedRect(any())
- }
-
- @Test
- fun notify_rect_tail() {
- val rect = Rect(0f, 1f, 2f, 3f)
- whenever(textLayoutResult.getBoundingBox(any())).thenReturn(rect)
- val point = Offset(5f, 6f)
- layoutCoordinates = MockCoordinates(
- rootOffset = point
- )
- val editorState = TextFieldValue(text = "Hello, World", selection = TextRange(12))
- val textInputSession: TextInputSession = mock()
- val input = TextLayoutInput(
- text = AnnotatedString(editorState.text),
- style = TextStyle(),
- placeholders = listOf(),
- maxLines = Int.MAX_VALUE,
- softWrap = true,
- overflow = TextOverflow.Clip,
- density = Density(1.0f),
- layoutDirection = LayoutDirection.Ltr,
- fontFamilyResolver = mock(),
- constraints = mock()
- )
- whenever(textLayoutResult.layoutInput).thenReturn(input)
-
- TextFieldDelegate.notifyFocusedRect(
- editorState,
- mDelegate,
- textLayoutResult,
- layoutCoordinates,
- textInputSession,
- true /* hasFocus */,
- OffsetMapping.Identity
- )
- verify(textInputSession).notifyFocusedRect(any())
- }
-
- @Test
- fun check_notify_rect_uses_offset_map() {
- val rect = Rect(0f, 1f, 2f, 3f)
- val point = Offset(5f, 6f)
- val editorState = TextFieldValue(text = "Hello, World", selection = TextRange(1, 3))
-
- whenever(textLayoutResult.getBoundingBox(any())).thenReturn(rect)
- val input = TextLayoutInput(
- text = AnnotatedString(editorState.text),
- style = TextStyle(),
- placeholders = listOf(),
- maxLines = Int.MAX_VALUE,
- softWrap = true,
- overflow = TextOverflow.Clip,
- density = Density(1.0f),
- layoutDirection = LayoutDirection.Ltr,
- fontFamilyResolver = mock(),
- constraints = mock()
- )
- whenever(textLayoutResult.layoutInput).thenReturn(input)
- layoutCoordinates = MockCoordinates(
- rootOffset = point
- )
- val textInputSession: TextInputSession = mock()
-
- TextFieldDelegate.notifyFocusedRect(
- editorState,
- mDelegate,
- textLayoutResult,
- layoutCoordinates,
- textInputSession,
- true /* hasFocus */,
- skippingOffsetMap
- )
- verify(textLayoutResult).getBoundingBox(6)
- verify(textInputSession).notifyFocusedRect(any())
- }
-
- @Test
fun check_setCursorOffset_uses_offset_map() {
val position = Offset(100f, 200f)
val offset = 10
@@ -405,106 +268,4 @@
)
)
}
-
- @Test
- fun notify_transformed_text() {
- val rect = Rect(0f, 1f, 2f, 3f)
- whenever(textLayoutResult.getBoundingBox(any())).thenReturn(rect)
- val point = Offset(5f, 6f)
- layoutCoordinates = MockCoordinates(
- rootOffset = point
- )
-
- val textInputSession: TextInputSession = mock()
- val input = TextLayoutInput(
- // In this test case, transform the text into double characters text.
- text = AnnotatedString("HHeelllloo,, WWoorrlldd"),
- style = TextStyle(),
- placeholders = listOf(),
- maxLines = Int.MAX_VALUE,
- softWrap = true,
- overflow = TextOverflow.Clip,
- density = Density(1.0f),
- layoutDirection = LayoutDirection.Ltr,
- fontFamilyResolver = mock(),
- constraints = mock()
- )
- whenever(textLayoutResult.layoutInput).thenReturn(input)
-
- val offsetMapping = object : OffsetMapping {
- override fun originalToTransformed(offset: Int): Int = offset * 2
- override fun transformedToOriginal(offset: Int): Int = offset / 2
- }
-
- // The beginning of the text.
- TextFieldDelegate.notifyFocusedRect(
- TextFieldValue(text = "Hello, World", selection = TextRange(0)),
- mDelegate,
- textLayoutResult,
- layoutCoordinates,
- textInputSession,
- true /* hasFocus */,
- offsetMapping
- )
- verify(textInputSession).notifyFocusedRect(any())
-
- // The tail of the transformed text.
- reset(textInputSession)
- TextFieldDelegate.notifyFocusedRect(
- TextFieldValue(text = "Hello, World", selection = TextRange(24)),
- mDelegate,
- textLayoutResult,
- layoutCoordinates,
- textInputSession,
- true /* hasFocus */,
- offsetMapping
- )
- verify(textInputSession).notifyFocusedRect(any())
-
- // Beyond the tail of the transformed text.
- reset(textInputSession)
- TextFieldDelegate.notifyFocusedRect(
- TextFieldValue(text = "Hello, World", selection = TextRange(25)),
- mDelegate,
- textLayoutResult,
- layoutCoordinates,
- textInputSession,
- true /* hasFocus */,
- offsetMapping
- )
- verify(textInputSession).notifyFocusedRect(any())
- }
-
- private class MockCoordinates(
- override val size: IntSize = IntSize.Zero,
- val localOffset: Offset = Offset.Zero,
- val globalOffset: Offset = Offset.Zero,
- val rootOffset: Offset = Offset.Zero
- ) : LayoutCoordinates {
- override val providedAlignmentLines: Set<AlignmentLine>
- get() = emptySet()
- override val parentLayoutCoordinates: LayoutCoordinates?
- get() = null
- override val parentCoordinates: LayoutCoordinates?
- get() = null
- override val isAttached: Boolean
- get() = true
-
- override fun windowToLocal(relativeToWindow: Offset): Offset = localOffset
-
- override fun localToWindow(relativeToLocal: Offset): Offset = globalOffset
-
- override fun localToRoot(relativeToLocal: Offset): Offset = rootOffset
- override fun localPositionOf(
- sourceCoordinates: LayoutCoordinates,
- relativeToSource: Offset
- ): Offset = Offset.Zero
-
- override fun localBoundingBoxOf(
- sourceCoordinates: LayoutCoordinates,
- clipBounds: Boolean
- ): Rect = Rect.Zero
-
- override fun get(alignmentLine: AlignmentLine): Int = 0
- }
}
\ No newline at end of file
diff --git a/compose/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/specification/SpecificationItem.kt b/compose/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/specification/SpecificationItem.kt
index 89dda08..24c5f18 100644
--- a/compose/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/specification/SpecificationItem.kt
+++ b/compose/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/specification/SpecificationItem.kt
@@ -16,39 +16,42 @@
package androidx.compose.material.catalog.ui.specification
-import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.catalog.model.Specification
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowRight
+import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
+import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SpecificationItem(
specification: Specification,
onClick: (specification: Specification) -> Unit
) {
- // TODO: Replace with M3 Card when available
- Surface(
- onClick = { onClick(specification) },
- modifier = Modifier.fillMaxWidth(),
- shape = SpecificationItemShape,
- border = BorderStroke(
- width = SpecificationItemBorderWidth,
- color = MaterialTheme.colorScheme.outline
- )
+ val interactionSource = remember { MutableInteractionSource() }
+ OutlinedCard(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(
+ interactionSource = interactionSource,
+ indication = null,
+ onClick = { onClick(specification) }),
+ interactionSource = interactionSource
) {
Row(
modifier = Modifier.padding(SpecificationItemPadding),
@@ -79,5 +82,3 @@
private val SpecificationItemPadding = 16.dp
private val SpecificationItemTextPadding = 8.dp
-private val SpecificationItemBorderWidth = 1.dp
-private val SpecificationItemShape = RoundedCornerShape(12.dp)
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index fcfc11e..c3e5a1a 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -8,6 +8,11 @@
method @androidx.compose.runtime.Composable public static void AlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function0<kotlin.Unit> confirmButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional float tonalElevation, optional long iconContentColor, optional long titleContentColor, optional long textContentColor, optional androidx.compose.ui.window.DialogProperties properties);
}
+ public final class AndroidMenu_androidKt {
+ method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean enabled, optional androidx.compose.material3.MenuItemColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+ }
+
public final class AppBarKt {
method @androidx.compose.runtime.Composable public static void CenterAlignedTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
@@ -218,6 +223,22 @@
method @androidx.compose.runtime.Composable public static void MaterialTheme(optional androidx.compose.material3.ColorScheme colorScheme, optional androidx.compose.material3.Typography typography, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
+ public final class MenuDefaults {
+ method public androidx.compose.foundation.layout.PaddingValues getDropdownMenuItemContentPadding();
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.MenuItemColors itemColors(optional long textColor, optional long leadingIconColor, optional long trailingIconColor, optional long disabledTextColor, optional long disabledLeadingIconColor, optional long disabledTrailingIconColor);
+ property public final androidx.compose.foundation.layout.PaddingValues DropdownMenuItemContentPadding;
+ field public static final androidx.compose.material3.MenuDefaults INSTANCE;
+ }
+
+ @androidx.compose.runtime.Stable public interface MenuItemColors {
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> leadingIconColor(boolean enabled);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> textColor(boolean enabled);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> trailingIconColor(boolean enabled);
+ }
+
+ public final class MenuKt {
+ }
+
@androidx.compose.runtime.Stable public interface NavigationBarItemColors {
method @androidx.compose.runtime.Composable public long getIndicatorColor();
method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> iconColor(boolean selected);
@@ -333,7 +354,7 @@
public final class SurfaceKt {
method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Surface(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.interaction.InteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Surface(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.foundation.Indication? indication, optional boolean enabled, optional String? onClickLabel, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Surface(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.foundation.Indication? indication, optional boolean enabled, optional String? onClickLabel, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> getLocalAbsoluteTonalElevation();
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> LocalAbsoluteTonalElevation;
}
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index 4f97112..0037ea0 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -8,6 +8,11 @@
method @androidx.compose.runtime.Composable public static void AlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function0<kotlin.Unit> confirmButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional float tonalElevation, optional long iconContentColor, optional long titleContentColor, optional long textContentColor, optional androidx.compose.ui.window.DialogProperties properties);
}
+ public final class AndroidMenu_androidKt {
+ method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean enabled, optional androidx.compose.material3.MenuItemColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+ }
+
public final class AppBarKt {
method @androidx.compose.runtime.Composable public static void CenterAlignedTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
@@ -274,6 +279,22 @@
method @androidx.compose.runtime.Composable public static void MaterialTheme(optional androidx.compose.material3.ColorScheme colorScheme, optional androidx.compose.material3.Typography typography, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
+ public final class MenuDefaults {
+ method public androidx.compose.foundation.layout.PaddingValues getDropdownMenuItemContentPadding();
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.MenuItemColors itemColors(optional long textColor, optional long leadingIconColor, optional long trailingIconColor, optional long disabledTextColor, optional long disabledLeadingIconColor, optional long disabledTrailingIconColor);
+ property public final androidx.compose.foundation.layout.PaddingValues DropdownMenuItemContentPadding;
+ field public static final androidx.compose.material3.MenuDefaults INSTANCE;
+ }
+
+ @androidx.compose.runtime.Stable public interface MenuItemColors {
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> leadingIconColor(boolean enabled);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> textColor(boolean enabled);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> trailingIconColor(boolean enabled);
+ }
+
+ public final class MenuKt {
+ }
+
@androidx.compose.runtime.Stable public interface NavigationBarItemColors {
method @androidx.compose.runtime.Composable public long getIndicatorColor();
method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> iconColor(boolean selected);
@@ -394,7 +415,7 @@
public final class SurfaceKt {
method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Surface(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.interaction.InteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Surface(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.foundation.Indication? indication, optional boolean enabled, optional String? onClickLabel, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Surface(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.foundation.Indication? indication, optional boolean enabled, optional String? onClickLabel, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> getLocalAbsoluteTonalElevation();
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> LocalAbsoluteTonalElevation;
}
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index fcfc11e..c3e5a1a 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -8,6 +8,11 @@
method @androidx.compose.runtime.Composable public static void AlertDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function0<kotlin.Unit> confirmButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissButton, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? title, optional kotlin.jvm.functions.Function0<kotlin.Unit>? text, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional float tonalElevation, optional long iconContentColor, optional long titleContentColor, optional long textContentColor, optional androidx.compose.ui.window.DialogProperties properties);
}
+ public final class AndroidMenu_androidKt {
+ method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean enabled, optional androidx.compose.material3.MenuItemColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+ }
+
public final class AppBarKt {
method @androidx.compose.runtime.Composable public static void CenterAlignedTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
@@ -218,6 +223,22 @@
method @androidx.compose.runtime.Composable public static void MaterialTheme(optional androidx.compose.material3.ColorScheme colorScheme, optional androidx.compose.material3.Typography typography, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
+ public final class MenuDefaults {
+ method public androidx.compose.foundation.layout.PaddingValues getDropdownMenuItemContentPadding();
+ method @androidx.compose.runtime.Composable public androidx.compose.material3.MenuItemColors itemColors(optional long textColor, optional long leadingIconColor, optional long trailingIconColor, optional long disabledTextColor, optional long disabledLeadingIconColor, optional long disabledTrailingIconColor);
+ property public final androidx.compose.foundation.layout.PaddingValues DropdownMenuItemContentPadding;
+ field public static final androidx.compose.material3.MenuDefaults INSTANCE;
+ }
+
+ @androidx.compose.runtime.Stable public interface MenuItemColors {
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> leadingIconColor(boolean enabled);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> textColor(boolean enabled);
+ method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> trailingIconColor(boolean enabled);
+ }
+
+ public final class MenuKt {
+ }
+
@androidx.compose.runtime.Stable public interface NavigationBarItemColors {
method @androidx.compose.runtime.Composable public long getIndicatorColor();
method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> iconColor(boolean selected);
@@ -333,7 +354,7 @@
public final class SurfaceKt {
method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Surface(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.interaction.InteractionSource? interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Surface(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.foundation.Indication? indication, optional boolean enabled, optional String? onClickLabel, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @Deprecated @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Surface(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape shape, optional long color, optional long contentColor, optional float tonalElevation, optional float shadowElevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.foundation.Indication? indication, optional boolean enabled, optional String? onClickLabel, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> getLocalAbsoluteTonalElevation();
property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.unit.Dp> LocalAbsoluteTonalElevation;
}
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
index 3d50af6..3ee98f8 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
@@ -131,6 +131,18 @@
examples = FloatingActionButtonsExamples,
)
+private val Menus = Component(
+ id = nextId(),
+ name = "Menus",
+ description = "Menus display a list of choices on temporary surfaces.",
+ // No menu icon
+ tintIcon = true,
+ guidelinesUrl = "$ComponentGuidelinesUrl/menus",
+ docsUrl = "$PackageSummaryUrl#dropdownmenu",
+ sourceUrl = "$Material3SourceUrl/Menu.kt",
+ examples = MenusExamples
+)
+
private val NavigationBar = Component(
id = nextId(),
name = "Navigation bar",
@@ -228,6 +240,7 @@
Dialogs,
ExtendedFloatingActionButton,
FloatingActionButtons,
+ Menus,
NavigationBar,
NavigationDrawer,
NavigationRail,
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index 450590ab..3ee5792 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -40,6 +40,7 @@
import androidx.compose.material3.samples.FloatingActionButtonSample
import androidx.compose.material3.samples.LargeFloatingActionButtonSample
import androidx.compose.material3.samples.LinearProgressIndicatorSample
+import androidx.compose.material3.samples.MenuSample
import androidx.compose.material3.samples.NavigationBarSample
import androidx.compose.material3.samples.NavigationBarWithOnlySelectedLabelsSample
import androidx.compose.material3.samples.NavigationDrawerSample
@@ -267,6 +268,18 @@
) { SmallFloatingActionButtonSample() }
)
+private const val MenusExampleDescription = "Menus examples"
+private const val MenusExampleSourceUrl = "$SampleSourceUrl/MenuSamples.kt"
+val MenusExamples = listOf(
+ Example(
+ name = ::MenuSample.name,
+ description = MenusExampleDescription,
+ sourceUrl = MenusExampleSourceUrl
+ ) {
+ MenuSample()
+ }
+)
+
private const val NavigationBarExampleDescription = "Navigation bar examples"
private const val NavigationBarExampleSourceUrl = "$SampleSourceUrl/NavigationBarSamples.kt"
val NavigationBarExamples =
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/component/ComponentItem.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/component/ComponentItem.kt
index 0576bae..f399060 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/component/ComponentItem.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/component/ComponentItem.kt
@@ -16,19 +16,22 @@
package androidx.compose.material3.catalog.library.ui.component
-import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
+import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.material3.catalog.library.model.Component
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
@@ -36,24 +39,24 @@
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ComponentItem(
component: Component,
onClick: (component: Component) -> Unit
) {
- // TODO: Replace with M3 Card when available
- Surface(
- onClick = { onClick(component) },
+ val interactionSource = remember { MutableInteractionSource() }
+ OutlinedCard(
modifier = Modifier
.height(ComponentItemHeight)
- .padding(ComponentItemOuterPadding),
- shape = ComponentItemShape,
- border = BorderStroke(
- width = ComponentItemBorderWidth,
- color = MaterialTheme.colorScheme.outline
- )
+ .padding(ComponentItemOuterPadding)
+ .clickable(
+ interactionSource = interactionSource,
+ indication = null,
+ onClick = { onClick(component) }),
+ interactionSource = interactionSource
) {
- Box(modifier = Modifier.padding(ComponentItemInnerPadding)) {
+ Box(modifier = Modifier.fillMaxSize().padding(ComponentItemInnerPadding)) {
Image(
painter = painterResource(id = component.icon),
contentDescription = null,
@@ -80,5 +83,3 @@
private val ComponentItemOuterPadding = 4.dp
private val ComponentItemInnerPadding = 16.dp
private val ComponentItemIconSize = 80.dp
-private val ComponentItemBorderWidth = 1.dp
-private val ComponentItemShape = RoundedCornerShape(12.dp)
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/example/ExampleItem.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/example/ExampleItem.kt
index e8d005e..807d84a 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/example/ExampleItem.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/ui/example/ExampleItem.kt
@@ -16,7 +16,8 @@
package androidx.compose.material3.catalog.library.ui.example
-import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -24,33 +25,35 @@
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowRight
+import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
+import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.material3.catalog.library.model.Example
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ExampleItem(
example: Example,
onClick: (example: Example) -> Unit
) {
- // TODO: Replace with M3 Card when available
- Surface(
- onClick = { onClick(example) },
- modifier = Modifier.fillMaxWidth(),
- shape = ExampleItemShape,
- border = BorderStroke(
- width = ExampleItemBorderWidth,
- color = MaterialTheme.colorScheme.outline
- )
+ val interactionSource = remember { MutableInteractionSource() }
+ OutlinedCard(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(
+ interactionSource = interactionSource,
+ indication = null,
+ onClick = { onClick(example) }),
+ interactionSource = interactionSource
) {
Row(modifier = Modifier.padding(ExampleItemPadding)) {
Column(modifier = Modifier.weight(1f, fill = true)) {
@@ -76,5 +79,3 @@
private val ExampleItemPadding = 16.dp
private val ExampleItemTextPadding = 8.dp
-private val ExampleItemBorderWidth = 1.dp
-private val ExampleItemShape = RoundedCornerShape(12.dp)
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/MenuSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/MenuSamples.kt
new file mode 100644
index 0000000..2455922
--- /dev/null
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/MenuSamples.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.outlined.Edit
+import androidx.compose.material.icons.outlined.Email
+import androidx.compose.material.icons.outlined.Settings
+import androidx.compose.material3.Divider
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+
+@Sampled
+@Composable
+fun MenuSample() {
+ var expanded by remember { mutableStateOf(false) }
+
+ Box(modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.TopStart)) {
+ IconButton(onClick = { expanded = true }) {
+ Icon(Icons.Default.MoreVert, contentDescription = "Localized description")
+ }
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false }
+ ) {
+ DropdownMenuItem(
+ text = { Text("Edit") },
+ onClick = { /* Handle edit! */ },
+ leadingIcon = {
+ Icon(
+ Icons.Outlined.Edit,
+ contentDescription = null
+ )
+ })
+ DropdownMenuItem(
+ text = { Text("Settings") },
+ onClick = { /* Handle settings! */ },
+ leadingIcon = {
+ Icon(
+ Icons.Outlined.Settings,
+ contentDescription = null
+ )
+ })
+ Divider()
+ DropdownMenuItem(
+ text = { Text("Send Feedback") },
+ onClick = { /* Handle send feedback! */ },
+ leadingIcon = {
+ Icon(
+ Icons.Outlined.Email,
+ contentDescription = null
+ )
+ },
+ trailingIcon = { Text("F11", textAlign = TextAlign.Center) })
+ }
+ }
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuScreenshotTest.kt
new file mode 100644
index 0000000..ea3f79f
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuScreenshotTest.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import android.os.Build
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Edit
+import androidx.compose.material.icons.outlined.Email
+import androidx.compose.material.icons.outlined.Lock
+import androidx.compose.material.icons.outlined.Settings
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Screenshot tests for the Material Menus.
+ *
+ * Note that currently nodes in a popup cannot be captured to bitmaps. A [DropdownMenu] is
+ * displaying its content in a popup, so the tests here focus on the [DropdownMenuContent].
+ */
+// TODO(b/208991956): Update to include DropdownMenu when popups can be captured into bitmaps.
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+class MenuScreenshotTest {
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @get:Rule
+ val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
+
+ private val testTag = "dropdown_menu"
+
+ @Test
+ fun dropdownMenu_lightTheme() {
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ TestMenu(enabledItems = true)
+ }
+ assertAgainstGolden(goldenIdentifier = "dropdownMenu_lightTheme")
+ }
+
+ @Test
+ fun dropdownMenu_darkTheme() {
+ composeTestRule.setMaterialContent(darkColorScheme()) {
+ TestMenu(enabledItems = true)
+ }
+ assertAgainstGolden(goldenIdentifier = "dropdownMenu_darkTheme")
+ }
+
+ @Test
+ fun dropdownMenu_disabled_lightTheme() {
+ composeTestRule.setMaterialContent(lightColorScheme()) {
+ TestMenu(enabledItems = false)
+ }
+ assertAgainstGolden(goldenIdentifier = "dropdownMenu_disabled_lightTheme")
+ }
+
+ @Test
+ fun dropdownMenu_disabled_darkTheme() {
+ composeTestRule.setMaterialContent(darkColorScheme()) {
+ TestMenu(enabledItems = false)
+ }
+ assertAgainstGolden(goldenIdentifier = "dropdownMenu_disabled_darkTheme")
+ }
+
+ @Composable
+ private fun TestMenu(enabledItems: Boolean) {
+ Box(Modifier.testTag(testTag).padding(20.dp), contentAlignment = Alignment.Center) {
+ DropdownMenuContent(
+ expandedStates = MutableTransitionState(initialState = true),
+ transformOriginState = mutableStateOf(TransformOrigin.Center)
+ ) {
+ DropdownMenuItem(
+ text = { Text("Edit") },
+ onClick = { },
+ enabled = enabledItems,
+ leadingIcon = {
+ Icon(
+ Icons.Outlined.Edit,
+ contentDescription = null
+ )
+ })
+ DropdownMenuItem(
+ text = { Text("Settings") },
+ onClick = { },
+ enabled = enabledItems,
+ leadingIcon = {
+ Icon(
+ Icons.Outlined.Settings,
+ contentDescription = null
+ )
+ },
+ trailingIcon = { Text("F11", textAlign = TextAlign.Center) })
+ Divider()
+ DropdownMenuItem(
+ text = { Text("Send Feedback") },
+ onClick = { },
+ enabled = enabledItems,
+ leadingIcon = {
+ Icon(
+ Icons.Outlined.Email,
+ contentDescription = null
+ )
+ },
+ trailingIcon = {
+ Icon(
+ Icons.Outlined.Lock,
+ contentDescription = null
+ )
+ })
+ }
+ }
+ }
+
+ private fun assertAgainstGolden(goldenIdentifier: String) {
+ composeTestRule.onNodeWithTag(testTag)
+ .captureToImage()
+ .assertAgainstGolden(screenshotRule, goldenIdentifier)
+ }
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt
new file mode 100644
index 0000000..124eac9
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt
@@ -0,0 +1,362 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.hasAnyDescendant
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.isPopup
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalTestApi::class)
+class MenuTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun menu_canBeTriggered() {
+ var expanded by mutableStateOf(false)
+
+ rule.setContent {
+ Box(Modifier.requiredSize(20.dp).background(color = Color.Blue)) {
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = {}
+ ) {
+ DropdownMenuItem(
+ text = { Text("Option 1") },
+ modifier = Modifier.testTag("MenuContent"),
+ onClick = {})
+ }
+ }
+ }
+
+ rule.onNodeWithTag("MenuContent").assertDoesNotExist()
+ rule.mainClock.autoAdvance = false
+
+ rule.runOnUiThread { expanded = true }
+ rule.mainClock.advanceTimeByFrame() // Trigger the popup
+ rule.waitForIdle()
+ rule.mainClock.advanceTimeByFrame() // Kick off the animation
+ rule.mainClock.advanceTimeBy(InTransitionDuration.toLong())
+ rule.onNodeWithTag("MenuContent").assertExists()
+
+ rule.runOnUiThread { expanded = false }
+ rule.mainClock.advanceTimeByFrame() // Trigger the popup
+ rule.mainClock.advanceTimeByFrame() // Kick off the animation
+ rule.mainClock.advanceTimeBy(OutTransitionDuration.toLong())
+ rule.mainClock.advanceTimeByFrame()
+ rule.onNodeWithTag("MenuContent").assertDoesNotExist()
+
+ rule.runOnUiThread { expanded = true }
+ rule.mainClock.advanceTimeByFrame() // Trigger the popup
+ rule.waitForIdle()
+ rule.mainClock.advanceTimeByFrame() // Kick off the animation
+ rule.mainClock.advanceTimeBy(InTransitionDuration.toLong())
+ rule.onNodeWithTag("MenuContent").assertExists()
+ }
+
+ @Test
+ fun menu_hasExpectedSize() {
+ rule.setContent {
+ with(LocalDensity.current) {
+ Box(Modifier.requiredSize(20.toDp()).background(color = Color.Blue)) {
+ DropdownMenu(
+ expanded = true,
+ onDismissRequest = {}
+ ) {
+ Box(Modifier.testTag("MenuContent1").size(70.toDp()))
+ Box(Modifier.testTag("MenuContent2").size(130.toDp()))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("MenuContent1").assertExists()
+ rule.onNodeWithTag("MenuContent2").assertExists()
+ val node = rule.onNode(
+ isPopup() and hasAnyDescendant(hasTestTag("MenuContent1")) and
+ hasAnyDescendant(hasTestTag("MenuContent2"))
+ ).assertExists().fetchSemanticsNode()
+ with(rule.density) {
+ assertThat(node.size.width).isEqualTo(130)
+ assertThat(node.size.height)
+ .isEqualTo(DropdownMenuVerticalPadding.roundToPx() * 2 + 200)
+ }
+ }
+
+ @Test
+ fun menu_positioning_bottomEnd() {
+ val screenWidth = 500
+ val screenHeight = 1000
+ val density = Density(1f)
+ val windowSize = IntSize(screenWidth, screenHeight)
+ val anchorPosition = IntOffset(100, 200)
+ val anchorSize = IntSize(10, 20)
+ val offsetX = 20
+ val offsetY = 40
+ val popupSize = IntSize(50, 80)
+
+ val ltrPosition = DropdownMenuPositionProvider(
+ DpOffset(offsetX.dp, offsetY.dp),
+ density
+ ).calculatePosition(
+ IntRect(anchorPosition, anchorSize),
+ windowSize,
+ LayoutDirection.Ltr,
+ popupSize
+ )
+
+ assertThat(ltrPosition.x).isEqualTo(
+ anchorPosition.x + offsetX
+ )
+ assertThat(ltrPosition.y).isEqualTo(
+ anchorPosition.y + anchorSize.height + offsetY
+ )
+
+ val rtlPosition = DropdownMenuPositionProvider(
+ DpOffset(offsetX.dp, offsetY.dp),
+ density
+ ).calculatePosition(
+ IntRect(anchorPosition, anchorSize),
+ windowSize,
+ LayoutDirection.Rtl,
+ popupSize
+ )
+
+ assertThat(rtlPosition.x).isEqualTo(
+ anchorPosition.x + anchorSize.width - offsetX - popupSize.width
+ )
+ assertThat(rtlPosition.y).isEqualTo(
+ anchorPosition.y + anchorSize.height + offsetY
+ )
+ }
+
+ @Test
+ fun menu_positioning_topStart() {
+ val screenWidth = 500
+ val screenHeight = 1000
+ val density = Density(1f)
+ val windowSize = IntSize(screenWidth, screenHeight)
+ val anchorPosition = IntOffset(450, 950)
+ val anchorPositionRtl = IntOffset(50, 950)
+ val anchorSize = IntSize(10, 20)
+ val offsetX = 20
+ val offsetY = 40
+ val popupSize = IntSize(150, 80)
+
+ val ltrPosition = DropdownMenuPositionProvider(
+ DpOffset(offsetX.dp, offsetY.dp),
+ density
+ ).calculatePosition(
+ IntRect(anchorPosition, anchorSize),
+ windowSize,
+ LayoutDirection.Ltr,
+ popupSize
+ )
+
+ assertThat(ltrPosition.x).isEqualTo(
+ anchorPosition.x + anchorSize.width - offsetX - popupSize.width
+ )
+ assertThat(ltrPosition.y).isEqualTo(
+ anchorPosition.y - popupSize.height - offsetY
+ )
+
+ val rtlPosition = DropdownMenuPositionProvider(
+ DpOffset(offsetX.dp, offsetY.dp),
+ density
+ ).calculatePosition(
+ IntRect(anchorPositionRtl, anchorSize),
+ windowSize,
+ LayoutDirection.Rtl,
+ popupSize
+ )
+
+ assertThat(rtlPosition.x).isEqualTo(
+ anchorPositionRtl.x + offsetX
+ )
+ assertThat(rtlPosition.y).isEqualTo(
+ anchorPositionRtl.y - popupSize.height - offsetY
+ )
+ }
+
+ @Test
+ fun menu_positioning_top() {
+ val screenWidth = 500
+ val screenHeight = 1000
+ val density = Density(1f)
+ val windowSize = IntSize(screenWidth, screenHeight)
+ val anchorPosition = IntOffset(0, 0)
+ val anchorSize = IntSize(50, 20)
+ val popupSize = IntSize(150, 500)
+
+ // The min margin above and below the menu, relative to the screen.
+ val menuVerticalMargin = 48.dp
+ val verticalMargin = with(density) { menuVerticalMargin.roundToPx() }
+
+ val position = DropdownMenuPositionProvider(
+ DpOffset(0.dp, 0.dp),
+ density
+ ).calculatePosition(
+ IntRect(anchorPosition, anchorSize),
+ windowSize,
+ LayoutDirection.Ltr,
+ popupSize
+ )
+
+ assertThat(position.y).isEqualTo(
+ verticalMargin
+ )
+ }
+
+ @Test
+ fun menu_positioning_anchorPartiallyVisible() {
+ val screenWidth = 500
+ val screenHeight = 1000
+ val density = Density(1f)
+ val windowSize = IntSize(screenWidth, screenHeight)
+ val anchorPosition = IntOffset(-25, -10)
+ val anchorPositionRtl = IntOffset(525, -10)
+ val anchorSize = IntSize(50, 20)
+ val popupSize = IntSize(150, 500)
+
+ // The min margin above and below the menu, relative to the screen.
+ val menuVerticalMargin = 48.dp
+ val verticalMargin = with(density) { menuVerticalMargin.roundToPx() }
+
+ val position = DropdownMenuPositionProvider(
+ DpOffset(0.dp, 0.dp),
+ density
+ ).calculatePosition(
+ IntRect(anchorPosition, anchorSize),
+ windowSize,
+ LayoutDirection.Ltr,
+ popupSize
+ )
+
+ assertThat(position.x).isEqualTo(
+ 0
+ )
+ assertThat(position.y).isEqualTo(
+ verticalMargin
+ )
+
+ val rtlPosition = DropdownMenuPositionProvider(
+ DpOffset(0.dp, 0.dp),
+ density
+ ).calculatePosition(
+ IntRect(anchorPositionRtl, anchorSize),
+ windowSize,
+ LayoutDirection.Rtl,
+ popupSize
+ )
+
+ assertThat(rtlPosition.x).isEqualTo(
+ screenWidth - popupSize.width
+ )
+ assertThat(rtlPosition.y).isEqualTo(
+ verticalMargin
+ )
+ }
+
+ @Test
+ fun menu_positioning_callback() {
+ val screenWidth = 500
+ val screenHeight = 1000
+ val density = Density(1f)
+ val windowSize = IntSize(screenWidth, screenHeight)
+ val anchorPosition = IntOffset(100, 200)
+ val anchorSize = IntSize(10, 20)
+ val offsetX = 20
+ val offsetY = 40
+ val popupSize = IntSize(50, 80)
+
+ var obtainedParentBounds = IntRect(0, 0, 0, 0)
+ var obtainedMenuBounds = IntRect(0, 0, 0, 0)
+ DropdownMenuPositionProvider(
+ DpOffset(offsetX.dp, offsetY.dp),
+ density
+ ) { parentBounds, menuBounds ->
+ obtainedParentBounds = parentBounds
+ obtainedMenuBounds = menuBounds
+ }.calculatePosition(
+ IntRect(anchorPosition, anchorSize),
+ windowSize,
+ LayoutDirection.Ltr,
+ popupSize
+ )
+
+ assertThat(obtainedParentBounds).isEqualTo(IntRect(anchorPosition, anchorSize))
+ assertThat(obtainedMenuBounds).isEqualTo(
+ IntRect(
+ anchorPosition.x + offsetX,
+ anchorPosition.y + anchorSize.height + offsetY,
+ anchorPosition.x + offsetX + popupSize.width,
+ anchorPosition.y + anchorSize.height + offsetY + popupSize.height
+ )
+ )
+ }
+
+ @Test
+ fun dropdownMenuItem_onClick() {
+ var clicked = false
+ val onClick: () -> Unit = { clicked = true }
+
+ rule.setContent {
+ DropdownMenuItem(
+ text = { Box(Modifier.requiredSize(40.dp)) },
+ onClick,
+ modifier = Modifier.testTag("MenuItem").clickable(onClick = onClick),
+ )
+ }
+
+ rule.onNodeWithTag("MenuItem").performClick()
+
+ rule.runOnIdle {
+ assertThat(clicked).isTrue()
+ }
+ }
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SurfaceTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SurfaceTest.kt
index 0d52061..265b771 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SurfaceTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SurfaceTest.kt
@@ -307,28 +307,7 @@
}
@Test
- fun clickableOverload_clickAction() {
- val count = mutableStateOf(0f)
- rule.setMaterialContent(lightColorScheme()) {
- Surface(
- modifier = Modifier.testTag("surface"),
- onClick = { count.value += 1 }
- ) {
- Spacer(Modifier.size(30.dp))
- }
- }
- rule.onNodeWithTag("surface")
- .performClick()
- Truth.assertThat(count.value).isEqualTo(1)
-
- rule.onNodeWithTag("surface")
- .performClick()
- .performClick()
- Truth.assertThat(count.value).isEqualTo(3)
- }
-
- @Test
- fun clickable_clickActionWithModifier() {
+ fun clickable_clickAction() {
val count = mutableStateOf(0f)
val interactionSource = MutableInteractionSource()
rule.setMaterialContent(lightColorScheme()) {
@@ -382,7 +361,7 @@
}
@Test
- fun clickableOverload_interactionSource() {
+ fun clickable_interactionSource() {
val interactionSource = MutableInteractionSource()
var scope: CoroutineScope? = null
@@ -392,8 +371,12 @@
rule.setContent {
scope = rememberCoroutineScope()
Surface(
- modifier = Modifier.testTag("surface"),
- onClick = {},
+ modifier =
+ Modifier.testTag("surface")
+ .clickable(
+ interactionSource = interactionSource,
+ indication = null,
+ onClick = {}),
interactionSource = interactionSource
) {
Spacer(Modifier.size(30.dp))
@@ -433,7 +416,24 @@
}
}
- // TODO(b/198216553): Add surface_blockClicksBehind test from M2 after Button is added.
+ @Test
+ fun surface_blockClicksBehind() {
+ val state = mutableStateOf(0)
+ rule.setContent {
+ Box(Modifier.fillMaxSize()) {
+ Button(
+ modifier = Modifier.fillMaxSize().testTag("clickable"),
+ onClick = { state.value += 1 }
+ ) { Text("button fullscreen") }
+ Surface(
+ Modifier.fillMaxSize().testTag("surface"),
+ ) {}
+ }
+ }
+ rule.onNodeWithTag("clickable").assertHasClickAction().performClick()
+ // still 0
+ Truth.assertThat(state.value).isEqualTo(0)
+ }
// regression test for b/189411183
@Test
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt
new file mode 100644
index 0000000..159f821
--- /dev/null
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.PopupProperties
+
+/**
+ * <a href="https://m3.material.io/components/menus/overview" class="external" target="_blank">Material Design dropdown menu</a>.
+ *
+ * A dropdown menu is a compact way of displaying multiple choices. It appears upon interaction with
+ * an element (such as an icon or button) or when users perform a specific action.
+ *
+ * A [DropdownMenu] behaves similarly to a [Popup], and will use the position of the parent layout
+ * to position itself on screen. Commonly a [DropdownMenu] will be placed in a [Box] with a sibling
+ * that will be used as the 'anchor'. Note that a [DropdownMenu] by itself will not take up any
+ * space in a layout, as the menu is displayed in a separate window, on top of other content.
+ *
+ * The [content] of a [DropdownMenu] will typically be [DropdownMenuItem]s, as well as custom
+ * content. Using [DropdownMenuItem]s will result in a menu that matches the Material
+ * specification for menus. Also note that the [content] is placed inside a scrollable [Column],
+ * so using a [LazyColumn] as the root layout inside [content] is unsupported.
+ *
+ * [onDismissRequest] will be called when the menu should close - for example when there is a
+ * tap outside the menu, or when the back key is pressed.
+ *
+ * [DropdownMenu] changes its positioning depending on the available space, always trying to be
+ * fully visible. It will try to expand horizontally, depending on layout direction, to the end of
+ * its parent, then to the start of its parent, and then screen end-aligned. Vertically, it will
+ * try to expand to the bottom of its parent, then from the top of its parent, and then screen
+ * top-aligned. An [offset] can be provided to adjust the positioning of the menu for cases when
+ * the layout bounds of its parent do not coincide with its visual bounds. Note the offset will
+ * be applied in the direction in which the menu will decide to expand.
+ *
+ * Example usage:
+ * @sample androidx.compose.material3.samples.MenuSample
+ *
+ * @param expanded Whether the menu is currently open and visible to the user
+ * @param onDismissRequest Called when the user requests to dismiss the menu, such as by
+ * tapping outside the menu's bounds
+ * @param offset [DpOffset] to be added to the position of the menu
+ */
+@Suppress("ModifierParameter")
+@Composable
+fun DropdownMenu(
+ expanded: Boolean,
+ onDismissRequest: () -> Unit,
+ modifier: Modifier = Modifier,
+ offset: DpOffset = DpOffset(0.dp, 0.dp),
+ properties: PopupProperties = PopupProperties(focusable = true),
+ content: @Composable ColumnScope.() -> Unit
+) {
+ val expandedStates = remember { MutableTransitionState(false) }
+ expandedStates.targetState = expanded
+
+ if (expandedStates.currentState || expandedStates.targetState) {
+ val transformOriginState = remember { mutableStateOf(TransformOrigin.Center) }
+ val density = LocalDensity.current
+ val popupPositionProvider = DropdownMenuPositionProvider(
+ offset,
+ density
+ ) { parentBounds, menuBounds ->
+ transformOriginState.value = calculateTransformOrigin(parentBounds, menuBounds)
+ }
+
+ Popup(
+ onDismissRequest = onDismissRequest,
+ popupPositionProvider = popupPositionProvider,
+ properties = properties
+ ) {
+ DropdownMenuContent(
+ expandedStates = expandedStates,
+ transformOriginState = transformOriginState,
+ modifier = modifier,
+ content = content
+ )
+ }
+ }
+}
+
+/**
+ * <a href="https://m3.material.io/components/menus/overview" class="external" target="_blank">Material Design dropdown menu</a> item.
+ *
+ *
+ * Example usage:
+ * @sample androidx.compose.material3.samples.MenuSample
+ *
+ * @param text The menu item text
+ * @param onClick Called when the menu item was clicked
+ * @param modifier The modifier to be applied to the menu item
+ * @param leadingIcon Optional leading icon to be displayed at the beginning of the item's text
+ * @param trailingIcon Optional trailing icon to be displayed at the end of the item's text. This
+ * trailing icon slot can also accept [Text] to indicate a keyboard shortcut, for example.
+ * @param enabled Controls the enabled state of the menu item - when `false`, the menu item
+ * will not be clickable and [onClick] will not be invoked
+ * @param colors [MenuItemColors] that will be used to resolve the background and content color for
+ * this item in different states. See [MenuDefaults.itemColors].
+ * @param contentPadding the padding applied to the content of this menu item
+ * @param interactionSource the [MutableInteractionSource] representing the stream of
+ * [Interaction]s for this DropdownMenuItem. You can create and pass in your own remembered
+ * [MutableInteractionSource] if you want to observe [Interaction]s and customize the
+ * appearance / behavior of this DropdownMenuItem in different [Interaction]s.
+ */
+@Composable
+fun DropdownMenuItem(
+ text: @Composable () -> Unit,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ leadingIcon: @Composable (() -> Unit)? = null,
+ trailingIcon: @Composable (() -> Unit)? = null,
+ enabled: Boolean = true,
+ colors: MenuItemColors = MenuDefaults.itemColors(),
+ contentPadding: PaddingValues = MenuDefaults.DropdownMenuItemContentPadding,
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+) {
+ DropdownMenuItemContent(
+ text = text,
+ onClick = onClick,
+ modifier = modifier,
+ leadingIcon = leadingIcon,
+ trailingIcon = trailingIcon,
+ enabled = enabled,
+ colors = colors,
+ contentPadding = contentPadding,
+ interactionSource = interactionSource,
+ )
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
index 9613eed..6b44cef 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt
@@ -19,6 +19,7 @@
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.VectorConverter
import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.FocusInteraction
import androidx.compose.foundation.interaction.HoverInteraction
import androidx.compose.foundation.interaction.Interaction
@@ -31,7 +32,6 @@
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
-import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.tokens.ElevatedButtonTokens
import androidx.compose.material3.tokens.FilledButtonTokens
import androidx.compose.material3.tokens.FilledButtonTonalTokens
@@ -119,19 +119,23 @@
// TODO(b/202880001): Apply shadow color from token (will not be possibly any time soon, if ever).
Surface(
- modifier = modifier.minimumTouchTargetSize(),
+ modifier = modifier
+ .minimumTouchTargetSize()
+ .clickable(
+ interactionSource = interactionSource,
+ indication = null,
+ enabled = enabled,
+ role = Role.Button,
+ onClick = onClick
+ ),
+ interactionSource = interactionSource,
shape = shape,
color = containerColor,
contentColor = contentColor,
- shadowElevation = shadowElevation,
tonalElevation = tonalElevation,
- border = border,
- onClick = onClick,
- enabled = enabled,
- role = Role.Button,
- interactionSource = interactionSource,
- indication = rememberRipple(),
- ) {
+ shadowElevation = shadowElevation,
+ border = border
+ ) {
CompositionLocalProvider(LocalContentColor provides contentColor) {
ProvideTextStyle(value = TypographyTokens.LabelLarge) {
Row(
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
new file mode 100644
index 0000000..4a357c8
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
@@ -0,0 +1,460 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.material3.tokens.MenuTokens
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.PopupPositionProvider
+import kotlin.math.max
+import kotlin.math.min
+
+@Suppress("ModifierParameter")
+@Composable
+internal fun DropdownMenuContent(
+ expandedStates: MutableTransitionState<Boolean>,
+ transformOriginState: MutableState<TransformOrigin>,
+ modifier: Modifier = Modifier,
+ content: @Composable ColumnScope.() -> Unit
+) {
+ // Menu open/close animation.
+ val transition = updateTransition(expandedStates, "DropDownMenu")
+
+ val scale by transition.animateFloat(
+ transitionSpec = {
+ if (false isTransitioningTo true) {
+ // Dismissed to expanded
+ tween(
+ durationMillis = InTransitionDuration,
+ easing = LinearOutSlowInEasing
+ )
+ } else {
+ // Expanded to dismissed.
+ tween(
+ durationMillis = 1,
+ delayMillis = OutTransitionDuration - 1
+ )
+ }
+ }
+ ) {
+ if (it) {
+ // Menu is expanded.
+ 1f
+ } else {
+ // Menu is dismissed.
+ 0.8f
+ }
+ }
+
+ val alpha by transition.animateFloat(
+ transitionSpec = {
+ if (false isTransitioningTo true) {
+ // Dismissed to expanded
+ tween(durationMillis = 30)
+ } else {
+ // Expanded to dismissed.
+ tween(durationMillis = OutTransitionDuration)
+ }
+ }
+ ) {
+ if (it) {
+ // Menu is expanded.
+ 1f
+ } else {
+ // Menu is dismissed.
+ 0f
+ }
+ }
+ Surface(
+ modifier = Modifier.graphicsLayer {
+ scaleX = scale
+ scaleY = scale
+ this.alpha = alpha
+ transformOrigin = transformOriginState.value
+ },
+ shape = MenuTokens.ContainerShape,
+ color = MaterialTheme.colorScheme.fromToken(MenuTokens.ContainerColor),
+ tonalElevation = MenuTokens.ContainerElevation,
+ shadowElevation = MenuTokens.ContainerElevation
+ ) {
+ Column(
+ modifier = modifier
+ .padding(vertical = DropdownMenuVerticalPadding)
+ .width(IntrinsicSize.Max)
+ .verticalScroll(rememberScrollState()),
+ content = content
+ )
+ }
+}
+
+@Composable
+internal fun DropdownMenuItemContent(
+ text: @Composable () -> Unit,
+ onClick: () -> Unit,
+ modifier: Modifier,
+ leadingIcon: @Composable (() -> Unit)?,
+ trailingIcon: @Composable (() -> Unit)?,
+ enabled: Boolean,
+ colors: MenuItemColors,
+ contentPadding: PaddingValues,
+ interactionSource: MutableInteractionSource
+) {
+ Row(
+ modifier = modifier
+ .clickable(
+ enabled = enabled,
+ onClick = onClick,
+ interactionSource = interactionSource,
+ indication = rememberRipple(true)
+ )
+ .fillMaxWidth()
+ // Preferred min and max width used during the intrinsic measurement.
+ .sizeIn(
+ minWidth = DropdownMenuItemDefaultMinWidth,
+ maxWidth = DropdownMenuItemDefaultMaxWidth,
+ minHeight = MenuTokens.ListItemContainerHeight
+ )
+ .padding(contentPadding),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ ProvideTextStyle(MaterialTheme.typography.fromToken(MenuTokens.ListItemLabelTextFont)) {
+ if (leadingIcon != null) {
+ CompositionLocalProvider(
+ LocalContentColor provides colors.leadingIconColor(enabled).value,
+ ) {
+ Box(Modifier.defaultMinSize(minWidth = MenuTokens.ListItemLeadingIconSize)) {
+ leadingIcon()
+ }
+ }
+ }
+ CompositionLocalProvider(LocalContentColor provides colors.textColor(enabled).value) {
+ Box(
+ Modifier.weight(1f)
+ .padding(
+ start = if (leadingIcon != null) {
+ DropdownMenuItemHorizontalPadding
+ } else {
+ 0.dp
+ },
+ end = if (trailingIcon != null) {
+ DropdownMenuItemHorizontalPadding
+ } else {
+ 0.dp
+ }
+ )
+ ) {
+ text()
+ }
+ }
+ if (trailingIcon != null) {
+ CompositionLocalProvider(
+ LocalContentColor provides colors.trailingIconColor(enabled).value
+ ) {
+ Box(Modifier.defaultMinSize(minWidth = MenuTokens.ListItemTrailingIconSize)) {
+ trailingIcon()
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Contains default values used for [DropdownMenuItem].
+ */
+object MenuDefaults {
+
+ /**
+ * Creates a [MenuItemColors] that represents the default text and icon colors used in a
+ * [DropdownMenuItemContent].
+ *
+ * @param textColor the text color of this [DropdownMenuItemContent] when enabled
+ * @param leadingIconColor the leading icon color of this [DropdownMenuItemContent] when enabled
+ * @param trailingIconColor the trailing icon color of this [DropdownMenuItemContent] when
+ * enabled
+ * @param disabledTextColor the text color of this [DropdownMenuItemContent] when not enabled
+ * @param disabledLeadingIconColor the leading icon color of this [DropdownMenuItemContent] when
+ * not enabled
+ * @param disabledTrailingIconColor the trailing icon color of this [DropdownMenuItemContent]
+ * when not enabled
+ */
+ @Composable
+ fun itemColors(
+ textColor: Color = MenuTokens.ListItemLabelTextColor.toColor(),
+ leadingIconColor: Color = MenuTokens.ListItemLeadingIconColor.toColor(),
+ trailingIconColor: Color = MenuTokens.ListItemTrailingIconColor.toColor(),
+ disabledTextColor: Color =
+ MenuTokens.ListItemDisabledLabelTextColor.toColor()
+ .copy(alpha = MenuTokens.ListItemDisabledLabelTextOpacity),
+ disabledLeadingIconColor: Color = MenuTokens.ListItemDisabledLeadingIconColor.toColor()
+ .copy(alpha = MenuTokens.ListItemDisabledLeadingIconOpacity),
+ disabledTrailingIconColor: Color = MenuTokens.ListItemDisabledTrailingIconColor.toColor()
+ .copy(alpha = MenuTokens.ListItemDisabledTrailingIconOpacity),
+ ): MenuItemColors =
+ DefaultMenuItemColors(
+ textColor = textColor,
+ leadingIconColor = leadingIconColor,
+ trailingIconColor = trailingIconColor,
+ disabledTextColor = disabledTextColor,
+ disabledLeadingIconColor = disabledLeadingIconColor,
+ disabledTrailingIconColor = disabledTrailingIconColor,
+ )
+
+ /**
+ * Default padding used for [DropdownMenuItem].
+ */
+ val DropdownMenuItemContentPadding = PaddingValues(
+ horizontal = DropdownMenuItemHorizontalPadding,
+ vertical = 0.dp
+ )
+}
+
+/**
+ * Represents the text and icon colors used in a menu item at different states.
+ *
+ * - See [MenuDefaults.itemColors] for the default colors used in a [DropdownMenuItemContent].
+ */
+@Stable
+interface MenuItemColors {
+
+ /**
+ * Represents the text color for a menu item, depending on its [enabled] state.
+ *
+ * @param enabled whether the menu item is enabled
+ */
+ @Composable
+ fun textColor(enabled: Boolean): State<Color>
+
+ /**
+ * Represents the leading icon color for a menu item, depending on its [enabled] state.
+ *
+ * @param enabled whether the menu item is enabled
+ */
+ @Composable
+ fun leadingIconColor(enabled: Boolean): State<Color>
+
+ /**
+ * Represents the trailing icon color for a menu item, depending on its [enabled] state.
+ *
+ * @param enabled whether the menu item is enabled
+ */
+ @Composable
+ fun trailingIconColor(enabled: Boolean): State<Color>
+}
+
+internal fun calculateTransformOrigin(
+ parentBounds: IntRect,
+ menuBounds: IntRect
+): TransformOrigin {
+ val pivotX = when {
+ menuBounds.left >= parentBounds.right -> 0f
+ menuBounds.right <= parentBounds.left -> 1f
+ menuBounds.width == 0 -> 0f
+ else -> {
+ val intersectionCenter =
+ (
+ max(parentBounds.left, menuBounds.left) +
+ min(parentBounds.right, menuBounds.right)
+ ) / 2
+ (intersectionCenter - menuBounds.left).toFloat() / menuBounds.width
+ }
+ }
+ val pivotY = when {
+ menuBounds.top >= parentBounds.bottom -> 0f
+ menuBounds.bottom <= parentBounds.top -> 1f
+ menuBounds.height == 0 -> 0f
+ else -> {
+ val intersectionCenter =
+ (
+ max(parentBounds.top, menuBounds.top) +
+ min(parentBounds.bottom, menuBounds.bottom)
+ ) / 2
+ (intersectionCenter - menuBounds.top).toFloat() / menuBounds.height
+ }
+ }
+ return TransformOrigin(pivotX, pivotY)
+}
+
+// Menu positioning.
+
+/**
+ * Calculates the position of a Material [DropdownMenu].
+ */
+// TODO(popam): Investigate if this can/should consider the app window size rather than screen size
+@Immutable
+internal data class DropdownMenuPositionProvider(
+ val contentOffset: DpOffset,
+ val density: Density,
+ val onPositionCalculated: (IntRect, IntRect) -> Unit = { _, _ -> }
+) : PopupPositionProvider {
+ override fun calculatePosition(
+ anchorBounds: IntRect,
+ windowSize: IntSize,
+ layoutDirection: LayoutDirection,
+ popupContentSize: IntSize
+ ): IntOffset {
+ // The min margin above and below the menu, relative to the screen.
+ val verticalMargin = with(density) { MenuVerticalMargin.roundToPx() }
+ // The content offset specified using the dropdown offset parameter.
+ val contentOffsetX = with(density) { contentOffset.x.roundToPx() }
+ val contentOffsetY = with(density) { contentOffset.y.roundToPx() }
+
+ // Compute horizontal position.
+ val toRight = anchorBounds.left + contentOffsetX
+ val toLeft = anchorBounds.right - contentOffsetX - popupContentSize.width
+ val toDisplayRight = windowSize.width - popupContentSize.width
+ val toDisplayLeft = 0
+ val x = if (layoutDirection == LayoutDirection.Ltr) {
+ sequenceOf(
+ toRight,
+ toLeft,
+ // If the anchor gets outside of the window on the left, we want to position
+ // toDisplayLeft for proximity to the anchor. Otherwise, toDisplayRight.
+ if (anchorBounds.left >= 0) toDisplayRight else toDisplayLeft
+ )
+ } else {
+ sequenceOf(
+ toLeft,
+ toRight,
+ // If the anchor gets outside of the window on the right, we want to position
+ // toDisplayRight for proximity to the anchor. Otherwise, toDisplayLeft.
+ if (anchorBounds.right <= windowSize.width) toDisplayLeft else toDisplayRight
+ )
+ }.firstOrNull {
+ it >= 0 && it + popupContentSize.width <= windowSize.width
+ } ?: toLeft
+
+ // Compute vertical position.
+ val toBottom = maxOf(anchorBounds.bottom + contentOffsetY, verticalMargin)
+ val toTop = anchorBounds.top - contentOffsetY - popupContentSize.height
+ val toCenter = anchorBounds.top - popupContentSize.height / 2
+ val toDisplayBottom = windowSize.height - popupContentSize.height - verticalMargin
+ val y = sequenceOf(toBottom, toTop, toCenter, toDisplayBottom).firstOrNull {
+ it >= verticalMargin &&
+ it + popupContentSize.height <= windowSize.height - verticalMargin
+ } ?: toTop
+
+ onPositionCalculated(
+ anchorBounds,
+ IntRect(x, y, x + popupContentSize.width, y + popupContentSize.height)
+ )
+ return IntOffset(x, y)
+ }
+}
+
+/** Default [MenuItemColors] implementation. */
+@Immutable
+private class DefaultMenuItemColors(
+ private val textColor: Color,
+ private val leadingIconColor: Color,
+ private val trailingIconColor: Color,
+ private val disabledTextColor: Color,
+ private val disabledLeadingIconColor: Color,
+ private val disabledTrailingIconColor: Color,
+) : MenuItemColors {
+
+ @Composable
+ override fun textColor(enabled: Boolean): State<Color> {
+ return rememberUpdatedState(if (enabled) textColor else disabledTextColor)
+ }
+
+ @Composable
+ override fun leadingIconColor(enabled: Boolean): State<Color> {
+ return rememberUpdatedState(if (enabled) leadingIconColor else disabledLeadingIconColor)
+ }
+
+ @Composable
+ override fun trailingIconColor(enabled: Boolean): State<Color> {
+ return rememberUpdatedState(if (enabled) trailingIconColor else disabledTrailingIconColor)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+
+ other as DefaultMenuItemColors
+
+ if (textColor != other.textColor) return false
+ if (leadingIconColor != other.leadingIconColor) return false
+ if (trailingIconColor != other.trailingIconColor) return false
+ if (disabledTextColor != other.disabledTextColor) return false
+ if (disabledLeadingIconColor != other.disabledLeadingIconColor) return false
+ if (disabledTrailingIconColor != other.disabledTrailingIconColor) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = textColor.hashCode()
+ result = 31 * result + leadingIconColor.hashCode()
+ result = 31 * result + trailingIconColor.hashCode()
+ result = 31 * result + disabledTextColor.hashCode()
+ result = 31 * result + disabledLeadingIconColor.hashCode()
+ result = 31 * result + disabledTrailingIconColor.hashCode()
+ return result
+ }
+}
+
+// Size defaults.
+internal val MenuVerticalMargin = 48.dp
+private val DropdownMenuItemHorizontalPadding = 12.dp
+internal val DropdownMenuVerticalPadding = 8.dp
+private val DropdownMenuItemDefaultMinWidth = 112.dp
+private val DropdownMenuItemDefaultMaxWidth = 280.dp
+
+// Menu open/close animation.
+internal const val InTransitionDuration = 120
+internal const val OutTransitionDuration = 75
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Surface.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Surface.kt
index 5062093..86623af 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Surface.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Surface.kt
@@ -207,6 +207,10 @@
*/
@Composable
@NonRestartableComposable
+@Deprecated(
+ message = "Please use Surface with an InteractionSource and a Modifier.clickable() instead.",
+ level = DeprecationLevel.ERROR
+)
fun Surface(
onClick: () -> Unit,
modifier: Modifier = Modifier,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/MenuTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/MenuTokens.kt
new file mode 100644
index 0000000..4602731
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/MenuTokens.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object MenuTokens {
+ val ContainerColor = ColorSchemeKeyTokens.Surface
+ val ContainerElevation = ElevationTokens.Level2
+ val ContainerShape = ShapeTokens.Small
+ val DividerColor = ColorSchemeKeyTokens.SurfaceVariant
+ val DividerHeight = 1.0.dp
+ val ListItemContainerHeight = 48.0.dp
+ val ListItemDisabledLabelTextColor = ColorSchemeKeyTokens.OnSurface
+ const val ListItemDisabledLabelTextOpacity = 0.38f
+ val ListItemFocusLabelTextColor = ColorSchemeKeyTokens.OnSurface
+ val ListItemFocusStateLayerColor = ColorSchemeKeyTokens.OnSurface
+ val ListItemHoverLabelTextColor = ColorSchemeKeyTokens.OnSurface
+ val ListItemHoverStateLayerColor = ColorSchemeKeyTokens.OnSurface
+ val ListItemLabelTextColor = ColorSchemeKeyTokens.OnSurface
+ val ListItemLabelTextFont = TypographyKeyTokens.LabelLarge
+ val ListItemPressedLabelTextColor = ColorSchemeKeyTokens.OnSurface
+ val ListItemPressedStateLayerColor = ColorSchemeKeyTokens.OnSurface
+ val ListItemSelectedContainerColor = ColorSchemeKeyTokens.SurfaceVariant
+ val ListItemDisabledLeadingIconColor = ColorSchemeKeyTokens.OnSurface
+ const val ListItemDisabledLeadingIconOpacity = 0.38f
+ val ListItemLeadingFocusIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+ val ListItemLeadingHoverIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+ val ListItemLeadingIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+ val ListItemLeadingIconSize = 24.0.dp
+ val ListItemLeadingPressedIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+ val ListItemDisabledTrailingIconColor = ColorSchemeKeyTokens.OnSurface
+ const val ListItemDisabledTrailingIconOpacity = 0.38f
+ val ListItemTrailingFocusIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+ val ListItemTrailingHoverIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+ val ListItemTrailingPressedIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+ val ListItemTrailingIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+ val ListItemTrailingIconSize = 24.0.dp
+}
diff --git a/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt b/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt
index 033b693..1d15b6c 100644
--- a/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt
+++ b/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt
@@ -410,6 +410,44 @@
assertThat(composedValue).isEqualTo(1)
}
}
+
+ @Test
+ fun changingInputIsNotAffectingOrderOfRestoration() {
+ var counter = 0
+ var input by mutableStateOf(0)
+ var withInput: Int? = null
+ var withoutInput: String? = null
+
+ restorationTester.setContent {
+ withInput = rememberSaveable(input) { counter++ }
+ withoutInput = rememberSaveable { (counter++).toString() }
+ }
+
+ rule.runOnIdle {
+ assertThat(withInput).isNotNull()
+ withInput = null
+ input++
+ }
+
+ var expectedWithInput: Int? = null
+ var expectedWithoutInput: String? = null
+
+ rule.runOnIdle {
+ assertThat(withInput).isNotNull()
+ assertThat(withoutInput).isNotNull()
+ expectedWithInput = withInput
+ expectedWithoutInput = withoutInput
+ withInput = null
+ withoutInput = null
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ rule.runOnIdle {
+ assertThat(withInput).isEqualTo(expectedWithInput)
+ assertThat(withoutInput).isEqualTo(expectedWithoutInput)
+ }
+ }
}
@Composable
diff --git a/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/RememberSaveable.kt b/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/RememberSaveable.kt
index 44c0b1f..68f4cee 100644
--- a/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/RememberSaveable.kt
+++ b/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/RememberSaveable.kt
@@ -25,6 +25,7 @@
import androidx.compose.runtime.neverEqualPolicy
import androidx.compose.runtime.referentialEqualityPolicy
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshots.SnapshotMutableState
import androidx.compose.runtime.structuralEqualityPolicy
@@ -87,17 +88,19 @@
restored ?: init()
}
- // save the latest passed saver object into a state object to be able to use it when we will
- // be saving the value. keeping value in mutableStateOf() allows us to properly handle
- // possible compose transactions cancellations
- val saverHolder = remember { mutableStateOf(saver) }
- saverHolder.value = saver
-
// re-register if the registry or key has been changed
if (registry != null) {
- DisposableEffect(registry, finalKey, value) {
+ // we want to use the latest instances of saver and value in the valueProvider lambda
+ // without restarting DisposableEffect as it would cause re-registering the provider in
+ // the different order. so we use rememberUpdatedState.
+ val saverState = rememberUpdatedState(saver)
+ val valueState = rememberUpdatedState(value)
+
+ DisposableEffect(registry, finalKey) {
val valueProvider = {
- with(saverHolder.value) { SaverScope { registry.canBeSaved(it) }.save(value) }
+ with(saverState.value) {
+ SaverScope { registry.canBeSaved(it) }.save(valueState.value)
+ }
}
registry.requireCanBeSaved(valueProvider())
val entry = registry.registerProvider(finalKey, valueProvider)
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index 9e5c9ce..6249ecb 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -922,7 +922,7 @@
public interface PlatformTextInputService {
method public void hideSoftwareKeyboard();
- method public void notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
+ method @Deprecated public default void notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
method public void showSoftwareKeyboard();
method public void startInput(androidx.compose.ui.text.input.TextFieldValue value, androidx.compose.ui.text.input.ImeOptions imeOptions, kotlin.jvm.functions.Function1<? super java.util.List<? extends androidx.compose.ui.text.input.EditCommand>,kotlin.Unit> onEditCommand, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed);
method public void stopInput();
@@ -999,7 +999,7 @@
method public void dispose();
method public boolean hideSoftwareKeyboard();
method public boolean isOpen();
- method public boolean notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
+ method @Deprecated public boolean notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
method public boolean showSoftwareKeyboard();
method public boolean updateState(androidx.compose.ui.text.input.TextFieldValue? oldValue, androidx.compose.ui.text.input.TextFieldValue newValue);
property public final boolean isOpen;
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index 49abfc4..71868a4 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -960,7 +960,7 @@
public interface PlatformTextInputService {
method public void hideSoftwareKeyboard();
- method public void notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
+ method @Deprecated public default void notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
method public void showSoftwareKeyboard();
method public void startInput(androidx.compose.ui.text.input.TextFieldValue value, androidx.compose.ui.text.input.ImeOptions imeOptions, kotlin.jvm.functions.Function1<? super java.util.List<? extends androidx.compose.ui.text.input.EditCommand>,kotlin.Unit> onEditCommand, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed);
method public void stopInput();
@@ -1037,7 +1037,7 @@
method public void dispose();
method public boolean hideSoftwareKeyboard();
method public boolean isOpen();
- method public boolean notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
+ method @Deprecated public boolean notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
method public boolean showSoftwareKeyboard();
method public boolean updateState(androidx.compose.ui.text.input.TextFieldValue? oldValue, androidx.compose.ui.text.input.TextFieldValue newValue);
property public final boolean isOpen;
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index 9e5c9ce..6249ecb 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -922,7 +922,7 @@
public interface PlatformTextInputService {
method public void hideSoftwareKeyboard();
- method public void notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
+ method @Deprecated public default void notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
method public void showSoftwareKeyboard();
method public void startInput(androidx.compose.ui.text.input.TextFieldValue value, androidx.compose.ui.text.input.ImeOptions imeOptions, kotlin.jvm.functions.Function1<? super java.util.List<? extends androidx.compose.ui.text.input.EditCommand>,kotlin.Unit> onEditCommand, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed);
method public void stopInput();
@@ -999,7 +999,7 @@
method public void dispose();
method public boolean hideSoftwareKeyboard();
method public boolean isOpen();
- method public boolean notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
+ method @Deprecated public boolean notifyFocusedRect(androidx.compose.ui.geometry.Rect rect);
method public boolean showSoftwareKeyboard();
method public boolean updateState(androidx.compose.ui.text.input.TextFieldValue? oldValue, androidx.compose.ui.text.input.TextFieldValue newValue);
property public final boolean isOpen;
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
index e145592..a97d3cd 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
@@ -147,17 +147,9 @@
}
}
- /**
- * Notify the focused rectangle to the system.
- *
- * If the session is not open, no action will be performed.
- *
- * @param rect the rectangle that describes the boundaries on the screen that requires focus
- * @return false if this session expired and no action was performed
- */
- fun notifyFocusedRect(rect: Rect): Boolean = ensureOpenSession {
- platformTextInputService.notifyFocusedRect(rect)
- }
+ @Suppress("DeprecatedCallableAddReplaceWith", "UNUSED_PARAMETER")
+ @Deprecated("This method is not called, used BringIntoViewRequester instead.")
+ fun notifyFocusedRect(rect: Rect): Boolean = false
/**
* Notify IME about the new [TextFieldValue] and latest state of the editing buffer. [oldValue]
@@ -261,10 +253,7 @@
*/
fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue)
- /**
- * Notify the focused rectangle to the system.
- *
- * @see TextInputSession.notifyFocusedRect
- */
- fun notifyFocusedRect(rect: Rect)
+ @Deprecated("This method is not called, used BringIntoViewRequester instead.")
+ fun notifyFocusedRect(rect: Rect) {
+ }
}
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextInputServiceTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextInputServiceTest.kt
index 92c1bb1..c904ccd 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextInputServiceTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextInputServiceTest.kt
@@ -16,9 +16,6 @@
package androidx.compose.ui.text
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.geometry.Size
import androidx.compose.ui.text.input.ImeOptions
import androidx.compose.ui.text.input.PlatformTextInputService
import androidx.compose.ui.text.input.TextFieldValue
@@ -238,51 +235,4 @@
secondSession.updateState(null, editorModel)
verify(platformService).updateState(eq(null), eq(editorModel))
}
-
- @Test
- fun notifyFocusedRect_with_valid_token() {
- val platformService = mock<PlatformTextInputService>()
-
- val textInputService = TextInputService(platformService)
-
- val firstSession = textInputService.startInput(
- TextFieldValue(),
- ImeOptions.Default,
- {}, // onEditCommand
- {} // onImeActionPerformed
- )
-
- val rect = Rect(Offset.Zero, Size(100f, 100f))
- firstSession.notifyFocusedRect(rect)
- verify(platformService, times(1)).notifyFocusedRect(eq(rect))
- }
-
- @Test
- fun notifyFocusedRect_with_expired_token() {
- val platformService = mock<PlatformTextInputService>()
-
- val textInputService = TextInputService(platformService)
-
- val firstSession = textInputService.startInput(
- TextFieldValue(),
- ImeOptions.Default,
- {}, // onEditCommand
- {} // onImeActionPerformed
- )
-
- // Start another session. The firstToken is now expired.
- val secondSession = textInputService.startInput(
- TextFieldValue(),
- ImeOptions.Default,
- {}, // onEditCommand
- {} // onImeActionPerformed
- )
-
- val rect = Rect(Offset.Zero, Size(100f, 100f))
- firstSession.notifyFocusedRect(rect)
- verify(platformService, never()).notifyFocusedRect(any())
-
- secondSession.notifyFocusedRect(rect)
- verify(platformService, times(1)).notifyFocusedRect(eq(rect))
- }
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CustomFocusTraversalTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CustomFocusTraversalTest.kt
index 9e2ccd6..91fdc26 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CustomFocusTraversalTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CustomFocusTraversalTest.kt
@@ -89,7 +89,9 @@
// Act.
if (moveFocusProgrammatically) {
- focusManager.moveFocus(FocusDirection.Next)
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Next)
+ }
} else {
rule.onRoot().performKeyPress(KeyEvent(AndroidKeyEvent(KeyDown, Tab.nativeKeyCode)))
}
@@ -136,7 +138,9 @@
// Act.
if (moveFocusProgrammatically) {
- focusManager.moveFocus(FocusDirection.Previous)
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Previous)
+ }
} else {
val nativeEvent = AndroidKeyEvent(0L, 0L, KeyDown, Tab.nativeKeyCode, 0, META_SHIFT_ON)
rule.onRoot().performKeyPress(KeyEvent(nativeEvent))
@@ -184,7 +188,9 @@
// Act.
if (moveFocusProgrammatically) {
- focusManager.moveFocus(FocusDirection.Up)
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Up)
+ }
} else {
val nativeKeyEvent = AndroidKeyEvent(KeyDown, DirectionUp.nativeKeyCode)
rule.onRoot().performKeyPress(KeyEvent(nativeKeyEvent))
@@ -232,7 +238,9 @@
// Act.
if (moveFocusProgrammatically) {
- focusManager.moveFocus(FocusDirection.Down)
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Down)
+ }
} else {
val nativeKeyEvent = AndroidKeyEvent(KeyDown, DirectionDown.nativeKeyCode)
rule.onRoot().performKeyPress(KeyEvent(nativeKeyEvent))
@@ -280,7 +288,9 @@
// Act.
if (moveFocusProgrammatically) {
- focusManager.moveFocus(FocusDirection.Left)
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Left)
+ }
} else {
val nativeKeyEvent = AndroidKeyEvent(KeyDown, DirectionLeft.nativeKeyCode)
rule.onRoot().performKeyPress(KeyEvent(nativeKeyEvent))
@@ -328,7 +338,9 @@
// Act.
if (moveFocusProgrammatically) {
- focusManager.moveFocus(FocusDirection.Right)
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Right)
+ }
} else {
val nativeKeyEvent = AndroidKeyEvent(KeyDown, DirectionRight.nativeKeyCode)
rule.onRoot().performKeyPress(KeyEvent(nativeKeyEvent))
@@ -378,7 +390,9 @@
// Act.
if (moveFocusProgrammatically) {
- focusManager.moveFocus(FocusDirection.Left)
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Left)
+ }
} else {
val nativeKeyEvent = AndroidKeyEvent(KeyDown, DirectionLeft.nativeKeyCode)
rule.onRoot().performKeyPress(KeyEvent(nativeKeyEvent))
@@ -428,7 +442,9 @@
// Act.
if (moveFocusProgrammatically) {
- focusManager.moveFocus(FocusDirection.Right)
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Right)
+ }
} else {
val nativeKeyEvent = AndroidKeyEvent(KeyDown, DirectionRight.nativeKeyCode)
rule.onRoot().performKeyPress(KeyEvent(nativeKeyEvent))
@@ -485,7 +501,9 @@
// Act.
if (moveFocusProgrammatically) {
- focusManager.moveFocus(FocusDirection.Next)
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Next)
+ }
} else {
rule.onRoot().performKeyPress(KeyEvent(AndroidKeyEvent(KeyDown, Tab.nativeKeyCode)))
}
@@ -535,7 +553,9 @@
// Act.
if (moveFocusProgrammatically) {
- focusManager.moveFocus(FocusDirection.Next)
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Next)
+ }
} else {
rule.onRoot().performKeyPress(KeyEvent(AndroidKeyEvent(KeyDown, Tab.nativeKeyCode)))
}
@@ -584,7 +604,9 @@
// Act.
if (moveFocusProgrammatically) {
- focusManager.moveFocus(FocusDirection.Next)
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Next)
+ }
} else {
rule.onRoot().performKeyPress(KeyEvent(AndroidKeyEvent(KeyDown, Tab.nativeKeyCode)))
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt
new file mode 100644
index 0000000..47e1944
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.focus
+
+import android.graphics.Rect as AndroidRect
+import android.view.View
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class FocusViewInteropTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun getFocusedRect_reportsFocusBounds_whenFocused() {
+ val focusRequester = FocusRequester()
+ var hasFocus = false
+ lateinit var view: View
+ rule.setContent {
+ view = LocalView.current
+ CompositionLocalProvider(LocalDensity provides Density(density = 1f)) {
+ Box(
+ Modifier
+ .size(90.dp, 100.dp)
+ .wrapContentSize(align = Alignment.TopStart)
+ .size(10.dp, 20.dp)
+ .offset(30.dp, 40.dp)
+ .onFocusChanged {
+ if (it.isFocused) {
+ hasFocus = true
+ }
+ }
+ .focusRequester(focusRequester)
+ .focusable()
+ )
+ }
+ }
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ }
+
+ rule.waitUntil { hasFocus }
+
+ assertThat(view.getFocusedRect()).isEqualTo(IntRect(30, 40, 40, 60))
+ }
+
+ @Test
+ fun getFocusedRect_reportsEntireView_whenNoFocus() {
+ lateinit var view: View
+ rule.setContent {
+ view = LocalView.current
+ CompositionLocalProvider(LocalDensity provides Density(density = 1f)) {
+ Box(
+ Modifier
+ .size(90.dp, 100.dp)
+ .wrapContentSize(align = Alignment.TopStart)
+ .size(10.dp, 20.dp)
+ .offset(30.dp, 40.dp)
+ .focusable()
+ )
+ }
+ }
+
+ assertThat(view.getFocusedRect()).isEqualTo(
+ IntRect(0, 0, 90, 100)
+ )
+ }
+
+ private fun View.getFocusedRect() = AndroidRect().run {
+ rule.runOnIdle {
+ getFocusedRect(this)
+ }
+ IntRect(left, top, right, bottom)
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalThreeItemsTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalThreeItemsTest.kt
index e74a88a..414f89d 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalThreeItemsTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalThreeItemsTest.kt
@@ -115,7 +115,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -184,7 +186,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -258,7 +262,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -332,7 +338,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -402,7 +410,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -472,7 +482,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -542,7 +554,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -612,7 +626,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -682,7 +698,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -752,7 +770,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -821,7 +841,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -891,7 +913,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -961,7 +985,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1031,7 +1057,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1101,7 +1129,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1170,7 +1200,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1240,7 +1272,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1310,7 +1344,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1381,7 +1417,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1451,7 +1489,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1521,7 +1561,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1592,7 +1634,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1660,7 +1704,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1729,7 +1775,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1798,7 +1846,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1867,7 +1917,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1934,7 +1986,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2001,7 +2055,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2070,7 +2126,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2140,7 +2198,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2210,7 +2270,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2279,7 +2341,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2344,7 +2408,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2412,7 +2478,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2477,7 +2545,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2544,7 +2614,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2613,7 +2685,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2683,7 +2757,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2753,7 +2829,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2821,7 +2899,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2885,7 +2965,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2948,7 +3030,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3011,7 +3095,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3076,7 +3162,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3145,7 +3233,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3212,7 +3302,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3281,7 +3373,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3348,7 +3442,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3417,7 +3513,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3484,7 +3582,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3554,7 +3654,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3632,7 +3734,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3711,7 +3815,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3792,7 +3898,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3872,7 +3980,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3950,7 +4060,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4031,7 +4143,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4111,7 +4225,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4189,7 +4305,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4260,7 +4378,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4329,7 +4449,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4398,7 +4520,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4468,7 +4592,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4538,7 +4664,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4607,7 +4735,9 @@
}
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4676,7 +4806,9 @@
}
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4745,7 +4877,9 @@
}
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4808,7 +4942,9 @@
}
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4881,7 +5017,9 @@
}
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4954,7 +5092,9 @@
}
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -5027,7 +5167,9 @@
}
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTwoItemsTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTwoItemsTest.kt
index 00eed69..52425ca 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTwoItemsTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTwoItemsTest.kt
@@ -77,7 +77,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -113,7 +115,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -149,7 +153,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -185,7 +191,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -221,7 +229,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -257,7 +267,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -293,7 +305,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -329,7 +343,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -364,7 +380,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -398,7 +416,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -432,7 +452,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -467,7 +489,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -502,7 +526,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -537,7 +563,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -572,7 +600,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -605,7 +635,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -639,7 +671,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -673,7 +707,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -708,7 +744,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -744,7 +782,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -780,7 +820,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -816,7 +858,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -852,7 +896,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -888,7 +934,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -924,7 +972,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -960,7 +1010,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -996,7 +1048,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1032,7 +1086,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1068,7 +1124,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1104,7 +1162,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1140,7 +1200,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1175,7 +1237,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1209,7 +1273,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1243,7 +1309,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1276,7 +1344,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1311,7 +1381,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1346,7 +1418,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1381,7 +1455,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1416,7 +1492,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1450,7 +1528,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1484,7 +1564,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1519,7 +1601,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1555,7 +1639,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1591,7 +1677,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1627,7 +1715,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1663,7 +1753,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1698,7 +1790,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1733,7 +1827,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1768,7 +1864,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1803,7 +1901,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1838,7 +1938,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1873,7 +1975,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1908,7 +2012,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1942,7 +2048,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -1976,7 +2084,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2011,7 +2121,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2046,7 +2158,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2081,7 +2195,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2116,7 +2232,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2149,7 +2267,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2183,7 +2303,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2217,7 +2339,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2252,7 +2376,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2287,7 +2413,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2322,7 +2450,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2357,7 +2487,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2392,7 +2524,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2427,7 +2561,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2462,7 +2598,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2497,7 +2635,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2532,7 +2672,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2567,7 +2709,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2601,7 +2745,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2635,7 +2781,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2668,7 +2816,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2703,7 +2853,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2738,7 +2890,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2773,7 +2927,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2808,7 +2964,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2842,7 +3000,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2876,7 +3036,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2911,7 +3073,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2946,7 +3110,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -2981,7 +3147,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3017,7 +3185,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3053,7 +3223,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3089,7 +3261,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3125,7 +3299,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3161,7 +3337,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3195,7 +3373,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3229,7 +3409,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3264,7 +3446,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3299,7 +3483,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3334,7 +3520,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3369,7 +3557,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3402,7 +3592,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3436,7 +3628,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3470,7 +3664,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3506,7 +3702,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3542,7 +3740,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3578,7 +3778,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3614,7 +3816,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3650,7 +3854,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3686,7 +3892,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3720,7 +3928,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3754,7 +3964,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3787,7 +3999,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3822,7 +4036,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3857,7 +4073,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3892,7 +4110,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3927,7 +4147,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3961,7 +4183,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -3995,7 +4219,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4031,7 +4257,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4066,7 +4294,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4092,7 +4322,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4118,7 +4350,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4144,7 +4378,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4170,7 +4406,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4196,7 +4434,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4222,7 +4462,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4248,7 +4490,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4275,7 +4519,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4301,7 +4547,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4327,7 +4575,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4353,7 +4603,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4379,7 +4631,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4405,7 +4659,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4431,7 +4687,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4457,7 +4715,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4483,7 +4743,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4510,7 +4772,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4575,7 +4839,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
@@ -4595,7 +4861,9 @@
}
// Act.
- focusManager.moveFocus(focusDirection)
+ rule.runOnIdle {
+ focusManager.moveFocus(focusDirection)
+ }
// Assert.
rule.runOnIdle {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
index 8637ce5..dc297f4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
@@ -35,6 +35,8 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
@@ -55,6 +57,7 @@
import androidx.compose.ui.test.assertPositionInRootIsEqualTo
import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.Constraints
@@ -67,14 +70,14 @@
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.Assert.assertEquals
+import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import org.junit.Assert.assertThrows
@MediumTest
@RunWith(AndroidJUnit4::class)
@@ -1756,6 +1759,44 @@
}
}
+ @Test
+ fun stateIsRestoredWhenGoBackToScreen1WithSubcomposition() {
+ val restorationTester = StateRestorationTester(rule)
+
+ var increment = 0
+ var screen by mutableStateOf(Screens.Screen1)
+ var restorableNumberOnScreen1 = -1
+ restorationTester.setContent {
+ val holder = rememberSaveableStateHolder()
+ holder.SaveableStateProvider(screen) {
+ if (screen == Screens.Screen1) {
+ SubcomposeLayout {
+ subcompose(Unit) {
+ restorableNumberOnScreen1 = rememberSaveable { increment++ }
+ }
+ layout(10, 10) {}
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(restorableNumberOnScreen1).isEqualTo(0)
+ screen = Screens.Screen2
+ }
+
+ // wait for the screen switch to apply
+ rule.runOnIdle {
+ restorableNumberOnScreen1 = -1
+ // switch back to screen1
+ screen = Screens.Screen1
+ }
+
+ rule.runOnIdle {
+ assertThat(restorableNumberOnScreen1).isEqualTo(0)
+ }
+ }
+
private fun composeItems(
state: SubcomposeLayoutState,
items: MutableState<List<Int>>
@@ -1804,4 +1845,9 @@
placeable.place(0, 0)
}
}
+}
+
+private enum class Screens {
+ Screen1,
+ Screen2,
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index d8bcdc2..b68ed8e 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.platform
+import android.view.KeyEvent as AndroidKeyEvent
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
@@ -113,6 +114,7 @@
import androidx.compose.ui.input.rotary.RotaryScrollEvent
import androidx.compose.ui.input.rotary.onRotaryScrollEvent
import androidx.compose.ui.layout.RootMeasurePolicy
+import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.node.InternalCoreApi
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.node.LayoutNode.UsageByParent
@@ -153,7 +155,7 @@
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.ViewTreeSavedStateRegistryOwner
import java.lang.reflect.Method
-import android.view.KeyEvent as AndroidKeyEvent
+import kotlin.math.roundToInt
@SuppressLint("ViewConstructor", "VisibleForTests")
@OptIn(ExperimentalComposeUiApi::class)
@@ -525,6 +527,19 @@
}
}
+ /**
+ * Since this view has its own concept of internal focus, it needs to report that to the view
+ * system for accurate focus searching and so ViewRootImpl will scroll correctly.
+ */
+ override fun getFocusedRect(rect: Rect) {
+ _focusManager.getActiveFocusModifier()?.focusNode?.boundsInRoot()?.let {
+ rect.left = it.left.roundToInt()
+ rect.top = it.top.roundToInt()
+ rect.right = it.right.roundToInt()
+ rect.bottom = it.bottom.roundToInt()
+ } ?: super.getFocusedRect(rect)
+ }
+
override fun onResume(owner: LifecycleOwner) {
// Refresh in onResume in case the value has changed.
@OptIn(InternalCoreApi::class)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
index 6a5dc7c..39afec1 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
@@ -20,18 +20,15 @@
import android.util.Log
import android.view.KeyEvent
import android.view.View
-import android.view.ViewTreeObserver
import android.view.inputmethod.BaseInputConnection
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
-import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextInputServiceAndroid.TextInputCommand.HideKeyboard
import androidx.compose.ui.text.input.TextInputServiceAndroid.TextInputCommand.ShowKeyboard
import androidx.compose.ui.text.input.TextInputServiceAndroid.TextInputCommand.StartInput
import androidx.compose.ui.text.input.TextInputServiceAndroid.TextInputCommand.StopInput
import androidx.core.view.inputmethod.EditorInfoCompat
-import kotlin.math.roundToInt
import kotlinx.coroutines.channels.Channel
private const val DEBUG_CLASS = "TextInputServiceAndroid"
@@ -78,8 +75,6 @@
BaseInputConnection(view, false)
}
- private var focusedRect: android.graphics.Rect? = null
-
/**
* A channel that is used to debounce rapid operations such as showing/hiding the keyboard and
* starting/stopping input, so we can make the minimal number of calls on the
@@ -88,30 +83,10 @@
*/
private val textInputCommandChannel = Channel<TextInputCommand>(Channel.UNLIMITED)
- private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
- // focusedRect is null if there is not ongoing text input session. So safe to request
- // latest focused rectangle whenever global layout has changed.
- focusedRect?.let {
- // Notice that view.requestRectangleOnScreen may modify the input Rect, we have to
- // create another Rect and then pass it.
- view.requestRectangleOnScreen(android.graphics.Rect(it))
- }
- }
-
internal constructor(view: View) : this(view, InputMethodManagerImpl(view.context))
init {
if (DEBUG) { Log.d(TAG, "$DEBUG_CLASS.create") }
-
- view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
- override fun onViewDetachedFromWindow(v: View?) {
- v?.rootView?.viewTreeObserver?.removeOnGlobalLayoutListener(layoutListener)
- }
-
- override fun onViewAttachedToWindow(v: View?) {
- v?.rootView?.viewTreeObserver?.addOnGlobalLayoutListener(layoutListener)
- }
- })
}
/**
@@ -178,7 +153,6 @@
editorHasFocus = false
onEditCommand = {}
onImeActionPerformed = {}
- focusedRect = null
// Don't actually send the command to the IME yet, it may be overruled by a subsequent call
// to startInput.
@@ -353,29 +327,6 @@
}
}
- override fun notifyFocusedRect(rect: Rect) {
- focusedRect = android.graphics.Rect(
- rect.left.roundToInt(),
- rect.top.roundToInt(),
- rect.right.roundToInt(),
- rect.bottom.roundToInt()
- )
-
- // Requesting rectangle too early after obtaining focus may bring view into wrong place
- // probably due to transient IME inset change. We don't know the correct timing of calling
- // requestRectangleOnScreen API, so try to call this API only after the IME is ready to
- // use, i.e. InputConnection has created.
- // Even if we miss all the timing of requesting rectangle during initial text field focus,
- // focused rectangle will be requested when software keyboard has shown.
- if (ic == null) {
- focusedRect?.let {
- // Notice that view.requestRectangleOnScreen may modify the input Rect, we have to
- // create another Rect and then pass it.
- view.requestRectangleOnScreen(android.graphics.Rect(it))
- }
- }
- }
-
/** Immediately restart the IME connection, bypassing the [textInputCommandChannel]. */
private fun restartInputImmediately() {
if (DEBUG) Log.d(TAG, "$DEBUG_CLASS.restartInputImmediately")
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index dbd2034..d719cc7 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -21,10 +21,12 @@
import androidx.compose.runtime.ComposeNode
import androidx.compose.runtime.Composition
import androidx.compose.runtime.CompositionContext
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.currentComposer
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCompositionContext
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.SubcomposeLayoutState.PrecomposedSlotHandle
import androidx.compose.ui.materialize
@@ -119,6 +121,12 @@
state.forceRecomposeChildren()
}
}
+ val stateHolder = rememberUpdatedState(state)
+ DisposableEffect(Unit) {
+ onDispose {
+ stateHolder.value.disposeCurrentNodes()
+ }
+ }
}
/**
@@ -211,6 +219,8 @@
internal fun forceRecomposeChildren() = state.forceRecomposeChildren()
+ internal fun disposeCurrentNodes() = state.disposeCurrentNodes()
+
/**
* Instance of this interface is returned by [precompose] function.
*/
@@ -421,6 +431,8 @@
reusableCount++
} else {
ignoreRemeasureRequests {
+ val nodeState = nodeToNodeState.remove(root.foldedChildren[i])!!
+ nodeState.composition?.dispose()
root.removeAt(i, 1)
}
}
@@ -486,12 +498,6 @@
}
}
- private fun disposeNode(node: LayoutNode) {
- val nodeState = nodeToNodeState.remove(node)!!
- nodeState.composition?.dispose()
- slotIdToNode.remove(nodeState.slotId)
- }
-
fun createMeasurePolicy(
block: SubcomposeMeasureScope.(Constraints) -> MeasureResult
): MeasurePolicy = object : LayoutNode.NoIntrinsicsMeasurePolicy(error = NoIntrinsicsMessage) {
@@ -601,12 +607,6 @@
ignoreRemeasureRequests {
root.insertAt(index, node)
}
- node.onDetach = {
- disposeNode(node)
- node.onAttach = {
- throw IllegalStateException("Disposed node shouldn't be reattached")
- }
- }
}
private fun move(from: Int, to: Int, count: Int = 1) {
@@ -618,6 +618,14 @@
private inline fun ignoreRemeasureRequests(block: () -> Unit) =
root.ignoreRemeasureRequests(block)
+ fun disposeCurrentNodes() {
+ nodeToNodeState.values.forEach {
+ it.composition?.dispose()
+ }
+ nodeToNodeState.clear()
+ slotIdToNode.clear()
+ }
+
private class NodeState(
var slotId: Any?,
var content: @Composable () -> Unit,
diff --git a/lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/Transformations.kt b/lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/Transformations.kt
index 656d4b1..3e22243 100644
--- a/lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/Transformations.kt
+++ b/lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/Transformations.kt
@@ -79,7 +79,7 @@
*/
@CheckResult
public inline fun <X, Y> LiveData<X>.switchMap(
- crossinline transform: (X) -> LiveData<Y>
+ crossinline transform: (X) -> LiveData<Y>?
): LiveData<Y> = Transformations.switchMap(this) { transform(it) }
/**
diff --git a/lifecycle/lifecycle-process/build.gradle b/lifecycle/lifecycle-process/build.gradle
index 3369989..b09c8bc 100644
--- a/lifecycle/lifecycle-process/build.gradle
+++ b/lifecycle/lifecycle-process/build.gradle
@@ -31,7 +31,7 @@
dependencies {
api(project(":lifecycle:lifecycle-runtime"))
- api("androidx.startup:startup-runtime:1.0.0")
+ api("androidx.startup:startup-runtime:1.1.1")
api("androidx.annotation:annotation:1.2.0")
testImplementation(libs.junit)
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleTest.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleTest.kt
index 8e3a3fb..6059aec 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleTest.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/androidTest/java/androidx/lifecycle/viewmodel/savedstate/SavedStateHandleTest.kt
@@ -140,6 +140,15 @@
@Test
@UiThreadTest
+ fun newliveData_withInitialGet() {
+ val handle = SavedStateHandle()
+ val ld: LiveData<String?> = handle.getLiveData("aa", "xx")
+ ld.assertValue("xx")
+ assertThat(handle.get<String?>("aa")).isEqualTo("xx")
+ }
+
+ @Test
+ @UiThreadTest
fun newLiveData_existingValue_withInitial() {
val handle = SavedStateHandle()
handle["aa"] = "existing"
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.kt
index 7824d94..61add75 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.kt
@@ -159,6 +159,7 @@
@Suppress("UNCHECKED_CAST")
SavingStateLiveData(this, key, regular[key] as T)
} else if (hasInitialValue) {
+ regular[key] = initialValue
SavingStateLiveData(this, key, initialValue)
} else {
SavingStateLiveData(this, key)