Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import com.example.helloandroidxr.ui.HelloAndroidXRApp
import com.example.helloandroidxr.ui.theme.HelloAndroidXRTheme

class MainActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2024 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.helloandroidxr.bugdroid

import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.xr.runtime.Session
import androidx.xr.scenecore.GltfModel
import com.example.helloandroidxr.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.io.InputStream

class BugdroidController(
private val xrSession: Session?,
private val context: Context,
private val coroutineScope: CoroutineScope
) {
var gltfModel by mutableStateOf<GltfModel?>(null)

init {
loadBugdroidModel()
}

private fun loadBugdroidModel() {
coroutineScope.launch {
gltfModel = BugdroidGltfModelCache.getOrLoadModel(xrSession, context)
}
}
}

private object BugdroidGltfModelCache {
private var cachedModel: GltfModel? = null
@SuppressLint("RestrictedApi")
suspend fun getOrLoadModel(
xrCoreSession: Session?, context: Context
): GltfModel? {
xrCoreSession ?: run {
Log.w(TAG, "Cannot load model, session is null.")
return null
}
return if (cachedModel == null) {
try {
val inputStream: InputStream =
context.resources.openRawResource(R.raw.bugdroid_animated_wave)
cachedModel = GltfModel.create(
xrCoreSession, inputStream.readBytes(), "BUGDROID"
)
cachedModel
} catch (e: Exception) {
Log.e(TAG, "Error loading GLTF model", e)
null
}
} else {
cachedModel
}
}

fun clearCache() {
cachedModel = null
}

const val TAG = "BugdroidGltfModelCache"
}
208 changes: 167 additions & 41 deletions app/src/main/java/com/example/helloandroidxr/ui/HelloAndroidXRApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,9 @@ import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
Expand All @@ -59,9 +57,12 @@ import androidx.compose.ui.unit.dp
import androidx.window.core.layout.WindowSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
import androidx.xr.compose.platform.LocalSpatialCapabilities
import androidx.xr.compose.platform.LocalSpatialConfiguration
import androidx.xr.compose.spatial.ContentEdge
import androidx.xr.compose.spatial.Orbiter
import androidx.xr.compose.spatial.Subspace
import androidx.xr.compose.subspace.MovePolicy
import androidx.xr.compose.subspace.ResizePolicy
import androidx.xr.compose.subspace.SpatialColumn
import androidx.xr.compose.subspace.SpatialPanel
import androidx.xr.compose.subspace.SpatialRow
Expand All @@ -70,34 +71,87 @@ import androidx.xr.compose.subspace.layout.alpha
import androidx.xr.compose.subspace.layout.fillMaxSize
import androidx.xr.compose.subspace.layout.fillMaxWidth
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.padding
import androidx.xr.compose.subspace.layout.resizable
import androidx.xr.compose.subspace.layout.rotate
import androidx.xr.compose.subspace.layout.size
import androidx.xr.compose.subspace.layout.width
import androidx.xr.runtime.math.Quaternion
import com.example.helloandroidxr.R
import com.example.helloandroidxr.ui.components.BugdroidControls
import com.example.helloandroidxr.ui.components.BugdroidModel
import com.example.helloandroidxr.ui.components.BugdroidSliderControls
import com.example.helloandroidxr.ui.components.EnvironmentControls
import com.example.helloandroidxr.ui.components.SearchBar
import com.example.helloandroidxr.ui.components.TextPane
import com.example.helloandroidxr.ui.theme.HelloAndroidXRTheme
import com.example.helloandroidxr.viewmodel.BugdroidUiState
import com.example.helloandroidxr.viewmodel.BugdroidViewModel
import com.example.helloandroidxr.viewmodel.ModelMaterialColor
import com.example.helloandroidxr.viewmodel.ModelMaterialProperties
import com.example.helloandroidxr.viewmodel.ModelOffset
import com.example.helloandroidxr.viewmodel.ModelRotation
import com.example.helloandroidxr.viewmodel.SliderGroup
import kotlinx.coroutines.launch

