XR용 Jetpack Compose를 사용하면 행 및 열과 같은 익숙한 Compose 개념을 사용하여 공간 UI 및 레이아웃을 선언적으로 빌드할 수 있습니다. 이렇게 하면 기존 Android UI를 3D 공간으로 확장하거나 완전히 새로운 몰입형 3D 애플리케이션을 빌드할 수 있습니다.
기존 Android 뷰 기반 앱을 공간화하는 경우 여러 개발 옵션이 있습니다. 상호 운용성 API를 사용하거나 Compose와 뷰를 함께 사용하거나 SceneCore 라이브러리를 직접 사용할 수 있습니다. 자세한 내용은 뷰 작업 가이드를 참고하세요.
하위 공간 및 공간화된 구성요소 정보
Android XR용 앱을 작성할 때는 하위 공간 및 공간화된 구성요소 의 개념을 이해하는 것이 중요합니다.
하위 공간 정보
Android XR용으로 개발할 때는 앱 또는 레이아웃에 하위 공간을 추가해야 합니다. 하위 공간은 앱 내의 3D 공간 파티션으로, 여기에서 3D 콘텐츠를 배치하고 3D 레이아웃을 빌드하고 2D 콘텐츠에 깊이를 더할 수 있습니다. 하위 공간은 공간화가 사용 설정된 경우에만 렌더링됩니다. 홈 공간 또는 비 XR 기기에서는 해당 하위 공간 내의 모든 코드가 무시됩니다.
다음과 같은 몇 가지 방법으로 하위 공간을 만들 수 있습니다.
Subspace: 이 컴포저블은 새로운 독립적인 공간 UI 계층 구조를 만듭니다. 중첩된 상위Subspace의 공간 위치, 방향 또는 크기를 상속하지 않습니다.Subspace는 시스템의 권장 콘텐츠 상자에 의해 자동으로 바인딩됩니다.PlanarEmbeddedSubspace: 이 컴포저블은 앱의 UI 계층 구조 내에 배치할 수 있으므로 2D 및 공간 UI의 레이아웃을 유지할 수 있습니다.PlanarEmbeddedSubspace는 상위 요소의 제약 조건과 위치를 준수합니다. 내부에 배치된 3D 콘텐츠는 이 2D 정의 영역을 기준으로 배치됩니다.
자세한 내용은 앱에 하위 공간 추가를 참고하세요.
공간화된 구성요소 정보
하위 공간 컴포저블: 이러한 구성요소는 하위 공간에서만 렌더링할 수 있습니다.
2D 레이아웃 내에 배치되기 전에 Subspace 내에 포함되어야 합니다.
SubspaceModifier를 사용하면 하위 공간 컴포저블에 깊이, 오프셋, 위치 지정과 같은 속성을 추가할 수 있습니다.
다른 공간화된 구성요소는 하위 공간 내에서 호출할 필요가 없습니다. 공간 컨테이너 내에 래핑된 기존 2D 요소로 구성됩니다. 이러한 요소는 2D 및 3D 레이아웃 모두에 정의된 경우 2D 또는 3D 레이아웃 내에서 사용할 수 있습니다. 공간화가 사용 설정되지 않은 경우 공간화된 기능은 무시되고 2D 대응 요소로 대체됩니다.
공간 패널 만들기
SpatialPanel은 앱
콘텐츠를 표시할 수 있는 하위 공간 컴포저블입니다. 예를 들어 공간 패널에 동영상 재생, 스틸 이미지 또는 기타
콘텐츠를 표시할 수 있습니다.

