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
2 changes: 2 additions & 0 deletions feature/camera/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ dependencies {
implementation(libs.androidx.concurrent.futures.ktx)
implementation(libs.androidx.window)
implementation(libs.androidx.window.core)
implementation(libs.androidx.xr.compose)
implementation(libs.accompanist.permissions)
implementation(libs.coil.compose)
implementation(libs.kotlinx.coroutines.play.services)
Expand All @@ -82,6 +83,7 @@ dependencies {
implementation(projects.core.theme)
implementation(projects.core.util)
implementation(projects.data)
implementation(projects.core.xr)

// Android Instrumented Tests
androidTestImplementation(platform(libs.androidx.compose.bom))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LifecycleStartEffect
import com.android.developers.androidify.camera.xr.CameraLayoutSpatial
import com.android.developers.androidify.theme.AndroidifyTheme
import com.android.developers.androidify.theme.TertiaryContainer
import com.android.developers.androidify.util.FoldablePreviewParameters
Expand All @@ -57,6 +59,7 @@ import com.android.developers.androidify.util.allowsFullContent
import com.android.developers.androidify.util.isAtLeastMedium
import com.android.developers.androidify.util.shouldShowTabletopLayout
import com.android.developers.androidify.util.supportsTabletop
import com.android.developers.androidify.xr.LocalSpatialCapabilities

@Composable
internal fun CameraLayout(
Expand All @@ -68,12 +71,16 @@ internal fun CameraLayout(
guideText: @Composable (modifier: Modifier) -> Unit,
guide: @Composable (modifier: Modifier) -> Unit,
rearCameraButton: @Composable (modifier: Modifier) -> Unit,
surfaceAspectRatio: Float,
xrEnabled: Boolean = false,
supportsTabletop: Boolean = supportsTabletop(),
isTabletop: Boolean = false,
) {
val mContext = LocalContext.current
val inspection = LocalInspectionMode.current
var isCameraLeft by remember { mutableStateOf(false) }
LifecycleStartEffect(Unit) {
if (inspection) return@LifecycleStartEffect onStopOrDispose { }
val displayManager = mContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val displayListener = object : DisplayManager.DisplayListener {
override fun onDisplayChanged(displayId: Int) {
Expand All @@ -94,6 +101,16 @@ internal fun CameraLayout(
.background(TertiaryContainer),
) {
when {
xrEnabled && LocalSpatialCapabilities.current.isSpatialUiEnabled -> CameraLayoutSpatial(
viewfinder,
captureButton,
flipCameraButton,
zoomButton,
guideText,
guide,
surfaceAspectRatio,
)

isAtLeastMedium() && shouldShowTabletopLayout(
supportsTabletop = supportsTabletop,
isTabletop = isTabletop,
Expand Down Expand Up @@ -565,6 +582,7 @@ private fun CameraOverlayPreview(
},
supportsTabletop = parameters.supportsTabletop,
isTabletop = parameters.isTabletop,
surfaceAspectRatio = 16f / 9f,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ fun CameraPreviewScreen(
toggleRearCameraFeature = { viewModel.toggleRearDisplayFeature(activity) },
isRearCameraEnabled = uiState.isRearCameraActive,
cameraSessionId = uiState.cameraSessionId,
xrEnabled = uiState.xrEnabled,
surfaceAspectRatio = uiState.surfaceAspectRatio,
)
}
} else {
Expand Down Expand Up @@ -203,12 +205,14 @@ fun StatelessCameraPreviewContent(
onAnimateZoom: (Float) -> Unit,
requestCaptureImage: () -> Unit,
modifier: Modifier = Modifier,
surfaceAspectRatio: Float = 9f / 16f,
xrEnabled: Boolean = false,
foldingFeature: FoldingFeature? = null,
shouldShowRearCameraFeature: () -> Boolean = { false },
toggleRearCameraFeature: () -> Unit = {},
isRearCameraEnabled: Boolean = false,
) {
var aspectRatio by remember { mutableFloatStateOf(9f / 16f) }
var layoutAspectRatio by remember { mutableFloatStateOf(9f / 16f) }
val emptyComposable: @Composable (Modifier) -> Unit = {}
val rearCameraButton: @Composable (Modifier) -> Unit = { rearModifier ->
RearCameraButton(
Expand Down Expand Up @@ -259,7 +263,7 @@ fun StatelessCameraPreviewContent(
CameraGuide(
detectedPose = detectedPose,
modifier = guideModifier,
defaultAspectRatio = aspectRatio,
defaultAspectRatio = layoutAspectRatio,
)
},
rearCameraButton = (
Expand All @@ -273,9 +277,12 @@ fun StatelessCameraPreviewContent(
modifier = modifier.onSizeChanged { size ->
if (size.height > 0) {
// Recalculate aspect ratio based on the overall layout size
aspectRatio = calculateCorrectAspectRatio(size.height, size.width, aspectRatio)
layoutAspectRatio =
calculateCorrectAspectRatio(size.height, size.width, layoutAspectRatio)
}
},
surfaceAspectRatio = surfaceAspectRatio,
xrEnabled = xrEnabled,
)
}

Expand All @@ -298,11 +305,13 @@ private fun CameraPreviewContent(
zoomLevel: () -> Float,
onChangeZoomLevel: (zoomLevel: Float) -> Unit,
requestCaptureImage: () -> Unit,
surfaceAspectRatio: Float,
modifier: Modifier = Modifier,
foldingFeature: FoldingFeature? = null,
shouldShowRearCameraFeature: () -> Boolean = { false },
toggleRearCameraFeature: () -> Unit = {},
isRearCameraEnabled: Boolean = false,
xrEnabled: Boolean = false,
) {
val scope = rememberCoroutineScope()
val zoomState = remember(cameraSessionId) {
Expand All @@ -324,7 +333,8 @@ private fun CameraPreviewContent(
onScaleZoom = { scope.launch { zoomState.scaleZoom(it) } },
modifier = viewfinderModifier.onSizeChanged { size -> // Apply modifier from slot
if (size.height > 0) {
aspectRatio = calculateCorrectAspectRatio(size.height, size.width, aspectRatio)
aspectRatio =
calculateCorrectAspectRatio(size.height, size.width, aspectRatio)
}
},
)
Expand All @@ -341,6 +351,8 @@ private fun CameraPreviewContent(
shouldShowRearCameraFeature = shouldShowRearCameraFeature,
toggleRearCameraFeature = toggleRearCameraFeature,
isRearCameraEnabled = isRearCameraEnabled,
surfaceAspectRatio = surfaceAspectRatio,
xrEnabled = xrEnabled,
modifier = modifier,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import com.android.developers.androidify.data.ConfigProvider
import com.android.developers.androidify.util.LocalFileProvider
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.pose.PoseDetection
Expand All @@ -76,8 +77,9 @@ class CameraViewModel
application: Application,
val localFileProvider: LocalFileProvider,
val rearCameraUseCase: RearCameraUseCase,
configProvider: ConfigProvider,
) : AndroidViewModel(application) {
private var _uiState = MutableStateFlow(CameraUiState())
private var _uiState = MutableStateFlow(CameraUiState(xrEnabled = configProvider.isXrEnabled()))
val uiState: StateFlow<CameraUiState>
get() = _uiState

Expand All @@ -90,10 +92,12 @@ class CameraViewModel

private val cameraPreviewUseCase = Preview.Builder().build().apply {
setSurfaceProvider { newSurfaceRequest ->
_uiState.update { it.copy(surfaceRequest = newSurfaceRequest) }
val width = newSurfaceRequest.resolution.width.toFloat()
val height = newSurfaceRequest.resolution.height.toFloat()
_uiState.update { it.copy(surfaceRequest = newSurfaceRequest, surfaceAspectRatio = height / width) }
surfaceMeteringPointFactory = SurfaceOrientedMeteringPointFactory(
newSurfaceRequest.resolution.width.toFloat(),
newSurfaceRequest.resolution.height.toFloat(),
width,
height,
)
}
}
Expand Down Expand Up @@ -351,6 +355,8 @@ data class CameraUiState(
val canFlipCamera: Boolean = true,
val isRearCameraActive: Boolean = false,
val autofocusUiState: AutofocusUiState = AutofocusUiState.Unspecified,
val xrEnabled: Boolean = false,
val surfaceAspectRatio: Float = 9f / 16f,
) {
val zoomOptions = when {
zoomMinRatio <= 0.6f && zoomMaxRatio >= 1f -> listOf(0.6f, 1f)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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.android.developers.androidify.camera.xr

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.xr.compose.spatial.ContentEdge
import androidx.xr.compose.spatial.Orbiter
import androidx.xr.compose.spatial.OrbiterOffsetType
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.aspectRatio
import androidx.xr.compose.subspace.layout.fillMaxSize
import com.android.developers.androidify.xr.MainPanelWorkaround
import com.android.developers.androidify.xr.RequestHomeSpaceIconButton

@Composable
fun CameraLayoutSpatial(
viewfinder: @Composable (modifier: Modifier) -> Unit,
captureButton: @Composable (modifier: Modifier) -> Unit,
flipCameraButton: @Composable (modifier: Modifier) -> Unit,
zoomButton: @Composable (modifier: Modifier) -> Unit,
guideText: @Composable (modifier: Modifier) -> Unit,
guide: @Composable (modifier: Modifier) -> Unit,
surfaceAspectRatio: Float,
) {
Subspace {
MainPanelWorkaround()
SpatialPanel(
SubspaceModifier
.fillMaxSize(0.5f)
.aspectRatio(surfaceAspectRatio),
) {
Orbiter(
position = ContentEdge.Top,
offsetType = OrbiterOffsetType.InnerEdge,
offset = 32.dp,
alignment = Alignment.End,
) {
RequestHomeSpaceIconButton(
modifier = Modifier
.size(64.dp, 64.dp)
.padding(8.dp),
colors = IconButtonDefaults.iconButtonColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
),
)
}
Orbiter(ContentEdge.Start, offsetType = OrbiterOffsetType.InnerEdge, offset = 16.dp) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
captureButton(Modifier)
flipCameraButton(Modifier)
}
}
Orbiter(ContentEdge.Bottom, offsetType = OrbiterOffsetType.InnerEdge) {
zoomButton(Modifier)
}
Box(Modifier.fillMaxSize()) {
viewfinder(Modifier)
guide(Modifier.fillMaxSize())
guideText(
Modifier
.align(Alignment.BottomCenter)
.padding(horizontal = 36.dp, vertical = 64.dp),
)
}
}
}
}