С помощью Jetpack Compose для XR можно декларативно создавать пространственный интерфейс и макет, используя знакомые концепции Compose, такие как строки и столбцы. Это позволяет расширить существующий интерфейс Android до трехмерного пространства или создать совершенно новые приложения с эффектом погружения.
Если вы пространственно обрабатываете существующее приложение на основе Android Views, у вас есть несколько вариантов разработки. Вы можете использовать API совместимости, работать с Compose и Views вместе или напрямую с библиотекой SceneCore. Подробнее о том, как работать с представлениями…
О подпространствах и пространственных компонентах
При разработке приложений для Android XR важно понимать, что такое подпространство и пространственные компоненты.
О подпространстве
При разработке для Android XR вам нужно добавить в приложение или макет подпространство. Подпространство – это часть трехмерного пространства в приложении, в которой можно размещать 3D-контент, создавать 3D-макеты и добавлять глубину в 2D-контент. Подпространство отображается, только если включена пространственная обработка. В главном пространстве или на устройствах, не поддерживающих XR, любой код в этом подпространстве игнорируется.
Создать подпространство можно несколькими способами:
Subspace: этот компонент создает новую независимую иерархию пространственного интерфейса. Он не наследует пространственное положение, ориентацию или масштаб родительского элементаSubspace, в который вложен.Subspaceавтоматически привязывается к рекомендованному системой блоку контента.PlanarEmbeddedSubspace– эту composable-функцию можно разместить в иерархии интерфейса приложения, чтобы поддерживать макеты для 2D- и пространственного интерфейса.PlanarEmbeddedSubspaceучитывает ограничения и положение родительского элемента. 3D-контент, помещенный в эту область, будет позиционироваться относительно нее.
Подробнее о том, как добавить подпространство в приложение…
О пространственных компонентах
Компоненты подпространства можно отрисовывать только в подпространстве.
Перед тем как разместить их в 2D-макете, их нужно заключить в теги Subspace.
SubspaceModifier позволяет добавлять в композиции подпространства такие атрибуты, как глубина, смещение и позиционирование.
Другие пространственные компоненты не обязательно вызывать внутри подпространства. Они состоят из обычных 2D-элементов, помещенных в пространственный контейнер. Эти элементы можно использовать в 2D- и 3D-макетах, если они определены для обоих типов. Если пространственное звучание отключено, пространственные функции будут игнорироваться и заменяться на 2D-аналоги.
Как создать пространственную панель
SpatialPanel – это composable-функция подпространства, которая позволяет показывать контент приложения, например видео, изображения или любой другой контент на пространственной панели.

Вы можете использовать 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 ) } }
Ключевые моменты
- Поскольку API
SpatialPanelявляются композициями подпространства, их необходимо вызывать внутриSubspace. Вызов их вне подпространства приводит к исключению. - Размер элемента
SpatialPanelзадан с помощью спецификацийheightиwidthдля элементаSubspaceModifier. Если не указать эти характеристики, размер панели будет определяться размерами ее содержимого. - Разрешите пользователю перемещать панель, добавив модификатор подпространства
movable. - Разрешите пользователю изменять размер панели, добавив модификатор подпространства
resizable. - Подробнее о размерах и расположении панелей можно узнать из руководства по дизайну пространственных панелей. Подробнее о реализации кода можно узнать в справочной документации.
Как работает модификатор movable
Когда пользователь перемещает панель, по умолчанию модификатор movable масштабирует ее так же, как система масштабирует панели в домашнем пространстве. Все детские видео наследуют это поведение. Чтобы отключить эту функцию, установите для параметра shouldScaleWithDistance значение false.
Как создать орбитер
Орбитер – это пространственный компонент интерфейса. Он предназначен для прикрепления к соответствующей пространственной панели или компоненту пространственного макета, например SpatialColumn, SpatialRow или SpatialBox. Орбитер обычно содержит элементы навигации и контекстные действия, связанные с объектом, к которому он прикреплен. Например, если вы создали пространственную панель для показа видеоконтента, то можете добавить элементы управления воспроизведением видео в орбитер.

