From b1b4ef6bc15aa71b2a01196f147d5ccd6d9e86fc Mon Sep 17 00:00:00 2001 From: Dereck Bridie Date: Wed, 16 Apr 2025 18:11:44 +0200 Subject: [PATCH] Port compose snippets from DAC --- xr/build.gradle.kts | 32 ++++ .../java/com/example/xr/compose/Orbiter.kt | 138 ++++++++++++++++++ .../com/example/xr/compose/SpatialDialog.kt | 63 ++++++++ .../com/example/xr/compose/SpatialLayout.kt | 94 ++++++++++++ .../com/example/xr/compose/SpatialPanel.kt | 72 +++++++++ .../java/com/example/xr/compose/Subspace.kt | 65 +++++++++ .../java/com/example/xr/compose/Volume.kt | 89 +++++++++++ 7 files changed, 553 insertions(+) create mode 100644 xr/src/main/java/com/example/xr/compose/Orbiter.kt create mode 100644 xr/src/main/java/com/example/xr/compose/SpatialDialog.kt create mode 100644 xr/src/main/java/com/example/xr/compose/SpatialLayout.kt create mode 100644 xr/src/main/java/com/example/xr/compose/SpatialPanel.kt create mode 100644 xr/src/main/java/com/example/xr/compose/Subspace.kt create mode 100644 xr/src/main/java/com/example/xr/compose/Volume.kt diff --git a/xr/build.gradle.kts b/xr/build.gradle.kts index afbff015..3e7575f1 100644 --- a/xr/build.gradle.kts +++ b/xr/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) + alias(libs.plugins.compose.compiler) } android { @@ -21,14 +22,45 @@ android { kotlinOptions { jvmTarget = "11" } + buildFeatures { + compose = true + } } dependencies { implementation(libs.androidx.xr.arcore) implementation(libs.androidx.xr.scenecore) implementation(libs.androidx.xr.compose) + implementation(libs.androidx.activity.ktx) implementation(libs.guava) implementation(libs.kotlinx.coroutines.guava) + val composeBom = platform(libs.androidx.compose.bom) + implementation(composeBom) + + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.util) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.graphics.shapes) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.ui.viewbinding) + implementation(libs.androidx.paging.compose) + implementation(libs.androidx.compose.animation.graphics) + + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material3.adaptive) + implementation(libs.androidx.compose.material3.adaptive.layout) + implementation(libs.androidx.compose.material3.adaptive.navigation) + implementation(libs.androidx.compose.material3.adaptive.navigation.suite) + implementation(libs.androidx.compose.material) + + implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.compose.runtime.livedata) + implementation(libs.androidx.compose.material.iconsExtended) + implementation(libs.androidx.compose.material.ripple) + implementation(libs.androidx.constraintlayout.compose) + + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.appcompat) } \ No newline at end of file diff --git a/xr/src/main/java/com/example/xr/compose/Orbiter.kt b/xr/src/main/java/com/example/xr/compose/Orbiter.kt new file mode 100644 index 00000000..7d8df270 --- /dev/null +++ b/xr/src/main/java/com/example/xr/compose/Orbiter.kt @@ -0,0 +1,138 @@ +/* + * 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 + * + * https://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 com.example.xr.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.xr.compose.spatial.EdgeOffset +import androidx.xr.compose.spatial.Orbiter +import androidx.xr.compose.spatial.OrbiterEdge +import androidx.xr.compose.spatial.Subspace +import androidx.xr.compose.subspace.SpatialPanel +import androidx.xr.compose.subspace.SpatialRow +import androidx.xr.compose.subspace.layout.SpatialRoundedCornerShape +import androidx.xr.compose.subspace.layout.SubspaceModifier +import androidx.xr.compose.subspace.layout.height +import androidx.xr.compose.subspace.layout.movable +import androidx.xr.compose.subspace.layout.resizable +import androidx.xr.compose.subspace.layout.width + +@Composable +private fun OrbiterExampleSubspace() { + // [START androidxr_compose_OrbiterExampleSubspace] + Subspace { + SpatialPanel( + SubspaceModifier + .height(824.dp) + .width(1400.dp) + .movable() + .resizable() + ) { + SpatialPanelContent() + OrbiterExample() + } + } + // [END androidxr_compose_OrbiterExampleSubspace] +} + +// [START androidxr_compose_OrbiterExample] +@Composable +fun OrbiterExample() { + Orbiter( + position = OrbiterEdge.Bottom, + offset = 96.dp, + alignment = Alignment.CenterHorizontally + ) { + Surface(Modifier.clip(CircleShape)) { + Row( + Modifier + .background(color = Color.Black) + .height(100.dp) + .width(600.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Orbiter", + color = Color.White, + fontSize = 50.sp + ) + } + } + } +} +// [END androidxr_compose_OrbiterExample] + +@Composable +fun OrbiterAnchoringExample() { + // [START androidxr_compose_OrbiterAnchoringExample] + Subspace { + SpatialRow { + Orbiter( + position = OrbiterEdge.Top, + offset = EdgeOffset.inner(8.dp), + shape = SpatialRoundedCornerShape(size = CornerSize(50)) + ) { + Text( + "Hello World!", + style = MaterialTheme.typography.h2, + modifier = Modifier + .background(Color.White) + .padding(16.dp) + ) + } + SpatialPanel( + SubspaceModifier + .height(824.dp) + .width(1400.dp) + ) { + Box( + modifier = Modifier + .background(Color.Red) + ) + } + SpatialPanel( + SubspaceModifier + .height(824.dp) + .width(1400.dp) + ) { + Box( + modifier = Modifier + .background(Color.Blue) + ) + } + } + } + // [END androidxr_compose_OrbiterAnchoringExample] +} diff --git a/xr/src/main/java/com/example/xr/compose/SpatialDialog.kt b/xr/src/main/java/com/example/xr/compose/SpatialDialog.kt new file mode 100644 index 00000000..9b29bfd7 --- /dev/null +++ b/xr/src/main/java/com/example/xr/compose/SpatialDialog.kt @@ -0,0 +1,63 @@ +/* + * 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 + * + * https://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 com.example.xr.compose + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.unit.dp +import androidx.xr.compose.spatial.SpatialDialog +import androidx.xr.compose.spatial.SpatialDialogProperties +import kotlinx.coroutines.delay + +// [START androidxr_compose_DelayedDialog] +@Composable +fun DelayedDialog() { + var showDialog by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + delay(3000) + showDialog = true + } + if (showDialog) { + SpatialDialog( + onDismissRequest = { showDialog = false }, + SpatialDialogProperties( + dismissOnBackPress = true + ) + ) { + Box( + Modifier + .height(150.dp) + .width(150.dp) + ) { + Button(onClick = { showDialog = false }) { + Text("OK") + } + } + } + } +} +// [END androidxr_compose_DelayedDialog] diff --git a/xr/src/main/java/com/example/xr/compose/SpatialLayout.kt b/xr/src/main/java/com/example/xr/compose/SpatialLayout.kt new file mode 100644 index 00000000..b0099b00 --- /dev/null +++ b/xr/src/main/java/com/example/xr/compose/SpatialLayout.kt @@ -0,0 +1,94 @@ +/* + * 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 + * + * https://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 com.example.xr.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.xr.compose.spatial.Subspace +import androidx.xr.compose.subspace.SpatialColumn +import androidx.xr.compose.subspace.SpatialPanel +import androidx.xr.compose.subspace.SpatialRow +import androidx.xr.compose.subspace.layout.SubspaceModifier +import androidx.xr.compose.subspace.layout.height +import androidx.xr.compose.subspace.layout.width + +@Composable +private fun SpatialLayoutExampleSubspace() { + // [START androidxr_compose_SpatialLayoutExampleSubspace] + Subspace { + SpatialRow { + SpatialColumn { + SpatialPanel(SubspaceModifier.height(250.dp).width(400.dp)) { + SpatialPanelContent("Top Left") + } + SpatialPanel(SubspaceModifier.height(200.dp).width(400.dp)) { + SpatialPanelContent("Middle Left") + } + SpatialPanel(SubspaceModifier.height(250.dp).width(400.dp)) { + SpatialPanelContent("Bottom Left") + } + } + SpatialColumn { + SpatialPanel(SubspaceModifier.height(250.dp).width(400.dp)) { + SpatialPanelContent("Top Right") + } + SpatialPanel(SubspaceModifier.height(200.dp).width(400.dp)) { + SpatialPanelContent("Middle Right") + } + SpatialPanel(SubspaceModifier.height(250.dp).width(400.dp)) { + SpatialPanelContent("Bottom Right") + } + } + } + } + // [END androidxr_compose_SpatialLayoutExampleSubspace] +} + +// [START androidxr_compose_SpatialLayoutExampleSpatialPanelContent] +@Composable +fun SpatialPanelContent(text: String) { + Column( + Modifier + .background(color = Color.Black) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Panel", + color = Color.White, + fontSize = 15.sp + ) + Text( + text = text, + color = Color.White, + fontSize = 25.sp, + fontWeight = FontWeight.Bold + ) + } +} +// [END androidxr_compose_SpatialLayoutExampleSpatialPanelContent] diff --git a/xr/src/main/java/com/example/xr/compose/SpatialPanel.kt b/xr/src/main/java/com/example/xr/compose/SpatialPanel.kt new file mode 100644 index 00000000..5e117b02 --- /dev/null +++ b/xr/src/main/java/com/example/xr/compose/SpatialPanel.kt @@ -0,0 +1,72 @@ +/* + * 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 + * + * https://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 com.example.xr.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.xr.compose.spatial.Subspace +import androidx.xr.compose.subspace.SpatialPanel +import androidx.xr.compose.subspace.layout.SubspaceModifier +import androidx.xr.compose.subspace.layout.height +import androidx.xr.compose.subspace.layout.movable +import androidx.xr.compose.subspace.layout.resizable +import androidx.xr.compose.subspace.layout.width + +@Composable +private fun SpatialPanelExample() { + // [START androidxr_compose_SpatialPanel] + Subspace { + SpatialPanel( + SubspaceModifier + .height(824.dp) + .width(1400.dp) + .movable() + .resizable() + ) { + SpatialPanelContent() + } + } + // [END androidxr_compose_SpatialPanel] +} + +// [START androidxr_compose_SpatialPanelContent] +@Composable +fun SpatialPanelContent() { + Box( + Modifier + .background(color = Color.Black) + .height(500.dp) + .width(500.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = "Spatial Panel", + color = Color.White, + fontSize = 25.sp + ) + } +} +// [END androidxr_compose_SpatialPanelContent] diff --git a/xr/src/main/java/com/example/xr/compose/Subspace.kt b/xr/src/main/java/com/example/xr/compose/Subspace.kt new file mode 100644 index 00000000..75579968 --- /dev/null +++ b/xr/src/main/java/com/example/xr/compose/Subspace.kt @@ -0,0 +1,65 @@ +/* + * 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 + * + * https://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 com.example.xr.compose + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.Composable +import androidx.xr.compose.spatial.Subspace +import androidx.xr.compose.subspace.SpatialPanel + +private class SubspaceActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + // [START androidxr_compose_SubspaceSetContent] + setContent { + // This is a top-level subspace + Subspace { + SpatialPanel { + MyComposable() + } + } + } + // [END androidxr_compose_SubspaceSetContent] + } +} + +// [START androidxr_compose_SubspaceComponents] +@Composable +private fun MyComposable() { + Row { + PrimaryPane() + SecondaryPane() + } +} + +@Composable +private fun PrimaryPane() { + // This is a nested subspace, because PrimaryPane is in a SpatialPanel + // and that SpatialPanel is in a top-level Subspace + Subspace { + ObjectInAVolume(true) + } +} +// [END androidxr_compose_SubspaceComponents] + +@Composable +private fun SecondaryPane() {} diff --git a/xr/src/main/java/com/example/xr/compose/Volume.kt b/xr/src/main/java/com/example/xr/compose/Volume.kt new file mode 100644 index 00000000..83073c22 --- /dev/null +++ b/xr/src/main/java/com/example/xr/compose/Volume.kt @@ -0,0 +1,89 @@ +/* + * 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 + * + * https://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 com.example.xr.compose + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.xr.compose.platform.LocalSession +import androidx.xr.compose.spatial.Subspace +import androidx.xr.compose.subspace.SpatialPanel +import androidx.xr.compose.subspace.Volume +import androidx.xr.compose.subspace.layout.SubspaceModifier +import androidx.xr.compose.subspace.layout.height +import androidx.xr.compose.subspace.layout.movable +import androidx.xr.compose.subspace.layout.offset +import androidx.xr.compose.subspace.layout.resizable +import androidx.xr.compose.subspace.layout.scale +import androidx.xr.compose.subspace.layout.width +import kotlinx.coroutines.launch + +@Composable +private fun VolumeExample() { + // [START androidxr_compose_Volume] + Subspace { + SpatialPanel( + SubspaceModifier.height(1500.dp).width(1500.dp) + .resizable().movable() + ) { + ObjectInAVolume(true) + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Welcome", + fontSize = 50.sp, + ) + } + } + } + // [END androidxr_compose_Volume] +} + +// [START androidxr_compose_ObjectInAVolume] +@Composable +fun ObjectInAVolume(show3DObject: Boolean) { + // [START_EXCLUDE silent] + val volumeXOffset = 0.dp + val volumeYOffset = 0.dp + val volumeZOffset = 0.dp + // [END_EXCLUDE silent] + val session = checkNotNull(LocalSession.current) + val scope = rememberCoroutineScope() + if (show3DObject) { + Subspace { + Volume( + modifier = SubspaceModifier + .offset(volumeXOffset, volumeYOffset, volumeZOffset) // Relative position + .scale(1.2f) // Scale to 120% of the size + + ) { parent -> + scope.launch { + // Load your 3D model here + } + } + } + } +} +// [END androidxr_compose_ObjectInAVolume]