다음 예와 같이 SubspaceModifier를 사용하여 공간 패널의 크기, 동작, 위치를 변경할 수 있습니다.
Subspace { SpatialPanel( SubspaceModifier .height(824.dp) .width(1400.dp), dragPolicy = MovePolicy(), resizePolicy = ResizePolicy(), ) { 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 ) } }
코드의 주요 사항
SpatialPanelAPI는 하위 공간 컴포저블이므로Subspace내에서 호출해야 합니다. 하위 공간 외부에서 호출하면 예외가 발생합니다.SpatialPanel의 크기는SubspaceModifier의height및width사양을 사용하여 설정되었습니다. 이러한 사양을 생략하면 패널의 크기가 콘텐츠의 측정에 따라 결정됩니다.movable하위 공간 수정자를 추가하여 사용자가 패널을 이동할 수 있도록 합니다.resizable하위 공간 수정자를 추가하여 사용자가 패널의 크기를 조절할 수 있도록 합니다.- 크기 조절 및 위치 지정에 관한 자세한 내용은 공간 패널 디자인 가이드를 참고하세요. 코드 구현에 관한 자세한 내용은 참조 문서를 참고하세요.
movable 수정자 작동 방식
사용자가 패널을 멀리 이동하면 기본적으로 movable 수정자는
홈 공간에서 시스템이 패널의 크기를 조절하는 방식과 유사하게 패널의 크기를 조절합니다
. 모든 하위 콘텐츠는 이 동작을 상속합니다. 이 기능을 사용 중지하려면 shouldScaleWithDistance 매개변수를 false로 설정합니다.
Orbiter 만들기
Orbiter는 공간 UI 구성요소입니다. SpatialColumn, SpatialRow 또는 SpatialBox와 같은 상응하는 공간 패널 또는 공간 레이아웃 구성요소에 연결되도록 설계되었습니다. Orbiter에는 일반적으로 고정된 항목과 관련된 탐색 및 상황별 작업 항목이 포함됩니다. 예를 들어 동영상 콘텐츠를 표시하는 공간 패널을 만든 경우 Orbiter 내에 동영상 재생 컨트롤을 추가할 수 있습니다.

다음 예와 같이 SpatialPanel의 2D 레이아웃 내에서 Orbiter를 호출하여 탐색과 같은 사용자 컨트롤을 래핑합니다. 이렇게 하면 2D 레이아웃에서 추출되어 구성에 따라 공간 패널에 연결됩니다.
Subspace { SpatialPanel( SubspaceModifier .height(824.dp) .width(1400.dp), dragPolicy = MovePolicy(), resizePolicy = ResizePolicy(), ) { SpatialPanelContent() OrbiterExample() } }
@Composable fun OrbiterExample() { Orbiter( position = ContentEdge.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 ) } } } }
코드의 주요 사항
- Orbiter는 공간 UI 구성요소이므로 코드를 2D 또는 3D 레이아웃에서 재사용할 수 있습니다. 2D 레이아웃에서 앱은 Orbiter 내의 콘텐츠만 렌더링하고 Orbiter 자체는 무시합니다.
- Orbiter를 사용하고 디자인하는 방법에 관한 자세한 내용은 디자인 가이드를 참고하세요.
공간 레이아웃에 여러 공간 패널 추가
여러 공간 패널을 만들고 공간 레이아웃 내에 배치할 수 있습니다.
SpatialRow, SpatialColumn, SpatialBox 및
SpatialSpacer을 사용하여