@Composable
fun HelloAndroidXRApp() {
val viewModel = BugdroidViewModel()
val uiState by viewModel.uiState.collectAsState()
if (LocalSpatialCapabilities.current.isSpatialUiEnabled) {
SpatialLayout(
primaryContent = { PrimaryContent() },
firstSupportingContent = { BlockOfContentOne() },
secondSupportingContent = { BlockOfContentTwo() }
primaryContent = {
PrimaryContent(
uiState = uiState,
onShowBugdroidToggle = viewModel::updateShowBugdroid,
onAnimateBugdroidToggle = viewModel::updateAnimateBugdroid
)
},
firstSupportingContent = {
BlockOfContentOne(
showBugdroid = uiState.showBugdroid,
onSliderGroupSelected = viewModel::updateShownSliderGroup,
onResetModel = viewModel::resetModel
)
},
secondSupportingContent = {
BlockOfContentTwo(
uiState = uiState,
showBugdroid = uiState.showBugdroid,
onScaleChange = viewModel::updateScale,
onRotationChange = viewModel::updateRotation,
onOffsetChange = viewModel::updateOffset,
onMaterialColorChange = viewModel::updateMaterialColor,
onMaterialPropertiesChange = viewModel::updateMaterialProperties
)
}
)
} else {
NonSpatialTwoPaneLayout(
secondaryPane = {
BlockOfContentOne()
BlockOfContentTwo()
BlockOfContentOne(
modifier = Modifier.height(240.dp),
showBugdroid = uiState.showBugdroid,
onSliderGroupSelected = viewModel::updateShownSliderGroup,
onResetModel = viewModel::resetModel
)
BlockOfContentTwo(
uiState = uiState,
showBugdroid = uiState.showBugdroid,
onScaleChange = viewModel::updateScale,
onRotationChange = viewModel::updateRotation,
onOffsetChange = viewModel::updateOffset,
onMaterialColorChange = viewModel::updateMaterialColor,
onMaterialPropertiesChange = viewModel::updateMaterialProperties
)
},
primaryPane = { PrimaryContent() }
primaryPane = {
PrimaryContent(
uiState = uiState,
onShowBugdroidToggle = viewModel::updateShowBugdroid,
onAnimateBugdroidToggle = viewModel::updateAnimateBugdroid
)
}
)
}
}
Expand Down Expand Up @@ -127,18 +181,18 @@ private fun SpatialLayout(
SubspaceModifier
.alpha(animatedAlpha.value)
.size(400.dp)
.padding(bottom = 16.dp)
.movable()
.resizable()
.padding(bottom = 16.dp),
dragPolicy = MovePolicy(isEnabled = true),
resizePolicy = ResizePolicy(isEnabled = true)
) {
firstSupportingContent()
}
SpatialPanel(
SubspaceModifier
.alpha(animatedAlpha.value)
.weight(1f)
.movable()
.resizable()
.weight(1f),
dragPolicy = MovePolicy(isEnabled = true),
resizePolicy = ResizePolicy(isEnabled = true)
) {
secondSupportingContent()
}
Expand All @@ -147,9 +201,9 @@ private fun SpatialLayout(
modifier = SubspaceModifier
.alpha(animatedAlpha.value)
.fillMaxSize()
.padding(left = 16.dp)
.movable()
.resizable()
.padding(left = 16.dp),
dragPolicy = MovePolicy(isEnabled = true),
resizePolicy = ResizePolicy(isEnabled = true)
) {
Column {
TopAppBar()
Expand Down Expand Up @@ -279,29 +333,63 @@ private fun TopAppBar() {
}

@Composable
private fun PrimaryContent(modifier: Modifier = Modifier) {
var showBugdroid by rememberSaveable { mutableStateOf(false) }
val stringResId = if (showBugdroid) R.string.hide_bugdroid else R.string.show_bugdroid

private fun PrimaryContent(
uiState: BugdroidUiState,
onShowBugdroidToggle: () -> Unit,
onAnimateBugdroidToggle: () -> Unit,
modifier: Modifier = Modifier,
) {
if (LocalSpatialCapabilities.current.isSpatialUiEnabled) {
val showStringResId =
if (uiState.showBugdroid) R.string.hide_bugdroid else R.string.show_bugdroid
val animateStringResId =
if (uiState.animateBugdroid) R.string.stop_animation_bugdroid else R.string.animate_bugdroid
val modelTransform = uiState.modelTransform
Surface(modifier.fillMaxSize()) {
Box(modifier.padding(48.dp), contentAlignment = Alignment.Center) {
Button(
onClick = {
showBugdroid = !showBugdroid
},
modifier = modifier
) {
Text(
text = stringResource(id = stringResId),
style = MaterialTheme.typography.labelLarge
)
Column(modifier.padding(48.dp), horizontalAlignment = Alignment.CenterHorizontally) {
Box(modifier.padding(48.dp), contentAlignment = Alignment.Center) {
Button(
onClick = onShowBugdroidToggle,
modifier = modifier
) {
Text(
text = stringResource(id = showStringResId),
style = MaterialTheme.typography.labelLarge
)
}
}
Box(modifier.padding(48.dp), contentAlignment = Alignment.Center) {
if (uiState.showBugdroid) {
Button(
onClick = onAnimateBugdroidToggle,
modifier = modifier
) {
Text(
text = stringResource(id = animateStringResId),
style = MaterialTheme.typography.labelLarge
)
}
}
}
BugdroidModel(
showBugdroid = showBugdroid,
modelTransform = modelTransform,
showBugdroid = uiState.showBugdroid,
animateBugdroid = uiState.animateBugdroid,
modifier = SubspaceModifier
.fillMaxSize()
.offset(z = 400.dp) // Relative position from the panel
.rotate(
Quaternion(
x = modelTransform.rotation.x,
y = modelTransform.rotation.y,
z = modelTransform.rotation.z,
w = modelTransform.rotation.w
)
)
.offset(
x = modelTransform.offset.x.dp,
y = modelTransform.offset.y.dp,
z = modelTransform.offset.z.dp // Relative position from the panel
)
)
}
}
Expand All @@ -314,13 +402,51 @@ private fun PrimaryContent(modifier: Modifier = Modifier) {
}

@Composable
private fun BlockOfContentOne(modifier: Modifier = Modifier) {
TextPane(stringResource(R.string.block_of_content_1), modifier = modifier.height(240.dp))
private fun BlockOfContentOne(
modifier: Modifier = Modifier,
showBugdroid: Boolean,
onSliderGroupSelected: (SliderGroup) -> Unit,
onResetModel: () -> Unit
) {
if (LocalSpatialConfiguration.current.hasXrSpatialFeature && showBugdroid) {
BugdroidControls(
onSliderGroupSelected = onSliderGroupSelected,
onResetModel = {
onResetModel()
onSliderGroupSelected(SliderGroup.NONE)
},
modifier = modifier
)
} else {
TextPane(stringResource(R.string.block_of_content_1), modifier = modifier.fillMaxHeight())
}
}

@Composable
private fun BlockOfContentTwo(modifier: Modifier = Modifier) {
TextPane(stringResource(R.string.block_of_content_2), modifier = modifier.fillMaxHeight())
private fun BlockOfContentTwo(
modifier: Modifier = Modifier,
uiState: BugdroidUiState,
showBugdroid: Boolean,
onScaleChange: (Float) -> Unit,
onRotationChange: (ModelRotation) -> Unit,
onOffsetChange: (ModelOffset) -> Unit,
onMaterialColorChange: (ModelMaterialColor) -> Unit,
onMaterialPropertiesChange: (ModelMaterialProperties) -> Unit,
) {
if (LocalSpatialConfiguration.current.hasXrSpatialFeature && showBugdroid) {
BugdroidSliderControls(
visibleSliderGroup = uiState.visibleSliderGroup,
modelTransform = uiState.modelTransform,
onScaleChange = onScaleChange,
onRotationChange = onRotationChange,
onOffsetChange = onOffsetChange,
onMaterialColorChange = onMaterialColorChange,
onMaterialPropertiesChange = onMaterialPropertiesChange,
modifier = modifier
)
} else {
TextPane(stringResource(R.string.block_of_content_2), modifier = modifier.fillMaxHeight())
}
}

@Composable
Expand Down
Loading