XR용 Jetpack Compose를 사용하면 행, 열과 같은 익숙한 Compose 개념을 사용하여 공간 UI와 레이아웃을 선언적으로 빌드할 수 있습니다. 이를 통해 기존 Android UI를 3D 공간으로 확장하거나 완전히 새로운 몰입형 3D 애플리케이션을 빌드할 수 있습니다.
기존 Android Views 기반 앱을 공간화하는 경우 몇 가지 개발 옵션이 있습니다. 상호 운용성 API를 사용하거나, Compose와 뷰를 함께 사용하거나, SceneCore 라이브러리를 직접 사용할 수 있습니다. 자세한 내용은 뷰 작업 가이드를 참고하세요.
서브스페이스 및 공간화된 구성요소 정보
Android XR용 앱을 작성할 때는 하위 공간과 공간화된 구성요소의 개념을 이해하는 것이 중요합니다.
하위 스페이스 정보
Android XR용으로 개발할 때는 앱이나 레이아웃에 Subspace
를 추가해야 합니다. 하위 공간은 앱 내의 3D 공간 파티션으로, 여기에서 3D 콘텐츠를 배치하고 3D 레이아웃을 빌드하고 2D 콘텐츠에 깊이를 더할 수 있습니다. 서브스페이스는 공간화가 사용 설정된 경우에만 렌더링됩니다. 홈 공간 또는 XR이 아닌 기기에서는 해당 하위 공간 내의 코드가 무시됩니다.
하위 공간을 만드는 방법에는 두 가지가 있습니다.
Subspace
: 이 컴포저블은 앱의 UI 계층 구조 내 어디에나 배치할 수 있으므로 파일 간 컨텍스트를 잃지 않고 2D 및 공간 UI의 레이아웃을 유지할 수 있습니다. 이렇게 하면 전체 UI 트리를 통해 상태를 호이스팅하거나 앱을 다시 설계하지 않고도 XR과 기타 폼 팩터 간에 기존 앱 아키텍처와 같은 항목을 더 쉽게 공유할 수 있습니다.ApplicationSubspace
: 이 함수는 앱 수준의 하위 공간만 생성하며 애플리케이션의 공간 UI 계층 구조에서 최상위 수준에 배치해야 합니다.ApplicationSubspace
은 선택사항인VolumeConstraints
을 사용하여 공간 콘텐츠를 렌더링합니다.Subspace
와 달리ApplicationSubspace
은 다른Subspace
또는ApplicationSubspace
내에 중첩될 수 없습니다.
자세한 내용은 앱에 하위 스페이스 추가하기를 참고하세요.
공간화된 구성요소 정보
하위 스페이스 컴포저블: 이러한 구성요소는 하위 스페이스에서만 렌더링할 수 있습니다.
2D 레이아웃 내에 배치되기 전에 Subspace
또는 setSubspaceContent()
내에 포함되어야 합니다. SubspaceModifier
를 사용하면 하위 공간 컴포저블에 깊이, 오프셋, 위치 지정과 같은 속성을 추가할 수 있습니다.
다른 공간화된 구성요소는 하위 스페이스 내에서 호출할 필요가 없습니다. 공간 컨테이너 내에 래핑된 기존 2D 요소로 구성됩니다. 이러한 요소는 2D 및 3D 레이아웃 모두에 정의된 경우 2D 또는 3D 레이아웃 내에서 사용할 수 있습니다. 공간화가 사용 설정되지 않으면 공간화된 기능이 무시되고 2D 대응 항목으로 대체됩니다.
공간 패널 만들기
SpatialPanel
는 앱 콘텐츠를 표시할 수 있는 하위 공간 컴포저블입니다. 예를 들어 공간 패널에 동영상 재생, 정지 이미지 또는 기타 콘텐츠를 표시할 수 있습니다.
다음 예와 같이 SubspaceModifier
를 사용하여 공간 패널의 크기, 동작, 위치를 변경할 수 있습니다.
Subspace { SpatialPanel( SubspaceModifier .height(824.dp) .width(1400.dp) .movable() .resizable() ) { 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 ) } }
코드에 관한 핵심 사항
SpatialPanel
API는 하위 공간 컴포저블이므로Subspace
내에서 호출해야 합니다. 하위 공간 외부에서 호출하면 예외가 발생합니다.SpatialPanel
의 크기는SubspaceModifier
에서height
및width
사양을 사용하여 설정되었습니다. 이 사양을 생략하면 패널의 크기가 콘텐츠의 측정값에 따라 결정됩니다.movable
또는resizable
수정자를 추가하여 사용자가 패널의 크기를 조절하거나 이동할 수 있도록 합니다.- 크기 조정 및 위치 지정에 관한 자세한 내용은 공간 패널 디자인 안내를 참고하세요. 코드 구현에 관한 자세한 내용은 참조 문서를 참고하세요.
이동 가능한 하위 공간 수정자 작동 방식
사용자가 패널을 멀리 이동하면 기본적으로 이동 가능한 하위 공간 수정자가 홈 스페이스에서 시스템이 패널의 크기를 조절하는 방식과 유사하게 패널의 크기를 조절합니다. 모든 하위 콘텐츠가 이 동작을 상속합니다. 이 기능을 사용 중지하려면 scaleWithDistance
매개변수를 false
로 설정하세요.
오비터 만들기
Orbiter는 공간 UI 구성요소입니다. 해당 공간 패널, 레이아웃 또는 기타 항목에 연결되도록 설계되었습니다. 오비터에는 일반적으로 고정된 항목과 관련된 탐색 및 컨텍스트 작업 항목이 포함됩니다. 예를 들어 동영상 콘텐츠를 표시하는 공간 패널을 만든 경우, 오비터 내에 동영상 재생 컨트롤을 추가할 수 있습니다.
다음 예와 같이 SpatialPanel
의 2D 레이아웃 내에서 어비터를 호출하여 탐색과 같은 사용자 컨트롤을 래핑합니다. 이렇게 하면 2D 레이아웃에서 추출되어 구성에 따라 공간 패널에 연결됩니다.
Subspace { SpatialPanel( SubspaceModifier .height(824.dp) .width(1400.dp) .movable() .resizable() ) { 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 ) } } } }
코드에 관한 핵심 사항
- 오비터는 공간 UI 구성요소이므로 코드를 2D 또는 3D 레이아웃에서 재사용할 수 있습니다. 2D 레이아웃에서 앱은 오비터 내부의 콘텐츠만 렌더링하고 오비터 자체는 무시합니다.
- 오비터를 사용하고 디자인하는 방법에 관한 자세한 내용은 디자인 안내를 참고하세요.
공간 레이아웃에 여러 공간 패널 추가
SpatialRow
, SpatialColumn
, SpatialBox
, SpatialLayoutSpacer
를 사용하여 여러 공간 패널을 만들고 공간 레이아웃 내에 배치할 수 있습니다.
다음 코드 예시에서는 이 작업을 수행하는 방법을 보여줍니다.
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
,SpatialLayoutSpacer
은 모두 하위 공간 컴포저블이며 하위 공간 내에 배치해야 합니다.SubspaceModifier
를 사용하여 레이아웃을 맞춤설정합니다.- 행에 패널이 여러 개 있는 레이아웃의 경우
SubspaceModifier
를 사용하여 곡선 반지름을 825dp로 설정하여 패널이 사용자를 둘러싸도록 하는 것이 좋습니다. 자세한 내용은 디자인 안내를 참고하세요.
볼륨을 사용하여 레이아웃에 3D 객체 배치
레이아웃에 3D 객체를 배치하려면 볼륨이라는 서브스페이스 컴포저블을 사용해야 합니다. 다음은 그 방법을 보여주는 예입니다.
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, ) } } }
@OptIn(ExperimentalSubspaceVolumeApi::class) @Composable fun ObjectInAVolume(show3DObject: Boolean) {
추가 정보
- 볼륨 내에서 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() } } } }
코드에 관한 핵심 사항
- 렌더링하는 콘텐츠 유형에 따라
StereoMode
을Mono
,SideBySide
또는TopBottom
로 설정합니다.Mono
: 이미지 또는 동영상 프레임이 양쪽 눈에 표시되는 단일한 동일한 이미지로 구성됩니다.SideBySide
: 이미지 또는 동영상 프레임에 나란히 배치된 이미지 또는 동영상 프레임 쌍이 포함되어 있습니다. 왼쪽의 이미지 또는 프레임은 왼쪽 눈 보기를 나타내고 오른쪽의 이미지 또는 프레임은 오른쪽 눈 보기를 나타냅니다.TopBottom
: 이미지 또는 동영상 프레임에 세로로 쌓인 이미지 또는 동영상 프레임 쌍이 포함되어 있습니다. 상단의 이미지 또는 프레임은 왼쪽 눈 보기를 나타내고 하단의 이미지 또는 프레임은 오른쪽 눈 보기를 나타냅니다.
SpatialExternalSurface
는 직사각형 표면만 지원합니다.- 이
Surface
는 입력 이벤트를 캡처하지 않습니다. StereoMode
변경사항을 애플리케이션 렌더링 또는 동영상 디코딩과 동기화할 수 없습니다.- 이 컴포저블은 다른 패널 앞에 렌더링할 수 없으므로 레이아웃에 다른 패널이 있는 경우 이동 가능한 수정자를 사용하면 안 됩니다.
DRM으로 보호된 동영상 콘텐츠를 위한 표시 경로 추가
SpatialExternalSurface
은 DRM으로 보호되는 동영상 스트림의 재생도 지원합니다. 이를 사용 설정하려면 보호된 그래픽 버퍼에 렌더링되는 보안 표면을 만들어야 합니다. 이렇게 하면 콘텐츠가 화면 녹화되거나 보안되지 않은 시스템 구성요소에 의해 액세스되는 것을 방지할 수 있습니다.
보안 노출 영역을 만들려면 SpatialExternalSurface
컴포저블에서 surfaceProtection
매개변수를 SurfaceProtection.Protected
으로 설정합니다.
또한 라이선스 서버에서 라이선스 획득을 처리할 수 있도록 적절한 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 = SurfaceProtection.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에서 지원하지 않는 맞춤 패널을 만들려면 SceneCore
API를 사용하여 PanelEntity
인스턴스 및 장면 그래프를 직접 사용하면 됩니다.
오비터를 공간 레이아웃 및 기타 항목에 고정
Compose에 선언된 모든 항목에 어비터를 고정할 수 있습니다. 여기에는 SpatialRow
, SpatialColumn
, SpatialBox
와 같은 UI 요소의 공간 레이아웃에서 orbiter를 선언하는 작업이 포함됩니다. 트래커는 선언된 위치에 가장 가까운 상위 항목에 고정됩니다.
오비터의 동작은 선언된 위치에 따라 결정됩니다.
SpatialPanel
로 래핑된 2D 레이아웃에서 (이전 코드 스니펫 참고) 오비터는 해당SpatialPanel
에 고정됩니다.Subspace
에서 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가 가장 가까운 상위 항목에 고정됩니다. 이 경우 오비터는 선언된
SpatialRow
의 상단에 고정됩니다. SpatialRow
,SpatialColumn
,SpatialBox
와 같은 공간 레이아웃에는 콘텐츠가 없는 엔티티가 연결되어 있습니다. 따라서 공간 레이아웃에서 선언된 오비터는 해당 레이아웃에 고정됩니다.