다음 코드 예에서는 이 작업을 실행하는 방법을 보여줍니다.
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") } } } }
@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 ) } }
코드의 주요 사항
SpatialRow,SpatialColumn,SpatialBox,SpatialSpacer는 모두 하위 공간 컴포저블이며 하위 공간 내에 배치해야 합니다.SubspaceModifier를 사용하여 레이아웃을 맞춤설정합니다.- 한 행에 여러 패널이 있는 레이아웃의 경우
SubspaceModifier를 사용하여 곡선 반지름을 825dp로 설정하는 것이 좋습니다. 이렇게 하면 패널이 사용자를 둘러싸게 됩니다. 자세한 내용은 디자인 가이드를 참고하세요.
SpatialGltfModel을 사용하여 레이아웃에 3D 객체 추가
Android XR은 일반적으로
.glb 파일로 저장되는 3D 모델의 glTF 형식을 지원합니다. 이러한 객체를 레이아웃에 추가하려면
SpatialGltfModel 컴포저블을 사용해야 합니다. 이 API는 애셋 로드 및 상태 관리 프로세스를 간소화합니다.
모델을 표시하려면 먼저 모델의 소스와 상태를 사용하여
rememberSpatialGltfModelState 정의합니다. 앱의 assets 폴더, URI 또는
raw data에서
모델을 로드할 수 있습니다.
val modelState = rememberSpatialGltfModelState( source = SpatialGltfModelSource.fromPath( Paths.get("models/model_name.glb") ) )
상태가 정의되면 SpatialGltfModel 컴포저블을 사용하여 하위 공간 내에서 렌더링합니다.
SpatialGltfModel(state = modelState, modifier = SubspaceModifier)
코드의 주요 사항
- 비동기 로드: 모델이 비동기식으로 로드됩니다. 초기 구성 중에 고유 크기가 0일 수 있습니다. 모델이 준비되면 레이아웃이 다시 측정됩니다.
- 상태 제어:
SpatialGltfModelState.status를 사용하여 로드 상태를 쿼리하거나 애니메이션을 제어합니다. - 크기 조절 및 크기 조정: 기본적으로 레이아웃 크기는 애셋의 경계 상자와 일치합니다.
SubspaceModifier.size를 사용하여 이 설정을 재정의하고 지정된 경계 내에 맞게 모델의 크기를 균일하게 조절할 수 있습니다.
SceneCoreEntity를 사용하여 레이아웃에 항목 배치
SceneCoreEntity 컴포저블은 Jetpack
SceneCore와 Compose for XR 라이브러리를 연결하므로 Compose 레이아웃에서 SceneCore로 빌드된
항목을 사용할 수 있습니다. 이렇게 하면 Compose에서 이러한 항목의 크기를 조절하고, 배치하고, 상위 요소를 변경하고, 하위 요소를 추가하고, 수정자를 적용할 수 있도록 하면서 하위 수준 항목과 맞춤 구성요소를 빌드할 수 있습니다.
Subspace { SceneCoreEntity( modifier = SubspaceModifier.offset(x = 50.dp), factory = { SurfaceEntity.create( session = session, pose = Pose.Identity, stereoMode = SurfaceEntity.StereoMode.MONO ) }, update = { entity -> // compose state changes may be applied to the // SceneCore entity here. entity.stereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE }, sizeAdapter = SceneCoreEntitySizeAdapter({ IntSize2d(it.width, it.height) }), ) { // Content here will be children of the SceneCoreEntity // in the scene graph. } }
코드의 주요 사항
- 팩토리 블록: 팩토리 블록은 기본
SceneCore항목을 초기화하는 위치입니다. - 업데이트 블록: 업데이트 블록을 사용하여 Compose 상태의 변경사항에 따라 항목의 속성을 수정합니다.
- 크기 조정:
sizeAdapter는 항목의 측정기준을 Compose 레이아웃 시스템에 다시 전달합니다.
추가 정보
SceneCoreEntity내에서 3D 콘텐츠를 로드하는 방법을 자세히 알아보려면 앱에 3D 모델 추가를 참고하세요.
이미지 또는 동영상 콘텐츠의 노출 영역 추가
SpatialExternalSurface는 앱에서 이미지 또는 동영상과 같은 콘텐츠를 그릴 수 있는 Surface를 만들고
관리하는 하위 공간 컴포저블입니다. SpatialExternalSurface 는 스테레오스코픽 또는 모노스코픽 콘텐츠를 지원합니다.
이 예에서는 Media3 Exoplayer 및 SpatialExternalSurface를 사용하여 나란히 입체 동영상을 로드하는 방법을 보여줍니다.
@OptIn(ExperimentalComposeApi::class) @Composable fun SpatialExternalSurfaceContent() { val context = LocalContext.current Subspace { SpatialExternalSurface( modifier = SubspaceModifier .width(1200.dp) // Default width is 400.dp if no width modifier is specified .height(676.dp), // Default height is 400.dp if no height modifier is specified // Use StereoMode.Mono, StereoMode.SideBySide, or StereoMode.TopBottom, depending // upon which type of content you are rendering: monoscopic content, side-by-side stereo // content, or top-bottom stereo content stereoMode = StereoMode.SideBySide, ) { val exoPlayer = remember { ExoPlayer.Builder(context).build() } val videoUri = Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) // Represents a side-by-side stereo video, where each frame contains a pair of // video frames arranged side-by-side. The frame on the left represents the left // eye view, and the frame on the right represents the right eye view. .path("sbs_video.mp4") .build() val mediaItem = MediaItem.fromUri(videoUri) // onSurfaceCreated is invoked only one time, when the Surface is created onSurfaceCreated { surface -> exoPlayer.setVideoSurface(surface) exoPlayer.setMediaItem(mediaItem) exoPlayer.prepare() exoPlayer.play() } // onSurfaceDestroyed is invoked when the SpatialExternalSurface composable and its // associated Surface are destroyed onSurfaceDestroyed { exoPlayer.release() } } } }
코드의 주요 사항
- 렌더링하는 콘텐츠 유형에 따라
Mono: 이미지 또는 동영상 프레임은 양쪽 눈에 표시되는 동일한 단일 이미지로 구성됩니다.SideBySide: 이미지 또는 동영상 프레임에는 나란히 배치된 이미지 또는 동영상 프레임 쌍이 포함되어 있습니다. 여기서 왼쪽의 이미지 또는 프레임은 왼쪽 눈 뷰를 나타내고 오른쪽의 이미지 또는 프레임은 오른쪽 눈 뷰를 나타냅니다.TopBottom: 이미지 또는 동영상 프레임에는 세로로 쌓인 이미지 또는 동영상 프레임 쌍이 포함되어 있습니다. 여기서 상단의 이미지 또는 프레임은 왼쪽 눈 뷰를 나타내고 하단의 이미지 또는 프레임은 오른쪽 눈 뷰를 나타냅니다.
StereoMode를Mono,SideBySide, 또는TopBottom으로 설정합니다.SpatialExternalSurface는 직사각형 노출 영역만 지원합니다.- 이
Surface는 입력 이벤트를 캡처하지 않습니다. StereoMode변경사항을 애플리케이션 렌더링 또는 동영상 디코딩과 동기화할 수 없습니다.- 이 컴포저블은 다른 패널 앞에 렌더링할 수 없으므로 레이아웃에 다른 패널이 있는 경우
MovePolicy를 사용해서는 안 됩니다.
DRM으로 보호된 동영상 콘텐츠의 노출 영역 추가
SpatialExternalSurface 는 DRM으로 보호된 동영상 스트림의 재생도 지원합니다. 이 기능을 사용 설정하려면 보호된 그래픽 버퍼에 렌더링되는 보안 노출 영역을 만들어야 합니다. 이렇게 하면 콘텐츠가 화면 녹화되거나 보안되지 않은 시스템 구성요소에서 액세스되는 것을 방지할 수 있습니다.
보안 표시 경로를 만들려면 SpatialExternalSurfaceProtection 매개변수
를 SpatialExternalSurfaceProtection.Protected로 설정합니다.
SpatialExternalSurface 컴포저블에서 또한 라이선스 서버에서 라이선스 획득을 처리하려면 적절한 DRM 정보로
Media3 Exoplayer를 구성해야 합니다.
다음 예에서는 DRM으로 보호된 동영상 스트림을 재생하도록 SpatialExternalSurface 및 ExoPlayer를 구성하는 방법을 보여줍니다.
@OptIn(ExperimentalComposeApi::class) @Composable fun DrmSpatialVideoPlayer() { val context = LocalContext.current Subspace { SpatialExternalSurface( modifier = SubspaceModifier .width(1200.dp) .height(676.dp), stereoMode = StereoMode.SideBySide, surfaceProtection = SurfaceProtection.Protected ) { val exoPlayer = remember { ExoPlayer.Builder(context).build() } // Define the URI for your DRM-protected content and license server. val videoUri = "https://your-content-provider.com/video.mpd" val drmLicenseUrl = "https://your-license-server.com/license" // Build a MediaItem with the necessary DRM configuration. val mediaItem = MediaItem.Builder() .setUri(videoUri) .setDrmConfiguration( MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID) .setLicenseUri(drmLicenseUrl) .build() ) .build() onSurfaceCreated { surface -> // The created surface is secure and can be used by the player. exoPlayer.setVideoSurface(surface) exoPlayer.setMediaItem(mediaItem) exoPlayer.prepare() exoPlayer.play() } onSurfaceDestroyed { exoPlayer.release() } } } }
코드의 주요 사항
- 보호된 노출 영역: 기본
Surface가 DRM 콘텐츠에 적합한 보안 버퍼로 지원되도록 하려면SpatialExternalSurface에서surfaceProtection = SpatialExternalSurfaceProtection.Protected을 설정하는 것이 필수적입니다. - DRM 구성: DRM 스키마(예:
C.WIDEVINE_UUID) 및 라이선스 서버의 URI로MediaItem을 구성해야 합니다. ExoPlayer는 이 정보를 사용하여 DRM 세션을 관리합니다. - 보안 콘텐츠: 보호된 노출 영역에 렌더링할 때 동영상 콘텐츠가 디코딩되어 보안 경로에 표시되므로 콘텐츠 라이선스 요구사항을 충족하는 데 도움이 됩니다. 또한 콘텐츠가 스크린샷에 표시되지 않도록 합니다.
기타 공간 UI 구성요소 추가
공간 UI 구성요소는 애플리케이션의 UI 계층 구조 내 어디에나 배치할 수 있습니다. 이러한 요소는 2D UI에서 재사용할 수 있으며 공간 기능이 사용 설정된 경우에만 공간 속성이 표시됩니다. 이렇게 하면 코드를 두 번 작성하지 않고도 메뉴, 대화상자, 기타 구성요소에 입체감을 더할 수 있습니다. 이러한 요소를 사용하는 방법을 자세히 알아보려면 다음 공간 UI 예를 참고하세요.
UI 구성요소 |
공간화가 사용 설정된 경우 |
2D 환경 |
|---|---|---|
|
패널이 z-깊이에서 약간 뒤로 밀려 입체감 있는 대화상자를 표시합니다. |
2D |
|
패널이 z-깊이에서 약간 뒤로 밀려 입체감 있는 팝업을 표시합니다. |
2D |
|
|
공간 입체감 없이 표시됩니다. |
SpatialDialog
이것은 짧은 지연 시간 후에 열리는 대화상자의 예입니다.
SpatialDialog가 사용되면 대화상자가
공간 패널과 동일한 z-깊이에 표시되고 공간화가
사용 설정되면 패널이 125dp 뒤로 밀립니다. SpatialDialog는 공간화가 사용 설정되지 않은 경우에도 사용할 수 있습니다. 이 경우 SpatialDialog는 2D 대응 요소인 Dialog로 대체됩니다.
@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") } } } } }
코드의 주요 사항
- 이것은
SpatialDialog의 예입니다.SpatialPopup및SpatialElevation을 사용하는 것은 매우 유사합니다. 자세한 내용은 API 참조를 참고하세요.
맞춤 패널 및 레이아웃 만들기
Compose for XR에서 지원되지 않는 맞춤 패널을 만들려면 PanelEntity 인스턴스 및 장면 그래프를 SceneCore API를 사용하여 직접 사용하면 됩니다.
공간 패널 및 레이아웃에 Orbiter 고정
Compose에서 선언된 SpatialPanels 및 공간 레이아웃 구성요소에 Orbiter를 고정할 수 있습니다. 여기에는 SpatialRow, SpatialColumn 또는 SpatialBox와 같은 UI 요소의 공간 레이아웃에서 Orbiter를 선언하는 것이 포함됩니다. Orbiter는 선언한 위치에 가장 가까운 상위 요소에 고정됩니다.
Orbiter의 동작은 선언하는 위치에 따라 결정됩니다.
- 이전 코드 예와 같이
SpatialPanel로 래핑된 2D 레이아웃에서 Orbiter는 해당SpatialPanel에 고정됩니다. Subspace에서 Orbiter는 Orbiter가 선언된 공간 레이아웃인 가장 가까운 상위 항목에 고정됩니다.
다음 예에서는 Orbiter를 공간 행에 고정하는 방법을 보여줍니다.
Subspace { SpatialRow { Orbiter( position = ContentEdge.Top, offset = 8.dp, offsetType = OrbiterOffsetType.InnerEdge, shape = SpatialRoundedCornerShape(size = CornerSize(50)) ) { Text( "Hello World!", style = MaterialTheme.typography.titleMedium, 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) ) } } }
코드의 주요 사항
- 2D 레이아웃 외부에서 Orbiter를 선언하면 Orbiter가 가장 가까운 상위 항목에 고정됩니다. 이 경우 Orbiter는 선언된
SpatialRow의 상단에 고정됩니다. SpatialRow,SpatialColumn,SpatialBox와 같은 공간 레이아웃에는 모두 콘텐츠가 없는 항목이 연결되어 있습니다. 따라서 공간 레이아웃에서 선언된 Orbiter는 해당 레이아웃에 고정됩니다.