Merge "Add `AnimatedSpatialVisibility` API and use it in XR `ThreePaneScaffold`" into androidx-main
diff --git a/xr/compose/compose/api/current.txt b/xr/compose/compose/api/current.txt
index 7dde2ab..248feae 100644
--- a/xr/compose/compose/api/current.txt
+++ b/xr/compose/compose/api/current.txt
@@ -369,6 +369,67 @@
}
+package androidx.xr.compose.subspace.animation {
+
+ public final class AnimatedSpatialVisibilityKt {
+ method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void AnimatedSpatialVisibility(androidx.compose.animation.core.MutableTransitionState<java.lang.Boolean> visibleState, optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.animation.SpatialEnterTransition enter, optional androidx.xr.compose.subspace.animation.SpatialExitTransition exit, optional String label, kotlin.jvm.functions.Function1<androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope,kotlin.Unit> content);
+ method @BytecodeOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void AnimatedSpatialVisibility(androidx.compose.animation.core.MutableTransitionState<java.lang.Boolean!>, androidx.xr.compose.subspace.layout.SubspaceModifier?, androidx.xr.compose.subspace.animation.SpatialEnterTransition?, androidx.xr.compose.subspace.animation.SpatialExitTransition?, String?, kotlin.jvm.functions.Function3<? super androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope!,? super androidx.compose.runtime.Composer!,? super java.lang.Integer!,kotlin.Unit!>, androidx.compose.runtime.Composer?, int, int);
+ method @BytecodeOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static <T> void AnimatedSpatialVisibility(androidx.compose.animation.core.Transition<T!>, kotlin.jvm.functions.Function1<? super T!,java.lang.Boolean!>, androidx.xr.compose.subspace.layout.SubspaceModifier?, androidx.xr.compose.subspace.animation.SpatialEnterTransition?, androidx.xr.compose.subspace.animation.SpatialExitTransition?, kotlin.jvm.functions.Function3<? super androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope!,? super androidx.compose.runtime.Composer!,? super java.lang.Integer!,kotlin.Unit!>, androidx.compose.runtime.Composer?, int, int);
+ method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static <T> void AnimatedSpatialVisibility(androidx.compose.animation.core.Transition<T>, kotlin.jvm.functions.Function1<T,java.lang.Boolean> visible, optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.animation.SpatialEnterTransition enter, optional androidx.xr.compose.subspace.animation.SpatialExitTransition exit, kotlin.jvm.functions.Function1<androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope,kotlin.Unit> content);
+ method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void AnimatedSpatialVisibility(boolean visible, optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.animation.SpatialEnterTransition enter, optional androidx.xr.compose.subspace.animation.SpatialExitTransition exit, optional String label, kotlin.jvm.functions.Function1<androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope,kotlin.Unit> content);
+ method @BytecodeOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void AnimatedSpatialVisibility(boolean, androidx.xr.compose.subspace.layout.SubspaceModifier?, androidx.xr.compose.subspace.animation.SpatialEnterTransition?, androidx.xr.compose.subspace.animation.SpatialExitTransition?, String?, kotlin.jvm.functions.Function3<? super androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope!,? super androidx.compose.runtime.Composer!,? super java.lang.Integer!,kotlin.Unit!>, androidx.compose.runtime.Composer?, int, int);
+ }
+
+ public sealed interface AnimatedSpatialVisibilityScope extends androidx.compose.animation.AnimatedVisibilityScope {
+ method @KotlinOnly @androidx.compose.runtime.Composable public default androidx.xr.compose.subspace.layout.SubspaceModifier animateEnterExit(androidx.xr.compose.subspace.layout.SubspaceModifier, optional androidx.xr.compose.subspace.animation.SpatialEnterTransition enter, optional androidx.xr.compose.subspace.animation.SpatialExitTransition exit);
+ method @BytecodeOnly @androidx.compose.runtime.Composable public default androidx.xr.compose.subspace.layout.SubspaceModifier animateEnterExit(androidx.xr.compose.subspace.layout.SubspaceModifier, androidx.xr.compose.subspace.animation.SpatialEnterTransition, androidx.xr.compose.subspace.animation.SpatialExitTransition, androidx.compose.runtime.Composer?, int);
+ method @BytecodeOnly @Deprecated @androidx.compose.runtime.Composable public default androidx.xr.compose.subspace.layout.SubspaceModifier! animateEnterExit(androidx.xr.compose.subspace.layout.SubspaceModifier!, androidx.xr.compose.subspace.animation.SpatialEnterTransition!, androidx.xr.compose.subspace.animation.SpatialExitTransition!, androidx.compose.runtime.Composer!, int, int);
+ }
+
+ public abstract sealed class SpatialEnterTransition {
+ method @androidx.compose.runtime.Stable public final operator androidx.xr.compose.subspace.animation.SpatialEnterTransition plus(androidx.xr.compose.subspace.animation.SpatialEnterTransition enter);
+ field public static final androidx.xr.compose.subspace.animation.SpatialEnterTransition.Companion Companion;
+ }
+
+ public static final class SpatialEnterTransition.Companion {
+ method @InaccessibleFromKotlin public androidx.xr.compose.subspace.animation.SpatialEnterTransition getNone();
+ property public androidx.xr.compose.subspace.animation.SpatialEnterTransition None;
+ }
+
+ public abstract sealed class SpatialExitTransition {
+ method @androidx.compose.runtime.Stable public final operator androidx.xr.compose.subspace.animation.SpatialExitTransition plus(androidx.xr.compose.subspace.animation.SpatialExitTransition exit);
+ field public static final androidx.xr.compose.subspace.animation.SpatialExitTransition.Companion Companion;
+ }
+
+ public static final class SpatialExitTransition.Companion {
+ method @InaccessibleFromKotlin public androidx.xr.compose.subspace.animation.SpatialExitTransition getNone();
+ property public androidx.xr.compose.subspace.animation.SpatialExitTransition None;
+ }
+
+ public final class SpatialTransitionDefaults {
+ method @InaccessibleFromKotlin public androidx.xr.compose.subspace.animation.SpatialEnterTransition getDefaultEnter();
+ method @InaccessibleFromKotlin public androidx.xr.compose.subspace.animation.SpatialExitTransition getDefaultExit();
+ property public androidx.xr.compose.subspace.animation.SpatialEnterTransition DefaultEnter;
+ property public androidx.xr.compose.subspace.animation.SpatialExitTransition DefaultExit;
+ field public static final androidx.xr.compose.subspace.animation.SpatialTransitionDefaults INSTANCE;
+ }
+
+ public final class SpatialTransitions {
+ method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialEnterTransition fadeIn(optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, optional float initialAlpha);
+ method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialExitTransition fadeOut(optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, optional float targetAlpha);
+ method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialEnterTransition slideIn(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super androidx.xr.compose.unit.IntVolumeSize,androidx.xr.compose.unit.IntVolumeOffset> initialOffset);
+ method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialEnterTransition slideInDepth(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> initialOffsetZ);
+ method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialEnterTransition slideInHorizontally(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> initialOffsetX);
+ method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialEnterTransition slideInVertically(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> initialOffsetY);
+ method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialExitTransition slideOut(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super androidx.xr.compose.unit.IntVolumeSize,androidx.xr.compose.unit.IntVolumeOffset> targetOffset);
+ method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialExitTransition slideOutDepth(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> targetOffsetZ);
+ method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialExitTransition slideOutHorizontally(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> targetOffsetX);
+ method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialExitTransition slideOutVertically(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> targetOffsetY);
+ field public static final androidx.xr.compose.subspace.animation.SpatialTransitions INSTANCE;
+ }
+
+}
+
package androidx.xr.compose.subspace.layout {
public final class AlphaKt {
@@ -384,6 +445,10 @@
property public abstract androidx.xr.compose.subspace.layout.SubspaceModifier.Node node;
}
+ public final class LayoutModifierKt {
+ method public static androidx.xr.compose.subspace.layout.SubspaceModifier layout(androidx.xr.compose.subspace.layout.SubspaceModifier, kotlin.jvm.functions.Function3<? super androidx.xr.compose.subspace.layout.SubspaceMeasureScope,? super androidx.xr.compose.subspace.layout.SubspaceMeasurable,? super androidx.xr.compose.unit.VolumeConstraints,? extends androidx.xr.compose.subspace.layout.SubspaceMeasureResult> measure);
+ }
+
public final class OffsetKt {
method @KotlinOnly public static androidx.xr.compose.subspace.layout.SubspaceModifier offset(androidx.xr.compose.subspace.layout.SubspaceModifier, optional androidx.compose.ui.unit.Dp x, optional androidx.compose.ui.unit.Dp y, optional androidx.compose.ui.unit.Dp z);
method @BytecodeOnly public static androidx.xr.compose.subspace.layout.SubspaceModifier offset-qQh39rQ(androidx.xr.compose.subspace.layout.SubspaceModifier, float, float, float);
@@ -977,6 +1042,22 @@
property public androidx.xr.compose.unit.DpVolumeSize Zero;
}
+ @androidx.compose.runtime.Immutable public final class IntVolumeOffset {
+ ctor public IntVolumeOffset(int x, int y, int z);
+ method @InaccessibleFromKotlin public int getX();
+ method @InaccessibleFromKotlin public int getY();
+ method @InaccessibleFromKotlin public int getZ();
+ property public int x;
+ property public int y;
+ property public int z;
+ field public static final androidx.xr.compose.unit.IntVolumeOffset.Companion Companion;
+ }
+
+ public static final class IntVolumeOffset.Companion {
+ method @InaccessibleFromKotlin public androidx.xr.compose.unit.IntVolumeOffset getZero();
+ property public androidx.xr.compose.unit.IntVolumeOffset Zero;
+ }
+
public final class IntVolumeSize {
ctor public IntVolumeSize(int width, int height, int depth);
method @InaccessibleFromKotlin public int getDepth();
diff --git a/xr/compose/compose/api/restricted_current.txt b/xr/compose/compose/api/restricted_current.txt
index bf97194..096dd52 100644
--- a/xr/compose/compose/api/restricted_current.txt
+++ b/xr/compose/compose/api/restricted_current.txt
@@ -397,6 +397,67 @@
}
+package androidx.xr.compose.subspace.animation {
+
+ public final class AnimatedSpatialVisibilityKt {
+ method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void AnimatedSpatialVisibility(androidx.compose.animation.core.MutableTransitionState<java.lang.Boolean> visibleState, optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.animation.SpatialEnterTransition enter, optional androidx.xr.compose.subspace.animation.SpatialExitTransition exit, optional String label, kotlin.jvm.functions.Function1<androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope,kotlin.Unit> content);
+ method @BytecodeOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void AnimatedSpatialVisibility(androidx.compose.animation.core.MutableTransitionState<java.lang.Boolean!>, androidx.xr.compose.subspace.layout.SubspaceModifier?, androidx.xr.compose.subspace.animation.SpatialEnterTransition?, androidx.xr.compose.subspace.animation.SpatialExitTransition?, String?, kotlin.jvm.functions.Function3<? super androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope!,? super androidx.compose.runtime.Composer!,? super java.lang.Integer!,kotlin.Unit!>, androidx.compose.runtime.Composer?, int, int);
+ method @BytecodeOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static <T> void AnimatedSpatialVisibility(androidx.compose.animation.core.Transition<T!>, kotlin.jvm.functions.Function1<? super T!,java.lang.Boolean!>, androidx.xr.compose.subspace.layout.SubspaceModifier?, androidx.xr.compose.subspace.animation.SpatialEnterTransition?, androidx.xr.compose.subspace.animation.SpatialExitTransition?, kotlin.jvm.functions.Function3<? super androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope!,? super androidx.compose.runtime.Composer!,? super java.lang.Integer!,kotlin.Unit!>, androidx.compose.runtime.Composer?, int, int);
+ method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static <T> void AnimatedSpatialVisibility(androidx.compose.animation.core.Transition<T>, kotlin.jvm.functions.Function1<T,java.lang.Boolean> visible, optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.animation.SpatialEnterTransition enter, optional androidx.xr.compose.subspace.animation.SpatialExitTransition exit, kotlin.jvm.functions.Function1<androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope,kotlin.Unit> content);
+ method @KotlinOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void AnimatedSpatialVisibility(boolean visible, optional androidx.xr.compose.subspace.layout.SubspaceModifier modifier, optional androidx.xr.compose.subspace.animation.SpatialEnterTransition enter, optional androidx.xr.compose.subspace.animation.SpatialExitTransition exit, optional String label, kotlin.jvm.functions.Function1<androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope,kotlin.Unit> content);
+ method @BytecodeOnly @androidx.compose.runtime.Composable @androidx.xr.compose.subspace.SubspaceComposable public static void AnimatedSpatialVisibility(boolean, androidx.xr.compose.subspace.layout.SubspaceModifier?, androidx.xr.compose.subspace.animation.SpatialEnterTransition?, androidx.xr.compose.subspace.animation.SpatialExitTransition?, String?, kotlin.jvm.functions.Function3<? super androidx.xr.compose.subspace.animation.AnimatedSpatialVisibilityScope!,? super androidx.compose.runtime.Composer!,? super java.lang.Integer!,kotlin.Unit!>, androidx.compose.runtime.Composer?, int, int);
+ }
+
+ public sealed interface AnimatedSpatialVisibilityScope extends androidx.compose.animation.AnimatedVisibilityScope {
+ method @KotlinOnly @androidx.compose.runtime.Composable public default androidx.xr.compose.subspace.layout.SubspaceModifier animateEnterExit(androidx.xr.compose.subspace.layout.SubspaceModifier, optional androidx.xr.compose.subspace.animation.SpatialEnterTransition enter, optional androidx.xr.compose.subspace.animation.SpatialExitTransition exit);
+ method @BytecodeOnly @androidx.compose.runtime.Composable public default androidx.xr.compose.subspace.layout.SubspaceModifier animateEnterExit(androidx.xr.compose.subspace.layout.SubspaceModifier, androidx.xr.compose.subspace.animation.SpatialEnterTransition, androidx.xr.compose.subspace.animation.SpatialExitTransition, androidx.compose.runtime.Composer?, int);
+ method @BytecodeOnly @Deprecated @androidx.compose.runtime.Composable public default androidx.xr.compose.subspace.layout.SubspaceModifier! animateEnterExit(androidx.xr.compose.subspace.layout.SubspaceModifier!, androidx.xr.compose.subspace.animation.SpatialEnterTransition!, androidx.xr.compose.subspace.animation.SpatialExitTransition!, androidx.compose.runtime.Composer!, int, int);
+ }
+
+ public abstract sealed class SpatialEnterTransition {
+ method @androidx.compose.runtime.Stable public final operator androidx.xr.compose.subspace.animation.SpatialEnterTransition plus(androidx.xr.compose.subspace.animation.SpatialEnterTransition enter);
+ field public static final androidx.xr.compose.subspace.animation.SpatialEnterTransition.Companion Companion;
+ }
+
+ public static final class SpatialEnterTransition.Companion {
+ method @InaccessibleFromKotlin public androidx.xr.compose.subspace.animation.SpatialEnterTransition getNone();
+ property public androidx.xr.compose.subspace.animation.SpatialEnterTransition None;
+ }
+
+ public abstract sealed class SpatialExitTransition {
+ method @androidx.compose.runtime.Stable public final operator androidx.xr.compose.subspace.animation.SpatialExitTransition plus(androidx.xr.compose.subspace.animation.SpatialExitTransition exit);
+ field public static final androidx.xr.compose.subspace.animation.SpatialExitTransition.Companion Companion;
+ }
+
+ public static final class SpatialExitTransition.Companion {
+ method @InaccessibleFromKotlin public androidx.xr.compose.subspace.animation.SpatialExitTransition getNone();
+ property public androidx.xr.compose.subspace.animation.SpatialExitTransition None;
+ }
+
+ public final class SpatialTransitionDefaults {
+ method @InaccessibleFromKotlin public androidx.xr.compose.subspace.animation.SpatialEnterTransition getDefaultEnter();
+ method @InaccessibleFromKotlin public androidx.xr.compose.subspace.animation.SpatialExitTransition getDefaultExit();
+ property public androidx.xr.compose.subspace.animation.SpatialEnterTransition DefaultEnter;
+ property public androidx.xr.compose.subspace.animation.SpatialExitTransition DefaultExit;
+ field public static final androidx.xr.compose.subspace.animation.SpatialTransitionDefaults INSTANCE;
+ }
+
+ public final class SpatialTransitions {
+ method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialEnterTransition fadeIn(optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, optional float initialAlpha);
+ method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialExitTransition fadeOut(optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, optional float targetAlpha);
+ method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialEnterTransition slideIn(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super androidx.xr.compose.unit.IntVolumeSize,androidx.xr.compose.unit.IntVolumeOffset> initialOffset);
+ method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialEnterTransition slideInDepth(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> initialOffsetZ);
+ method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialEnterTransition slideInHorizontally(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> initialOffsetX);
+ method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialEnterTransition slideInVertically(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> initialOffsetY);
+ method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialExitTransition slideOut(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super androidx.xr.compose.unit.IntVolumeSize,androidx.xr.compose.unit.IntVolumeOffset> targetOffset);
+ method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialExitTransition slideOutDepth(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> targetOffsetZ);
+ method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialExitTransition slideOutHorizontally(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> targetOffsetX);
+ method @androidx.compose.runtime.Stable public androidx.xr.compose.subspace.animation.SpatialExitTransition slideOutVertically(optional androidx.compose.animation.core.FiniteAnimationSpec<androidx.xr.compose.unit.IntVolumeOffset> animationSpec, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super java.lang.Integer,java.lang.Integer> targetOffsetY);
+ field public static final androidx.xr.compose.subspace.animation.SpatialTransitions INSTANCE;
+ }
+
+}
+
package androidx.xr.compose.subspace.layout {
public final class AlphaKt {
@@ -412,6 +473,10 @@
property public abstract androidx.xr.compose.subspace.layout.SubspaceModifier.Node node;
}
+ public final class LayoutModifierKt {
+ method public static androidx.xr.compose.subspace.layout.SubspaceModifier layout(androidx.xr.compose.subspace.layout.SubspaceModifier, kotlin.jvm.functions.Function3<? super androidx.xr.compose.subspace.layout.SubspaceMeasureScope,? super androidx.xr.compose.subspace.layout.SubspaceMeasurable,? super androidx.xr.compose.unit.VolumeConstraints,? extends androidx.xr.compose.subspace.layout.SubspaceMeasureResult> measure);
+ }
+
public final class OffsetKt {
method @KotlinOnly public static androidx.xr.compose.subspace.layout.SubspaceModifier offset(androidx.xr.compose.subspace.layout.SubspaceModifier, optional androidx.compose.ui.unit.Dp x, optional androidx.compose.ui.unit.Dp y, optional androidx.compose.ui.unit.Dp z);
method @BytecodeOnly public static androidx.xr.compose.subspace.layout.SubspaceModifier offset-qQh39rQ(androidx.xr.compose.subspace.layout.SubspaceModifier, float, float, float);
@@ -1039,6 +1104,22 @@
property public androidx.xr.compose.unit.DpVolumeSize Zero;
}
+ @androidx.compose.runtime.Immutable public final class IntVolumeOffset {
+ ctor public IntVolumeOffset(int x, int y, int z);
+ method @InaccessibleFromKotlin public int getX();
+ method @InaccessibleFromKotlin public int getY();
+ method @InaccessibleFromKotlin public int getZ();
+ property public int x;
+ property public int y;
+ property public int z;
+ field public static final androidx.xr.compose.unit.IntVolumeOffset.Companion Companion;
+ }
+
+ public static final class IntVolumeOffset.Companion {
+ method @InaccessibleFromKotlin public androidx.xr.compose.unit.IntVolumeOffset getZero();
+ property public androidx.xr.compose.unit.IntVolumeOffset Zero;
+ }
+
public final class IntVolumeSize {
ctor public IntVolumeSize(int width, int height, int depth);
method @InaccessibleFromKotlin public int getDepth();
diff --git a/xr/compose/compose/samples/src/main/java/androidx/xr/compose/samples/animation/SpatialTransitionSamples.kt b/xr/compose/compose/samples/src/main/java/androidx/xr/compose/samples/animation/SpatialTransitionSamples.kt
new file mode 100644
index 0000000..930bdb4
--- /dev/null
+++ b/xr/compose/compose/samples/src/main/java/androidx/xr/compose/samples/animation/SpatialTransitionSamples.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2025 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.xr.compose.samples.animation
+
+import androidx.annotation.Sampled
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.xr.compose.spatial.ApplicationSubspace
+import androidx.xr.compose.subspace.SpatialPanel
+import androidx.xr.compose.subspace.animation.AnimatedSpatialVisibility
+import androidx.xr.compose.subspace.animation.SpatialTransitions
+import androidx.xr.compose.unit.IntVolumeOffset
+
+@Sampled
+@Composable
+fun SpatialFade() {
+ ApplicationSubspace {
+ val visibleState = remember { MutableTransitionState(false) }
+ visibleState.targetState = true
+
+ AnimatedSpatialVisibility(
+ visibleState = visibleState,
+ enter = SpatialTransitions.fadeIn(initialAlpha = 0.5f),
+ exit =
+ SpatialTransitions.fadeOut(
+ animationSpec = spring(stiffness = Spring.StiffnessVeryLow)
+ ),
+ ) {
+ SpatialPanel { Text("Spatial panel") }
+ }
+ }
+}
+
+@Sampled
+@Composable
+fun SpatialSlide() {
+ ApplicationSubspace {
+ val visibleState = remember { MutableTransitionState(false) }
+ visibleState.targetState = true
+
+ AnimatedSpatialVisibility(
+ visibleState = visibleState,
+ // if sliding on multiple axes, an initial/target point must be given
+ enter =
+ SpatialTransitions.slideIn { size ->
+ IntVolumeOffset(
+ // negative x: from left to right
+ x = -size.width / 2,
+ // negative y: from bottom to top
+ y = -size.height / 2,
+ // negative z: from far to close
+ z = -size.depth / 2,
+ )
+ },
+ // defaults are provided when only sliding on one axis
+ exit = SpatialTransitions.slideOutHorizontally(),
+ ) {
+ SpatialPanel { Text("Spatial panel") }
+ }
+ }
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/AnimatedSpatialVisibility.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/AnimatedSpatialVisibility.kt
new file mode 100644
index 0000000..789a2a5
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/AnimatedSpatialVisibility.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2025 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.xr.compose.subspace.animation
+
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.rememberTransition
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.runtime.Composable
+import androidx.xr.compose.subspace.SubspaceComposable
+import androidx.xr.compose.subspace.layout.SubspaceLayout
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+
+/**
+ * [AnimatedSpatialVisibility] composable animates the appearance and disappearance of its subspace
+ * content, as [visible] value changes. Different [SpatialEnterTransition]s and
+ * [SpatialExitTransition]s can be defined in [enter] and [exit] for the appearance and
+ * disappearance animation. There are 4 types of [SpatialEnterTransition] and
+ * [SpatialExitTransition]: Fade, Expand/Shrink, Scale and Slide. The enter transitions can be
+ * combined using `+`. Same for exit transitions. The order of the combination does not matter, as
+ * the transition animations will start simultaneously. See [SpatialEnterTransition] and
+ * [SpatialExitTransition] for details on the three types of transition.
+ *
+ * @param visible defines whether the content should be visible
+ * @param modifier modifier for the [SubspaceLayout] created to contain the [content]
+ * @param enter EnterTransition(s) used for the appearing animation
+ * @param exit ExitTransition(s) used for the disappearing animation
+ * @param label A label to differentiate from other animations in Android Studio animation preview.
+ * @param content Content to appear or disappear based on the value of [visible]
+ * @see androidx.compose.animation.AnimatedVisibility
+ * @see SpatialEnterTransition
+ * @see SpatialExitTransition
+ * @see AnimatedSpatialVisibilityScope
+ */
+@Composable
+@SubspaceComposable
+public fun AnimatedSpatialVisibility(
+ visible: Boolean,
+ modifier: SubspaceModifier = SubspaceModifier,
+ enter: SpatialEnterTransition = SpatialTransitionDefaults.DefaultEnter,
+ exit: SpatialExitTransition = SpatialTransitionDefaults.DefaultExit,
+ label: String = "AnimatedSpatialVisibility",
+ content: @Composable @SubspaceComposable AnimatedSpatialVisibilityScope.() -> Unit,
+) {
+ val transition: Transition<Boolean> = updateTransition(targetState = visible, label = label)
+ AnimatedSpatialVisibilityImpl(transition, { it }, modifier, enter, exit, content)
+}
+
+/**
+ * [AnimatedSpatialVisibility] composable animates the appearance and disappearance of its content,
+ * as [visibleState]'s [targetState][MutableTransitionState.targetState] changes. The [visibleState]
+ * can also be used to observe the state of [AnimatedSpatialVisibility]. For example:
+ * `visibleState.isIdle` indicates whether all the animations have finished in
+ * [AnimatedSpatialVisibility], and `visibleState.currentState` returns the initial state of the
+ * current animations.
+ *
+ * @param visibleState defines whether the content should be visible
+ * @param modifier modifier for the [SubspaceLayout] created to contain the [content]
+ * @param enter EnterTransition(s) used for the appearing animation
+ * @param exit ExitTransition(s) used for the disappearing animation
+ * @param label A label to differentiate from other animations in Android Studio animation preview.
+ * @param content Content to appear or disappear based on the value of [visibleState]
+ * @see androidx.compose.animation.AnimatedVisibility
+ * @see SpatialEnterTransition
+ * @see SpatialExitTransition
+ * @see AnimatedSpatialVisibilityScope
+ */
+@Composable
+@SubspaceComposable
+public fun AnimatedSpatialVisibility(
+ visibleState: MutableTransitionState<Boolean>,
+ modifier: SubspaceModifier = SubspaceModifier,
+ enter: SpatialEnterTransition = SpatialTransitionDefaults.DefaultEnter,
+ exit: SpatialExitTransition = SpatialTransitionDefaults.DefaultExit,
+ label: String = "AnimatedSpatialVisibility",
+ content: @Composable @SubspaceComposable AnimatedSpatialVisibilityScope.() -> Unit,
+) {
+ val transition = rememberTransition(visibleState, label)
+ AnimatedSpatialVisibilityImpl(transition, { it }, modifier, enter, exit, content)
+}
+
+/**
+ * This extension function creates an [AnimatedSpatialVisibility] composable as a child Transition
+ * of the given Transition. This means: 1) the enter/exit transition is now triggered by the
+ * provided [Transition]'s [targetState][Transition.targetState] change. When the targetState
+ * changes, the visibility will be derived using the [visible] lambda and
+ * [Transition.targetState]. 2) The enter/exit transitions, as well as any custom enter/exit
+ * animations defined in [AnimatedSpatialVisibility] are now hoisted to the parent Transition. The
+ * parent Transition will wait for all of them to finish before it considers itself finished (i.e.
+ * [Transition.currentState] = [Transition.targetState]), and subsequently removes the content in
+ * the exit case.
+ *
+ * @param visible defines whether the content should be visible based on transition state T
+ * @param modifier modifier for the [SubspaceLayout] created to contain the [content]
+ * @param enter EnterTransition(s) used for the appearing animation
+ * @param exit ExitTransition(s) used for the disappearing animation
+ * @param content Content to appear or disappear based on the visibility derived from the
+ * [Transition.targetState] and the provided [visible] lambda
+ * @see androidx.compose.animation.AnimatedVisibility
+ * @see SpatialEnterTransition
+ * @see SpatialExitTransition
+ * @see AnimatedSpatialVisibilityScope
+ */
+@Composable
+@SubspaceComposable
+public fun <T> Transition<T>.AnimatedSpatialVisibility(
+ visible: (T) -> Boolean,
+ modifier: SubspaceModifier = SubspaceModifier,
+ enter: SpatialEnterTransition = SpatialTransitionDefaults.DefaultEnter,
+ exit: SpatialExitTransition = SpatialTransitionDefaults.DefaultExit,
+ content: @Composable @SubspaceComposable AnimatedSpatialVisibilityScope.() -> Unit,
+) {
+ AnimatedSpatialVisibilityImpl(this, visible, modifier, enter, exit, content)
+}
+
+/**
+ * This is the scope for the content of [AnimatedSpatialVisibility]. In this scope, direct and
+ * indirect children of [AnimatedSpatialVisibility] will be able to define their own enter/exit
+ * transitions using the built-in options via [SubspaceModifier.animateEnterExit]. They will also be
+ * able define custom enter/exit animations using the [transition] object.
+ * [AnimatedSpatialVisibility] will ensure both custom and built-in enter/exit animations finish
+ * before it considers itself idle, and subsequently removes its content in the case of exit.
+ *
+ * __Note:__ Custom enter/exit animations that are created *independent* of the
+ * [AnimatedSpatialVisibilityScope.transition] will have no guarantee to finish when exiting, as
+ * [AnimatedSpatialVisibility] would have no visibility of such animations.
+ *
+ * @see AnimatedVisibilityScope
+ */
+public sealed interface AnimatedSpatialVisibilityScope : AnimatedVisibilityScope {
+ @Composable
+ public fun SubspaceModifier.animateEnterExit(
+ enter: SpatialEnterTransition = SpatialTransitionDefaults.DefaultEnter,
+ exit: SpatialExitTransition = SpatialTransitionDefaults.DefaultExit,
+ ): SubspaceModifier = transition.createModifier(enter, exit)
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/AnimatedSpatialVisibilityImpl.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/AnimatedSpatialVisibilityImpl.kt
new file mode 100644
index 0000000..b110c07
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/AnimatedSpatialVisibilityImpl.kt
@@ -0,0 +1,320 @@
+/*
+ * Copyright 2025 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.xr.compose.subspace.animation
+
+import androidx.compose.animation.EnterExitState
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector3D
+import androidx.compose.animation.core.ExperimentalTransitionApi
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.TwoWayConverter
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.createChildTransition
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastMap
+import androidx.compose.ui.util.fastRoundToInt
+import androidx.xr.compose.subspace.SubspaceComposable
+import androidx.xr.compose.subspace.animation.SpatialTransitionDefaults.DefaultAlphaAnimationSpec
+import androidx.xr.compose.subspace.animation.SpatialTransitionDefaults.DefaultSlideAnimationSpec
+import androidx.xr.compose.subspace.layout.SubspaceLayout
+import androidx.xr.compose.subspace.layout.SubspaceMeasurable
+import androidx.xr.compose.subspace.layout.SubspaceMeasurePolicy
+import androidx.xr.compose.subspace.layout.SubspaceMeasureResult
+import androidx.xr.compose.subspace.layout.SubspaceMeasureScope
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.subspace.layout.alpha
+import androidx.xr.compose.subspace.layout.layout
+import androidx.xr.compose.unit.IntVolumeOffset
+import androidx.xr.compose.unit.IntVolumeSize
+import androidx.xr.compose.unit.VolumeConstraints
+import androidx.xr.runtime.math.Pose
+import androidx.xr.runtime.math.Vector3
+import kotlin.math.max
+
+@Composable
+@SubspaceComposable
+internal fun <T> AnimatedSpatialVisibilityImpl(
+ transition: Transition<T>,
+ visible: (T) -> Boolean,
+ modifier: SubspaceModifier,
+ enter: SpatialEnterTransition,
+ exit: SpatialExitTransition,
+ content: @Composable @SubspaceComposable AnimatedSpatialVisibilityScope.() -> Unit,
+) {
+ AnimatedSpatialEnterExitImpl(
+ transition = transition,
+ visible = visible,
+ modifier =
+ modifier.layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ val size =
+ if (!visible(transition.targetState)) {
+ IntVolumeSize.Zero
+ } else {
+ IntVolumeSize(
+ placeable.measuredWidth,
+ placeable.measuredHeight,
+ placeable.measuredDepth,
+ )
+ }
+ layout(size.width, size.height, size.depth) { placeable.place(Pose.Identity) }
+ },
+ enter = enter,
+ exit = exit,
+ shouldDisposeBlock = { current, target ->
+ current == target && target == EnterExitState.PostExit
+ },
+ content = content,
+ )
+}
+
+@OptIn(ExperimentalTransitionApi::class)
+@Composable
+@SubspaceComposable
+private fun <T> AnimatedSpatialEnterExitImpl(
+ transition: Transition<T>,
+ visible: (T) -> Boolean,
+ modifier: SubspaceModifier,
+ enter: SpatialEnterTransition,
+ exit: SpatialExitTransition,
+ shouldDisposeBlock: (EnterExitState, EnterExitState) -> Boolean,
+ content: @Composable @SubspaceComposable AnimatedSpatialVisibilityScope.() -> Unit,
+) {
+ // TODO(kmost): The original implementation of this in AnimatedEnterExitImpl also checks
+ // `transition.isSeeking` and `transition.hasInitialValueAnimations`, but these are internal
+ // APIs, so we can't access them.
+ if (visible(transition.targetState) || visible(transition.currentState)) {
+ val childTransition =
+ transition.createChildTransition(label = "SpatialEnterExitTransition") {
+ transition.targetEnterExit(visible, it)
+ }
+
+ val shouldDisposeBlockUpdated by rememberUpdatedState(shouldDisposeBlock)
+
+ val shouldDisposeAfterExit by
+ produceState(
+ initialValue =
+ shouldDisposeBlock(childTransition.currentState, childTransition.targetState)
+ ) {
+ snapshotFlow { childTransition.exitFinished }
+ .collect {
+ value =
+ if (it) {
+ shouldDisposeBlockUpdated(
+ childTransition.currentState,
+ childTransition.targetState,
+ )
+ } else {
+ false
+ }
+ }
+ }
+ if (!childTransition.exitFinished || !shouldDisposeAfterExit) {
+ val scope = remember(transition) { AnimatedSpatialVisibilityScopeImpl(childTransition) }
+ SubspaceLayout(
+ modifier = modifier.then(childTransition.createModifier(enter, exit)),
+ content = { scope.content() },
+ measurePolicy = remember { AnimatedSpatialEnterExitMeasurePolicy() },
+ )
+ }
+ }
+}
+
+private class AnimatedSpatialEnterExitMeasurePolicy() : SubspaceMeasurePolicy {
+ override fun SubspaceMeasureScope.measure(
+ measurables: List<SubspaceMeasurable>,
+ constraints: VolumeConstraints,
+ ): SubspaceMeasureResult {
+ var maxWidth = 0
+ var maxHeight = 0
+ var maxDepth = 0
+ val placeables =
+ measurables.fastMap {
+ it.measure(constraints).apply {
+ maxWidth = max(maxWidth, measuredWidth)
+ maxHeight = max(maxHeight, measuredHeight)
+ maxDepth = max(maxDepth, measuredDepth)
+ }
+ }
+ return layout(maxWidth, maxHeight, maxDepth) {
+ placeables.fastForEach { it.place(Pose.Identity) }
+ }
+ }
+}
+
+@Composable
+internal fun Transition<EnterExitState>.createModifier(
+ enter: SpatialEnterTransition,
+ exit: SpatialExitTransition,
+): SubspaceModifier {
+ return fadeAnimationModifier(enter, exit).then(slideAnimationModifier(enter, exit))
+}
+
+@Composable
+private fun Transition<EnterExitState>.fadeAnimationModifier(
+ activeEnter: SpatialEnterTransition,
+ activeExit: SpatialExitTransition,
+): SubspaceModifier {
+ // No fade animations
+ if (activeEnter.data.fade == null && activeExit.data.fade == null) return SubspaceModifier
+
+ val alphaSpec: @Composable Transition.Segment<EnterExitState>.() -> FiniteAnimationSpec<Float> =
+ {
+ when {
+ EnterExitState.PreEnter isTransitioningTo EnterExitState.Visible -> {
+ activeEnter.data.fade?.animationSpec ?: DefaultAlphaAnimationSpec
+ }
+ EnterExitState.Visible isTransitioningTo EnterExitState.PostExit -> {
+ activeExit.data.fade?.animationSpec ?: DefaultAlphaAnimationSpec
+ }
+ else -> DefaultAlphaAnimationSpec
+ }
+ }
+ val animationLabel = remember { "$label alpha" }
+ val alpha by
+ animateFloat(alphaSpec, label = animationLabel) { targetState ->
+ when (targetState) {
+ EnterExitState.Visible -> 1f
+ EnterExitState.PreEnter -> activeEnter.data.fade?.alpha ?: 1f
+ EnterExitState.PostExit -> activeExit.data.fade?.alpha ?: 1f
+ }
+ }
+ return SubspaceModifier.alpha(alpha)
+}
+
+@Composable
+private fun Transition<EnterExitState>.slideAnimationModifier(
+ activeEnter: SpatialEnterTransition,
+ activeExit: SpatialExitTransition,
+): SubspaceModifier {
+ // No slide animations
+ if (activeEnter.data.slide == null && activeExit.data.slide == null) return SubspaceModifier
+
+ val offset = remember { Animatable(IntVolumeOffset.Zero, IntVolumeOffsetToVector) }
+
+ val animationSpec =
+ with(segment) {
+ when {
+ EnterExitState.PreEnter isTransitioningTo EnterExitState.Visible ->
+ activeEnter.data.slide?.animationSpec ?: DefaultSlideAnimationSpec
+ EnterExitState.Visible isTransitioningTo EnterExitState.PostExit ->
+ activeExit.data.slide?.animationSpec ?: DefaultSlideAnimationSpec
+ else -> DefaultSlideAnimationSpec
+ }
+ }
+
+ val density = LocalDensity.current
+ fun EnterExitState.calculateOffset(fullSize: IntVolumeSize): IntVolumeOffset {
+ return when (this) {
+ EnterExitState.PreEnter ->
+ activeEnter.data.slide?.slideOffset?.invoke(density, fullSize)
+ ?: IntVolumeOffset.Zero
+ EnterExitState.Visible -> IntVolumeOffset.Zero
+ EnterExitState.PostExit ->
+ activeExit.data.slide?.slideOffset?.invoke(density, fullSize)
+ ?: IntVolumeOffset.Zero
+ }
+ }
+
+ // This will be updated by the layout modifier below, and is necessary to compute offsets.
+ var fullSize: IntVolumeSize by remember { mutableStateOf(IntVolumeSize.Zero) }
+ var isInitialSnapToFinished by remember { mutableStateOf(false) }
+
+ // When the size, target state, or animationSpec changes, animate towards the new target offset.
+ LaunchedEffect(fullSize, targetState, animationSpec) {
+ // Ideally, the offset would just be initialized via fullSize, but fullSize is not known
+ // until layout time, so we need to defer this snapTo.
+ // We also don't want to snapTo more than once, as it will result in the animation
+ // restarting in odd ways if the developer changes the layout's size.
+ if (!isInitialSnapToFinished) {
+ isInitialSnapToFinished = true
+ offset.snapTo(currentState.calculateOffset(fullSize))
+ }
+ offset.animateTo(targetState.calculateOffset(fullSize), animationSpec)
+ }
+
+ return SubspaceModifier.layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ // Size can only be determined in a custom layout modifier
+ fullSize =
+ IntVolumeSize(
+ placeable.measuredWidth,
+ placeable.measuredHeight,
+ placeable.measuredDepth,
+ )
+ layout(placeable.measuredWidth, placeable.measuredHeight, placeable.measuredDepth) {
+ placeable.place(
+ Pose(
+ // Place this layout at the offset calculated by our animation
+ Vector3(
+ offset.value.x.toFloat(),
+ offset.value.y.toFloat(),
+ offset.value.z.toFloat(),
+ )
+ )
+ )
+ }
+ }
+}
+
+// This converts Boolean visible to EnterExitState
+@Composable
+private fun <T> Transition<T>.targetEnterExit(
+ visible: (T) -> Boolean,
+ targetState: T,
+): EnterExitState =
+ key(this) {
+ val hasBeenVisible = remember { mutableStateOf(false) }
+ if (visible(currentState)) {
+ hasBeenVisible.value = true
+ }
+ if (visible(targetState)) {
+ EnterExitState.Visible
+ } else {
+ // If never been visible, visible = false means PreEnter, otherwise PostExit
+ if (hasBeenVisible.value) {
+ EnterExitState.PostExit
+ } else {
+ EnterExitState.PreEnter
+ }
+ }
+ }
+
+private val Transition<EnterExitState>.exitFinished
+ get() = currentState == EnterExitState.PostExit && targetState == EnterExitState.PostExit
+
+private val IntVolumeOffsetToVector: TwoWayConverter<IntVolumeOffset, AnimationVector3D> =
+ TwoWayConverter(
+ { AnimationVector3D(it.x.toFloat(), it.y.toFloat(), it.z.toFloat()) },
+ { IntVolumeOffset(it.v1.fastRoundToInt(), it.v2.fastRoundToInt(), it.v3.fastRoundToInt()) },
+ )
+
+private class AnimatedSpatialVisibilityScopeImpl(
+ override val transition: Transition<EnterExitState>
+) : AnimatedSpatialVisibilityScope
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/SpatialTransition.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/SpatialTransition.kt
new file mode 100644
index 0000000..c080fcf
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/SpatialTransition.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2025 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.xr.compose.subspace.animation
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.unit.Density
+import androidx.xr.compose.unit.IntVolumeOffset
+import androidx.xr.compose.unit.IntVolumeSize
+
+/**
+ * [SpatialEnterTransition] defines how an [AnimatedSpatialVisibility] Composable appears on screen
+ * as it becomes visible. The categories of EnterTransitions available are:
+ * 1. fade: [SpatialTransitions.fadeIn]
+ * 2. slide: [SpatialTransitions.slideIn], [SpatialTransitions.slideInHorizontally],
+ * [SpatialTransitions.slideInVertically], [SpatialTransitions.slideInDepth]
+ *
+ * [SpatialEnterTransition.None] can be used when no enter transition is desired. Different
+ * [SpatialEnterTransition]s can be combined using [SpatialEnterTransition.plus].
+ *
+ * __Note__: [SpatialTransitions.fadeIn] and [SpatialTransitions.slideIn] do not affect the size of
+ * the [AnimatedVisibility] composable.
+ *
+ * @see EnterTransition
+ */
+public sealed class SpatialEnterTransition {
+ internal abstract val data: SpatialTransitionData
+
+ /**
+ * Combines different enter transitions. The order of the [SpatialEnterTransition]s being
+ * combined does not matter, as these [SpatialEnterTransition]s will start simultaneously. The
+ * order of applying transforms from these enter transitions (if defined) is: alpha and scale
+ * first, shrink or expand, then slide.
+ *
+ * @param enter another [SpatialEnterTransition] to be combined
+ */
+ @Stable
+ public operator fun plus(enter: SpatialEnterTransition): SpatialEnterTransition {
+ return Impl(data.merge(enter.data))
+ }
+
+ @Immutable
+ internal class Impl(override val data: SpatialTransitionData) : SpatialEnterTransition()
+
+ public companion object {
+ /**
+ * This can be used when no enter transition is desired.
+ *
+ * @see [SpatialExitTransition.None]
+ */
+ public val None: SpatialEnterTransition = Impl(SpatialTransitionData())
+ }
+}
+
+/**
+ * [SpatialExitTransition] defines how an [AnimatedSpatialVisibility] Composable disappears on
+ * screen as it becomes not visible. The categories of [SpatialExitTransition] available are:
+ * 1. fade: [SpatialTransitions.fadeOut]
+ * 2. slide: [SpatialTransitions.slideOut], [SpatialTransitions.slideOutHorizontally],
+ * [SpatialTransitions.slideOutVertically], [SpatialTransitions.slideOutDepth]
+ *
+ * [SpatialExitTransition.None] can be used when no exit transition is desired. Different
+ * [SpatialExitTransition]s can be combined using [SpatialExitTransition.plus].
+ *
+ * __Note__: [SpatialTransitions.fadeOut] and [SpatialTransitions.slideOut] do not affect the size
+ * of the [AnimatedSpatialVisibility] composable.
+ *
+ * @see ExitTransition
+ */
+public sealed class SpatialExitTransition {
+ internal abstract val data: SpatialTransitionData
+
+ /**
+ * Combines different exit transitions. The order of the [SpatialExitTransition]s being combined
+ * does not matter, as these [SpatialExitTransition]s will start simultaneously. The order of
+ * applying transforms from these exit transitions (if defined) is: alpha and scale first,
+ * shrink or expand, then slide.
+ *
+ * @param exit another [SpatialExitTransition] to be combined
+ */
+ @Stable
+ public operator fun plus(exit: SpatialExitTransition): SpatialExitTransition {
+ return Impl(data.merge(exit.data))
+ }
+
+ @Immutable
+ internal class Impl(override val data: SpatialTransitionData) : SpatialExitTransition()
+
+ public companion object {
+ /**
+ * This can be used when no enter transition is desired.
+ *
+ * @see [SpatialEnterTransition.None]
+ */
+ public val None: SpatialExitTransition = Impl(SpatialTransitionData())
+ }
+}
+
+/** ********************* Below are internal classes and methods ***************** */
+@Immutable
+internal data class Fade(val alpha: Float, val animationSpec: FiniteAnimationSpec<Float>)
+
+@Immutable
+internal data class Slide(
+ val slideOffset: Density.(fullSize: IntVolumeSize) -> IntVolumeOffset,
+ val animationSpec: FiniteAnimationSpec<IntVolumeOffset>,
+)
+
+@Immutable
+internal data class SpatialTransitionData(val fade: Fade? = null, val slide: Slide? = null)
+
+private fun SpatialTransitionData.merge(other: SpatialTransitionData): SpatialTransitionData {
+ return SpatialTransitionData(fade = other.fade ?: fade, slide = other.slide ?: slide)
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/SpatialTransitions.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/SpatialTransitions.kt
new file mode 100644
index 0000000..87a213a
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/animation/SpatialTransitions.kt
@@ -0,0 +1,328 @@
+/*
+ * Copyright 2025 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.xr.compose.subspace.animation
+
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
+import androidx.xr.compose.unit.IntVolumeOffset
+import androidx.xr.compose.unit.IntVolumeSize
+
+/** Public transition spec APIs for use with [AnimatedSpatialVisibility]. */
+public object SpatialTransitions {
+ /**
+ * This fades in the content of the transition, from the specified starting alpha (i.e.
+ * [initialAlpha]) to 1f, using the supplied [animationSpec]. [initialAlpha] defaults to 0f, and
+ * [spring] is used by default.
+ *
+ * @sample androidx.xr.compose.samples.animation.SpatialFade
+ * @param animationSpec the [FiniteAnimationSpec] for this animation, [spring] by default
+ * @param initialAlpha the starting alpha of the enter transition, 0f by default
+ */
+ @Stable
+ public fun fadeIn(
+ animationSpec: FiniteAnimationSpec<Float> =
+ SpatialTransitionDefaults.DefaultAlphaAnimationSpec,
+ initialAlpha: Float = 0f,
+ ): SpatialEnterTransition {
+ return SpatialEnterTransition.Impl(
+ SpatialTransitionData(fade = Fade(initialAlpha, animationSpec))
+ )
+ }
+
+ /**
+ * This fades out the content of the transition, from full opacity to the specified target alpha
+ * (i.e. [targetAlpha]), using the supplied [animationSpec]. By default, the content will be
+ * faded out to fully transparent (i.e. [targetAlpha] defaults to 0), and [animationSpec] uses
+ * [spring] by default.
+ *
+ * @sample androidx.xr.compose.samples.animation.SpatialFade
+ * @param animationSpec the [FiniteAnimationSpec] for this animation, [spring] by default
+ * @param targetAlpha the target alpha of the exit transition, 0f by default
+ */
+ @Stable
+ public fun fadeOut(
+ animationSpec: FiniteAnimationSpec<Float> =
+ SpatialTransitionDefaults.DefaultAlphaAnimationSpec,
+ targetAlpha: Float = 0f,
+ ): SpatialExitTransition {
+ return SpatialExitTransition.Impl(
+ SpatialTransitionData(fade = Fade(targetAlpha, animationSpec))
+ )
+ }
+
+ /**
+ * This slides in the content of the transition, from a starting offset defined in
+ * [initialOffset] to `IntVolumeOffset(0, 0, 0)`. The direction of the slide can be controlled
+ * by configuring the [initialOffset]. A positive x value means sliding from right to left,
+ * whereas a negative x value will slide the content to the right. Similarly positive and
+ * negative y values correspond to sliding up and down, respectively, and positive and negative
+ * z values correspond to sliding closer and further, respectively.
+ *
+ * If the sliding is only desired along one axis, consider using [slideInHorizontally],
+ * [slideInVertically], or [slideInDepth].
+ *
+ * [initialOffset] is a lambda that takes the full size of the content and returns an offset.
+ * This allows the offset to be defined proportional to the full size, or as an absolute value.
+ *
+ * @sample androidx.xr.compose.samples.animation.SpatialSlide
+ * @param animationSpec the animation used for the slide-in, [spring] by default.
+ * @param initialOffset a lambda that takes the full size of the content and returns the initial
+ * offset for the slide-in
+ */
+ @Stable
+ public fun slideIn(
+ animationSpec: FiniteAnimationSpec<IntVolumeOffset> =
+ SpatialTransitionDefaults.DefaultSlideAnimationSpec,
+ initialOffset: Density.(fullSize: IntVolumeSize) -> IntVolumeOffset,
+ ): SpatialEnterTransition {
+ return SpatialEnterTransition.Impl(
+ SpatialTransitionData(slide = Slide(initialOffset, animationSpec))
+ )
+ }
+
+ /**
+ * This slides in the content horizontally, from a starting offset defined in [initialOffsetX]
+ * to `0` **pixels**. The direction of the slide can be controlled by configuring the
+ * [initialOffsetX]. A positive value means sliding from right to left, whereas a negative value
+ * would slide the content from left to right.
+ *
+ * [initialOffsetX] is a lambda that takes the full width of the content and returns an offset.
+ * This allows the starting offset to be defined proportional to the full size, or as an
+ * absolute value. It defaults to return half of negative width, which would offset the content
+ * to the left by half of its width, and slide towards the right.
+ *
+ * @sample androidx.xr.compose.samples.animation.SpatialSlide
+ * @param animationSpec the animation used for the slide-in, [spring] by default.
+ * @param initialOffsetX a lambda that takes the full width of the content in pixels and returns
+ * the initial offset for the slide-in, by default it returns `-fullWidth/2`
+ */
+ @Stable
+ public fun slideInHorizontally(
+ animationSpec: FiniteAnimationSpec<IntVolumeOffset> =
+ SpatialTransitionDefaults.DefaultSlideAnimationSpec,
+ initialOffsetX: Density.(fullWidth: Int) -> Int = { -it / 2 },
+ ): SpatialEnterTransition {
+ return slideIn(
+ initialOffset = { IntVolumeOffset(x = initialOffsetX(it.width), y = 0, z = 0) },
+ animationSpec = animationSpec,
+ )
+ }
+
+ /**
+ * This slides in the content vertically, from a starting offset defined in [initialOffsetY] to
+ * `0` **pixels**. The direction of the slide can be controlled by configuring the
+ * [initialOffsetY]. A positive value means sliding from top to bottom, whereas a negative value
+ * would slide the content from bottom to top.
+ *
+ * [initialOffsetY] is a lambda that takes the full height of the content and returns an offset.
+ * This allows the starting offset to be defined proportional to the full size, or as an
+ * absolute value. It defaults to return half of negative height, which would offset the content
+ * down by half of its height, and slide upwards.
+ *
+ * @sample androidx.xr.compose.samples.animation.SpatialSlide
+ * @param animationSpec the animation used for the slide-in, [spring] by default.
+ * @param initialOffsetY a lambda that takes the full height of the content in pixels and
+ * returns the initial offset for the slide-in, by default it returns `-fullHeight/2`
+ */
+ @Stable
+ public fun slideInVertically(
+ animationSpec: FiniteAnimationSpec<IntVolumeOffset> =
+ SpatialTransitionDefaults.DefaultSlideAnimationSpec,
+ initialOffsetY: Density.(fullHeight: Int) -> Int = { -it / 2 },
+ ): SpatialEnterTransition {
+ return slideIn(
+ initialOffset = { IntVolumeOffset(x = 0, y = initialOffsetY(it.height), z = 0) },
+ animationSpec = animationSpec,
+ )
+ }
+
+ /**
+ * This slides in the content depthwise, from a starting offset defined in [initialOffsetZ] to
+ * `0` **pixels**. The direction of the slide can be controlled by configuring the
+ * [initialOffsetZ]. A positive value means sliding from close to far, whereas a negative value
+ * would slide the content from far to close.
+ *
+ * [initialOffsetZ] is a lambda that takes the full depth of the content and returns an offset.
+ * This allows the starting offset to be defined proportional to the full size, or as an
+ * absolute value.
+ *
+ * Unlike [slideInVertically] and [slideInHorizontally], this defaults to sliding in from `20dp`
+ * **away** from the neutral depth point. This is because many commonly-animated Spatial
+ * elements, such as `SpatialPanel`, report a depth of 0.
+ *
+ * @sample androidx.xr.compose.samples.animation.SpatialSlide
+ * @param animationSpec the animation used for the slide-in, [spring] by default.
+ * @param initialOffsetZ a lambda that takes the full height of the content in pixels and
+ * returns the initial offset for the slide-in, by default it returns `-20.dp.toPx()`
+ */
+ @Stable
+ public fun slideInDepth(
+ animationSpec: FiniteAnimationSpec<IntVolumeOffset> =
+ SpatialTransitionDefaults.DefaultSlideAnimationSpec,
+ initialOffsetZ: Density.(fullDepth: Int) -> Int = { -20.dp.roundToPx() },
+ ): SpatialEnterTransition {
+ return slideIn(
+ initialOffset = { IntVolumeOffset(x = 0, y = 0, z = initialOffsetZ(it.depth)) },
+ animationSpec = animationSpec,
+ )
+ }
+
+ /**
+ * This slides out the content of the transition, from an offset of `IntVolumeOffset(0, 0, 0)`
+ * to the target offset defined in [targetOffset]. The direction of the slide can be controlled
+ * by configuring the [targetOffset]. A positive x value means sliding from left to right,
+ * whereas a negative x value would slide the content from right to left. Similarly, positive
+ * and negative y values correspond to sliding down and up, respectively, and positive and
+ * negative z values correspond to sliding closer and further, respectively.
+ *
+ * If the sliding is only desired along one axis, consider using [slideOutHorizontally],
+ * [slideOutVertically], or [slideOutDepth].
+ *
+ * [targetOffset] is a lambda that takes the full size of the content and returns an offset.
+ * This allows the offset to be defined proportional to the full size, or as an absolute value.
+ *
+ * @sample androidx.xr.compose.samples.animation.SpatialSlide
+ * @param animationSpec the animation used for the slide-out, [spring] by default.
+ * @param targetOffset a lambda that takes the full size of the content and returns the target
+ * offset for the slide-out
+ */
+ @Stable
+ public fun slideOut(
+ animationSpec: FiniteAnimationSpec<IntVolumeOffset> =
+ SpatialTransitionDefaults.DefaultSlideAnimationSpec,
+ targetOffset: Density.(fullSize: IntVolumeSize) -> IntVolumeOffset,
+ ): SpatialExitTransition {
+ return SpatialExitTransition.Impl(
+ SpatialTransitionData(slide = Slide(targetOffset, animationSpec))
+ )
+ }
+
+ /**
+ * This slides out the content horizontally, from 0 to a target offset defined in
+ * [targetOffsetX] in **pixels**. The direction of the slide can be controlled by configuring
+ * the [targetOffsetX]. A positive value means sliding to the right, whereas a negative value
+ * would slide the content towards the left.
+ *
+ * [targetOffsetX] is a lambda that takes the full width of the content and returns an offset.
+ * This allows the target offset to be defined proportional to the full size, or as an absolute
+ * value. It defaults to return half of negative width, which would slide the content to the
+ * left by half of its width.
+ *
+ * @sample androidx.xr.compose.samples.animation.SpatialSlide
+ * @param animationSpec the animation used for the slide-out, [spring] by default.
+ * @param targetOffsetX a lambda that takes the full width of the content and returns the
+ * initial offset for the slide-in, by default it returns `fullWidth/2`
+ */
+ @Stable
+ public fun slideOutHorizontally(
+ animationSpec: FiniteAnimationSpec<IntVolumeOffset> =
+ SpatialTransitionDefaults.DefaultSlideAnimationSpec,
+ targetOffsetX: Density.(fullWidth: Int) -> Int = { -it / 2 },
+ ): SpatialExitTransition {
+ return slideOut(
+ targetOffset = { IntVolumeOffset(x = targetOffsetX(it.width), y = 0, z = 0) },
+ animationSpec = animationSpec,
+ )
+ }
+
+ /**
+ * This slides out the content vertically, from 0 to a target offset defined in [targetOffsetY]
+ * in **pixels**. The direction of the slide-out can be controlled by configuring the
+ * [targetOffsetY]. A positive target offset means sliding down, whereas a negative value would
+ * slide the content up.
+ *
+ * [targetOffsetY] is a lambda that takes the full Height of the content and returns an offset.
+ * This allows the target offset to be defined proportional to the full height, or as an
+ * absolute value. It defaults to return half of the negative height, which would slide the
+ * content up by half of its Height.
+ *
+ * @sample androidx.xr.compose.samples.animation.SpatialSlide
+ * @param animationSpec the animation used for the slide-out, [spring] by default.
+ * @param targetOffsetY a lambda that takes the full Height of the content and returns the
+ * target offset for the slide-out, by default it returns `fullHeight/2`
+ */
+ @Stable
+ public fun slideOutVertically(
+ animationSpec: FiniteAnimationSpec<IntVolumeOffset> =
+ SpatialTransitionDefaults.DefaultSlideAnimationSpec,
+ targetOffsetY: Density.(fullHeight: Int) -> Int = { -it / 2 },
+ ): SpatialExitTransition {
+ return slideOut(
+ targetOffset = { IntVolumeOffset(x = 0, y = targetOffsetY(it.height), z = 0) },
+ animationSpec = animationSpec,
+ )
+ }
+
+ /**
+ * This slides out the content depthwise, from 0 to a target offset defined in [targetOffsetZ]
+ * in **pixels**. The direction of the slide-out can be controlled by configuring the
+ * [targetOffsetZ]. A positive value means sliding from close to far, whereas a negative value
+ * would slide the content from far to close.
+ *
+ * [targetOffsetZ] is a lambda that takes the full depth of the content and returns an offset.
+ * This allows the target offset to be defined proportional to the full depth, or as an absolute
+ * value.
+ *
+ * Unlike [slideInVertically] and [slideInHorizontally], this defaults to sliding in from `20dp`
+ * **away** from the neutral depth point. This is because many commonly-animated Spatial
+ * elements, such as `SpatialPanel`, report a depth of 0.
+ *
+ * @sample androidx.xr.compose.samples.animation.SpatialSlide
+ * @param animationSpec the animation used for the slide-out, [spring] by default.
+ * @param targetOffsetZ a lambda that takes the full depth of the content and returns the target
+ * offset for the slide-out, by default it returns `-20.dp.toPx()`
+ */
+ @Stable
+ public fun slideOutDepth(
+ animationSpec: FiniteAnimationSpec<IntVolumeOffset> =
+ SpatialTransitionDefaults.DefaultSlideAnimationSpec,
+ targetOffsetZ: Density.(fullDepth: Int) -> Int = { -20.dp.roundToPx() },
+ ): SpatialExitTransition {
+ return slideOut(
+ targetOffset = { IntVolumeOffset(x = 0, y = 0, z = targetOffsetZ(it.depth)) },
+ animationSpec = animationSpec,
+ )
+ }
+}
+
+/** Defaults for use with [SpatialTransitions]. */
+public object SpatialTransitionDefaults {
+ /** The default [SpatialEnterTransition] used by [SpatialTransitions]. */
+ public val DefaultEnter: SpatialEnterTransition =
+ SpatialTransitions.fadeIn() + SpatialTransitions.slideInDepth()
+
+ /** The default [SpatialExitTransition] used by [SpatialTransitions]. */
+ public val DefaultExit: SpatialExitTransition =
+ SpatialTransitions.fadeOut() + SpatialTransitions.slideOutDepth()
+
+ internal val DefaultAlphaAnimationSpec: FiniteAnimationSpec<Float> =
+ spring(stiffness = Spring.StiffnessLow, dampingRatio = DEFAULT_SPATIAL_SPRING_DAMPING)
+
+ internal val DefaultSlideAnimationSpec: FiniteAnimationSpec<IntVolumeOffset> =
+ spring(
+ stiffness = Spring.StiffnessLow,
+ dampingRatio = DEFAULT_SPATIAL_SPRING_DAMPING,
+ visibilityThreshold = IntVolumeOffset(1, 1, 1),
+ )
+
+ /** Spatial Animations use a different damping value than 2D animations. */
+ private const val DEFAULT_SPATIAL_SPRING_DAMPING = 0.8f
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/LayoutModifier.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/LayoutModifier.kt
new file mode 100644
index 0000000..4b315de
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/LayoutModifier.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2025 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.xr.compose.subspace.layout
+
+import androidx.xr.compose.subspace.node.SubspaceLayoutModifierNode
+import androidx.xr.compose.subspace.node.SubspaceModifierNodeElement
+import androidx.xr.compose.unit.VolumeConstraints
+
+/** Creates a node that allows changing how the wrapped element is measured and laid out. */
+public fun SubspaceModifier.layout(
+ measure: SubspaceMeasureScope.(SubspaceMeasurable, VolumeConstraints) -> SubspaceMeasureResult
+): SubspaceModifier = this then LayoutElement(measure)
+
+private data class LayoutElement(
+ val measure:
+ SubspaceMeasureScope.(SubspaceMeasurable, VolumeConstraints) -> SubspaceMeasureResult
+) : SubspaceModifierNodeElement<SubspaceLayoutModifierImpl>() {
+ override fun create() = SubspaceLayoutModifierImpl(measure)
+
+ override fun update(node: SubspaceLayoutModifierImpl) {
+ node.measureBlock = measure
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as LayoutElement
+
+ return measure === other.measure
+ }
+
+ override fun hashCode(): Int {
+ return measure.hashCode()
+ }
+}
+
+private class SubspaceLayoutModifierImpl(
+ var measureBlock:
+ SubspaceMeasureScope.(SubspaceMeasurable, VolumeConstraints) -> SubspaceMeasureResult
+) : SubspaceLayoutModifierNode, SubspaceModifier.Node() {
+ override fun SubspaceMeasureScope.measure(
+ measurable: SubspaceMeasurable,
+ constraints: VolumeConstraints,
+ ): SubspaceMeasureResult = measureBlock(measurable, constraints)
+}
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/IntVolumeOffset.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/IntVolumeOffset.kt
new file mode 100644
index 0000000..2fc6de2
--- /dev/null
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/unit/IntVolumeOffset.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2025 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.xr.compose.unit
+
+import androidx.compose.runtime.Immutable
+
+/** Represents the offset of an object in 3D space. */
+@Immutable
+public class IntVolumeOffset(public val x: Int, public val y: Int, public val z: Int) {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as IntVolumeOffset
+
+ if (x != other.x) return false
+ if (y != other.y) return false
+ if (z != other.z) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = x
+ result = 31 * result + y
+ result = 31 * result + z
+ return result
+ }
+
+ override fun toString(): String {
+ return "IntVolumeOffset(x=$x, y=$y, z=$z)"
+ }
+
+ /** Contains a common constant */
+ public companion object {
+ /** A [IntVolumeOffset] with all offsets set to 0. */
+ public val Zero: IntVolumeOffset = IntVolumeOffset(0, 0, 0)
+ }
+}
diff --git a/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/DetailPane.kt b/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/DetailPane.kt
index 22a477f..af092dd 100644
--- a/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/DetailPane.kt
+++ b/xr/compose/material3/integration-tests/testapp/src/main/java/androidx/xr/compose/material3/integration/testapp/DetailPane.kt
@@ -37,7 +37,14 @@
@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3Api::class)
@Composable
internal fun DetailPane(navigator: ThreePaneScaffoldNavigator<Destination>) {
- val destination = navigator.currentDestination?.contentKey ?: return
+ val destination = navigator.currentDestination?.contentKey
+ if (destination == null) {
+ // Populate the pane with blank content so it can show as a "placeholder", as suggested in
+ // the guidelines:
+ // https://developer.android.com/develop/ui/compose/layouts/adaptive/canonical-layouts#list-detail
+ Surface(Modifier.fillMaxSize()) {}
+ return
+ }
val coroutineScope = rememberCoroutineScope()
Scaffold(
topBar = { MediumTopAppBar(title = { Text("XR Compose Adaptive: ${destination.label}") }) }
diff --git a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/Pane.kt b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/Pane.kt
index 8f902be..9d9d7d9 100644
--- a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/Pane.kt
+++ b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/Pane.kt
@@ -16,34 +16,22 @@
package androidx.xr.compose.material3
-import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi
import androidx.compose.material3.adaptive.layout.AnimatedPaneOverride
import androidx.compose.material3.adaptive.layout.AnimatedPaneOverrideScope
import androidx.compose.material3.adaptive.layout.AnimatedPaneScope
-import androidx.compose.material3.adaptive.layout.ExtendedPaneScaffoldPaneScope
+import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
import androidx.compose.material3.adaptive.layout.PaneScaffoldRole
import androidx.compose.material3.adaptive.layout.PaneScaffoldValue
+import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-
-@OptIn(
- ExperimentalMaterial3AdaptiveComponentOverrideApi::class,
- ExperimentalMaterial3AdaptiveApi::class,
-)
-@Composable
-private fun <S : PaneScaffoldRole, T : PaneScaffoldValue<S>> Pane(
- scope: ExtendedPaneScaffoldPaneScope<S, T>,
- modifier: Modifier,
- content: @Composable (AnimatedPaneScope.() -> Unit),
-) {
- with(scope) {
- scaffoldStateTransition.AnimatedVisibility(visible = { true }, modifier = modifier) {
- AnimatedPaneScope.create(this).content()
- }
- }
-}
+import androidx.xr.compose.subspace.SpatialPanel
+import androidx.xr.compose.subspace.animation.AnimatedSpatialVisibility
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.subspace.layout.fillMaxHeight
+import androidx.xr.compose.subspace.layout.width
@OptIn(
ExperimentalMaterial3AdaptiveComponentOverrideApi::class,
@@ -52,8 +40,29 @@
@ExperimentalMaterial3XrApi
internal object XrAnimatedPaneOverride : AnimatedPaneOverride {
@Composable
- override fun <S : PaneScaffoldRole, T : PaneScaffoldValue<S>> AnimatedPaneOverrideScope<S, T>
- .AnimatedPane() {
- Pane(scope, modifier, content)
+ override fun <
+ Role : PaneScaffoldRole,
+ ScaffoldValue : PaneScaffoldValue<Role>,
+ > AnimatedPaneOverrideScope<Role, ScaffoldValue>.AnimatedPane() {
+ // TODO(kmost): No way to convert between Enter/ExitTransition and
+ // SpatialEnter/ExitTransition, so for now we cannot respect those scope properties.
+
+ val state = MutableTransitionState(false)
+ state.targetState =
+ scope.scaffoldStateTransition.targetState[scope.paneRole] != PaneAdaptedValue.Hidden
+
+ AnimatedSpatialVisibility(visibleState = state) {
+ val width =
+ when (scope.paneRole as ThreePaneScaffoldRole) {
+ ThreePaneScaffoldRole.Primary -> XrThreePaneScaffoldTokens.PrimaryPanePanelWidth
+ ThreePaneScaffoldRole.Secondary ->
+ XrThreePaneScaffoldTokens.SecondaryPanePanelWidth
+ ThreePaneScaffoldRole.Tertiary ->
+ XrThreePaneScaffoldTokens.TertiaryPanePanelWidth
+ }
+ SpatialPanel(SubspaceModifier.width(width).fillMaxHeight()) {
+ AnimatedPaneScope.create(this).content()
+ }
+ }
}
}
diff --git a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/ThreePaneScaffold.kt b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/ThreePaneScaffold.kt
index cdb2632..eaf19e5 100644
--- a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/ThreePaneScaffold.kt
+++ b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/ThreePaneScaffold.kt
@@ -19,28 +19,21 @@
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi
import androidx.compose.material3.adaptive.layout.PaneScaffoldDirective
-import androidx.compose.material3.adaptive.layout.PaneScaffoldParentData
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldHorizontalOrder
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldOverride
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldOverrideScope
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.xr.compose.material3.XrThreePaneScaffoldOverride.ThreePaneScaffold
import androidx.xr.compose.spatial.Subspace
import androidx.xr.compose.subspace.SpatialLayoutSpacer
-import androidx.xr.compose.subspace.SpatialPanel
import androidx.xr.compose.subspace.SpatialRow
+import androidx.xr.compose.subspace.SubspaceComposable
import androidx.xr.compose.subspace.layout.SubspaceModifier
-import androidx.xr.compose.subspace.layout.fillMaxHeight
import androidx.xr.compose.subspace.layout.height
import androidx.xr.compose.subspace.layout.offset
import androidx.xr.compose.subspace.layout.width
-import kotlin.math.roundToInt
/**
* A pane scaffold composable that can display up to three panes in the order that
@@ -73,40 +66,28 @@
) {
Subspace {
SpatialRow(
- // Offset by 1dp as a workaround to fix b/395685251, where elements in the XR-overrides
- // ThreePaneScaffold are not clickable when composed from within the XR-overrides
- // NavigationSuiteScaffold.
- modifier = modifier.offset(z = 1.dp).height(XrThreePaneScaffoldTokens.PanelHeight)
+ modifier =
+ modifier
+ // Offset by 1dp as a workaround to fix b/395685251, where elements in the
+ // XR-overrides ThreePaneScaffold are not clickable when composed from within
+ // the XR-overrides NavigationSuiteScaffold.
+ .offset(z = 1.dp)
+ .height(XrThreePaneScaffoldTokens.PanelHeight)
) {
var drawSpacer = false // Only draws spacers after the first pane is drawn
paneOrder.each { role ->
when (role) {
ThreePaneScaffoldRole.Primary -> {
- Panel(
- scaffoldDirective,
- XrThreePaneScaffoldTokens.PrimaryPanePanelWidth,
- drawSpacer,
- primaryPane,
- )
+ Panel(scaffoldDirective, drawSpacer, primaryPane)
drawSpacer = true
}
ThreePaneScaffoldRole.Secondary -> {
- Panel(
- scaffoldDirective,
- XrThreePaneScaffoldTokens.SecondaryPanePanelWidth,
- drawSpacer,
- secondaryPane,
- )
+ Panel(scaffoldDirective, drawSpacer, secondaryPane)
drawSpacer = true
}
ThreePaneScaffoldRole.Tertiary ->
if (tertiaryPane != null) {
- Panel(
- scaffoldDirective,
- XrThreePaneScaffoldTokens.TertiaryPanePanelWidth,
- drawSpacer,
- tertiaryPane,
- )
+ Panel(scaffoldDirective, drawSpacer, tertiaryPane)
drawSpacer = true
}
}
@@ -117,34 +98,17 @@
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
+@SubspaceComposable
private fun Panel(
scaffoldDirective: PaneScaffoldDirective,
- defaultPreferredWidth: Dp,
drawSpacer: Boolean,
- content: @Composable () -> Unit,
+ content: @Composable @SubspaceComposable () -> Unit,
) {
if (drawSpacer) {
SpatialLayoutSpacer(SubspaceModifier.width(scaffoldDirective.horizontalPartitionSpacerSize))
}
- SpatialPanel(SubspaceModifier.width(defaultPreferredWidth).fillMaxHeight()) {
- Layout(content) { measurables, constraints ->
- val measurable = measurables.getOrNull(0)
- if (measurable == null) {
- return@Layout layout(
- defaultPreferredWidth.toPx().roundToInt(),
- constraints.maxHeight,
- ) {}
- }
- val parentData = measurable.parentData as? PaneScaffoldParentData
- val widthFloat = parentData?.preferredWidth ?: defaultPreferredWidth
- val width = widthFloat.toPx().roundToInt()
- val height = constraints.maxHeight
- return@Layout layout(width, height) {
- measurable.measure(Constraints.fixed(width, height)).place(0, 0)
- }
- }
- }
+ content()
}
/**
@@ -172,7 +136,7 @@
}
// TODO(conradchen): Confirm the values with design
-private object XrThreePaneScaffoldTokens {
+internal object XrThreePaneScaffoldTokens {
val PanelHeight = 1024.dp
val PrimaryPanePanelWidth = 800.dp
val SecondaryPanePanelWidth = 412.dp