Как показано в примере ниже, вызовите орбитер в двумерной разметке в SpatialPanel, чтобы обернуть элементы управления, например навигацию. При этом они будут извлечены из 2D-макета и прикреплены к пространственной панели в соответствии с вашей конфигурацией.
Subspace { SpatialPanel( SubspaceModifier .height(824.dp) .width(1400.dp) .resizable() .movable(), ) { SpatialPanelContent() OrbiterExample() } }
@Composable fun OrbiterExample() { Orbiter( alignment = OrbiterAlignment.BottomCenter( edgeOffsetType = OrbiterEdgeOffsetType.OuterEdge, offset = DpVolumeOffset(y = 96.dp) ), ) { 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 ) } } } }
Ключевые моменты
- Поскольку орбиты являются пространственными компонентами интерфейса, код можно использовать повторно в 2D- или 3D-макетах. В 2D-макете приложение обрабатывает только контент внутри орбитера и игнорирует сам орбитер.
- Подробнее о том, как использовать и создавать орбитеры, рассказывается в руководстве по дизайну.
Как добавить несколько пространственных панелей в пространственную раскладку
Вы можете создать несколько пространственных панелей и разместить их в пространственной разметке, используя 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, чтобы настроить макет. - Для макетов с несколькими панелями в ряд рекомендуется задать радиус кривизны 825 dp, используя
SubspaceModifier, чтобы панели окружали пользователя. Подробнее о дизайне…
Как добавить 3D-объект в макет с помощью SpatialGltfModel
Android XR поддерживает формат glTF для 3D-моделей, которые обычно сохраняются в виде файлов .glb. Чтобы добавить эти объекты в макет, используйте функцию SpatialGltfModel. Этот API упрощает процесс загрузки объектов и управления их состоянием.
Чтобы отобразить модель, сначала определите ее источник и состояние с помощью rememberSpatialGltfModelState. Вы можете загружать модели из папки assets приложения, URI или raw data.
val modelState = rememberSpatialGltfModelState( source = SpatialGltfModelSource.fromPath( Paths.get("models/model_name.glb") ) )
После того как состояние определено, используйте composable-функцию SpatialGltfModel, чтобы отрисовать его в Subspace.
SpatialGltfModel(state = modelState, modifier = SubspaceModifier)
Ключевые моменты
- Асинхронная загрузка. Модель загружается асинхронно. При первоначальном создании размер может быть нулевым. Макет будет пересчитан, когда модель будет готова.
- Управление состоянием. Используйте
SpatialGltfModelState.status, чтобы запросить статус загрузки или управлять анимацией. - Размер и масштабирование. По умолчанию размер макета соответствует ограничительной рамке объекта. Вы можете переопределить это значение с помощью параметра
SubspaceModifier.size, чтобы равномерно масштабировать модель в пределах указанных границ.
Используйте SceneCoreEntity, чтобы размещать объекты в макете
Компонуемый элемент SceneCoreEntity связывает библиотеки Jetpack SceneCore и Compose for XR, чтобы вы могли использовать объекты, созданные с помощью SceneCore, в макетах Compose. Это позволяет создавать объекты более низкого уровня и собственные компоненты, а Compose – определять их размер и положение, добавлять дочерние элементы и модификаторы, а также изменять родительские объекты.
val virtualPixelDensity = session.scene.virtualPixelDensity 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 = object : SceneCoreEntitySizeAdapter<SurfaceEntity> { override fun onLayoutSizeChanged( entity: SurfaceEntity, size: IntVolumeSize ) { val extents = FloatSize2d( size.width.toFloat(), size.height.toFloat() ) entity.shape = SurfaceEntity.Shape.Quad(extents) } }, ) { // Content here will be children of the SceneCoreEntity // in the scene graph. } }
Ключевые моменты
- Блок фабрики. В этом блоке инициализируется базовый объект
SceneCore. - Блок обновления. Используйте его, чтобы изменять свойства объекта в ответ на изменения состояния Compose.
- Адаптация размера.
sizeAdapterпередает размеры объекта обратно в систему макетов Compose.
Дополнительная информация
- Чтобы узнать, как загружать 3D-контент в
SceneCoreEntity, ознакомьтесь со статьей Как добавить 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с отрисовкой приложения или декодированием видео. - Этот компонент не может быть отрисован поверх других панелей, поэтому не следует использовать
MovePolicy, если в макете есть другие панели.
Как добавить поверхность для видеоконтента с защитой DRM
SpatialExternalSurface также поддерживает воспроизведение видеопотоков, защищенных DRM. Чтобы включить эту функцию, необходимо создать защищенную поверхность, которая будет отрисовываться в защищенных графических буферах. Это предотвращает запись экрана и доступ к контенту через небезопасные компоненты системы.
Чтобы создать безопасную поверхность, задайте для параметра SpatialExternalSurfaceProtection значение SpatialExternalSurfaceProtection.Protected в composable-функции SpatialExternalSurface. Кроме того, вам необходимо настроить Media3 Exoplayer с подходящей информацией о DRM, чтобы обрабатывать получение лицензии с сервера лицензий.
В следующем примере показано, как настроить SpatialExternalSurface и ExoPlayer для воспроизведения видеопотока, защищенного DRM:
@OptIn(ExperimentalComposeApi::class) @Composable fun DrmSpatialVideoPlayer() { val context = LocalContext.current Subspace { SpatialExternalSurface( modifier = SubspaceModifier .width(1200.dp) .height(676.dp), stereoMode = StereoMode.SideBySide, surfaceProtection = SpatialExternalSurfaceProtection.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() } } } }
Ключевые моменты
- Защищенная поверхность: настройка
surfaceProtection = SpatialExternalSurfaceProtection.ProtectedнаSpatialExternalSurfaceнеобходима, чтобы базовыйSurfaceподдерживался защищенными буферами, подходящими для контента с DRM. - Настройки DRM. В файле
MediaItemнеобходимо указать схему DRM (например,C.WIDEVINE_UUID) и URI сервера лицензий. ExoPlayer использует эту информацию для управления сеансом DRM. - Защищенный контент. При отрисовке на защищенной поверхности видеоконтент декодируется и отображается по защищенному пути, что помогает соблюдать требования лицензирования контента. Это также предотвращает появление контента на скриншотах.
Как добавить другие пространственные компоненты интерфейса
Компоненты пространственного интерфейса можно размещать в любом месте иерархии интерфейса приложения. Эти элементы можно повторно использовать в 2D-интерфейсе, а их пространственные атрибуты будут видны только при включенных пространственных возможностях. Это позволяет добавлять тень к меню, диалоговым окнам и другим компонентам, не дублируя код. Ниже приведены примеры пространственного интерфейса, которые помогут вам понять, как использовать эти элементы.
Компонент интерфейса |
Если пространственное звучание включено |
В 2D-среде |
|---|---|---|
|
Панель немного сдвинется назад по оси Z, чтобы показать диалоговое окно. |
Переход на 2D-режим |
|
Панель немного сдвинется назад по оси Z, чтобы показать всплывающее окно. |
Переход на 2D-версию |
|
|
Шоу без пространственного звучания. |
SpatialDialog
Это пример диалогового окна, которое открывается с небольшой задержкой. Если используется SpatialDialog, диалоговое окно появляется на той же глубине, что и пространственная панель, а панель отодвигается на 125 dp, когда пространственное звучание включено. SpatialDialog также можно использовать, когда пространственное звучание отключено. В этом случае SpatialDialog будет работать как 2D-аналог – Dialog.
@Composable fun DelayedDialog() { var showDialog by remember { mutableStateOf(false) } LaunchedEffect(Unit) { delay(3000.milliseconds) 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 и графом сцены, используя API SceneCore.
Как прикрепить орбитеры к пространственным панелям и макетам
Орбиту можно привязать к компонентам SpatialPanels и пространственной разметки, объявленным в Compose. Для этого нужно объявить орбитер в пространственной структуре элементов интерфейса, например SpatialRow, SpatialColumn или SpatialBox. Орбитер прикрепляется к родительскому элементу, который находится ближе всего к тому месту, где вы его объявили.
Поведение орбитера зависит от того, где вы его объявите:
- В 2D-макете, заключенном в тег
SpatialPanel(как показано в предыдущем фрагменте кода), орбитер привязывается к этому тегуSpatialPanel. - В
Subspaceорбитер привязывается к ближайшему родительскому объекту, то есть к пространственной структуре, в которой он объявлен.
В следующем примере показано, как привязать орбитер к пространственной строке:
Subspace { SpatialRow { Orbiter( alignment = OrbiterAlignment.TopCenter( edgeOffsetType = OrbiterEdgeOffsetType.OuterEdge, offset = DpVolumeOffset(y = 8.dp) ), 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-макета, он будет привязан к ближайшему родительскому объекту. В этом случае орбитер будет прикреплен к верхней части элемента
SpatialRow, в котором он объявлен. - Пространственные макеты, такие как
SpatialRow,SpatialColumn,SpatialBox, связаны с объектами без контента. Поэтому орбитер, объявленный в пространственной разметке, привязывается к ней.
Дополнительная информация
- Как добавить 3D-модели в приложение
- Как разрабатывать интерфейсы для приложений на основе объектов View в Android
- Как реализовать Material Design для XR