Android XR SDK теперь доступен в предварительной версии для разработчиков. Нам нужны ваши отзывы! Посетите нашу
страницу поддержки , чтобы связаться с нами.
Разработка пользовательского интерфейса с помощью Jetpack Compose для XR Оптимизируйте свои подборки Сохраняйте и классифицируйте контент в соответствии со своими настройками.
С Jetpack Compose для XR вы можете декларативно создавать пространственный пользовательский интерфейс и макет, используя знакомые концепции Compose, такие как строки и столбцы. Это позволяет расширить существующий пользовательский интерфейс Android в трёхмерное пространство или создавать совершенно новые иммерсивные 3D-приложения.
Если вы занимаетесь пространственной визуализацией существующего приложения Android на основе Views, у вас есть несколько вариантов разработки. Вы можете использовать API взаимодействия, использовать Compose и Views вместе или работать напрямую с библиотекой SceneCore. Подробнее см. в нашем руководстве по работе с Views .

Codelab
Изучите основы Android XR: Часть 1 — Режимы и пространственные панели
arrow_forward О подпространствах и пространственных компонентах
При написании приложения для Android XR важно понимать концепции подпространства и пространственных компонентов .
О подпространстве
При разработке для Android XR вам потребуется добавить Subspace в приложение или макет. Подпространство — это раздел трёхмерного пространства в вашем приложении, где вы можете размещать 3D-контент, создавать 3D-макеты и добавлять глубину двумерному контенту. Подпространство отображается только при включённом пространственном отображении. В Home Space или на устройствах без поддержки XR любой код в этом подпространстве игнорируется.
Существует два способа создания подпространства:
-
Subspace : этот компонуемый объект можно разместить в любом месте иерархии пользовательского интерфейса вашего приложения, что позволяет поддерживать макеты для двухмерного и пространственного пользовательского интерфейса без потери контекста между файлами. Это упрощает обмен такими элементами, как существующая архитектура приложения, между XR и другими форм-факторами без необходимости переносить состояние по всему дереву пользовательского интерфейса или перестраивать архитектуру приложения. -
ApplicationSubspace : эта функция создаёт только подпространство уровня приложения и должна располагаться на самом верхнем уровне пространственной иерархии пользовательского интерфейса вашего приложения. ApplicationSubspace визуализирует пространственный контент с необязательными VolumeConstraints . В отличие от Subspace , ApplicationSubspace не может быть вложен в другое Subspace или ApplicationSubspace .
Для получения дополнительной информации см. раздел Добавление подпространства в ваше приложение .
О пространственных компонентах
Компонуемые элементы подпространства : эти компоненты могут быть визуализированы только в подпространстве. Перед размещением в 2D-макете они должны быть заключены в Subspace . SubspaceModifier позволяет добавлять к компонуемым элементам подпространства атрибуты, такие как глубина, смещение и позиционирование .
Другие пространственные компоненты не требуют вызова внутри подпространства. Они состоят из обычных 2D-элементов, обёрнутых в пространственный контейнер. Эти элементы могут использоваться в 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 ) } } Ключевые моменты кода
- Поскольку API
SpatialPanel являются компонуемыми подпространствами, их необходимо вызывать внутри Subspace . Вызов вне подпространства приведёт к исключению. - Размер
SpatialPanel задаётся с помощью спецификаций height и width SubspaceModifier . Если эти спецификации не заданы, размер панели определяется размерами её содержимого. - Разрешите пользователю перемещать панель, добавив
MovePolicy . - Разрешить пользователю изменять размер панели, добавив
ResizePolicy . - Подробную информацию о размерах и расположении см. в нашем руководстве по проектированию пространственных панелей . Более подробную информацию о реализации кода см. в нашей справочной документации .
Как работает MovePolicy
Когда пользователь перемещает панель от себя, по умолчанию MovePolicy масштабирует панель аналогично тому, как система изменяет размер панелей в домашнем пространстве . Всё дочернее содержимое наследует это поведение. Чтобы отключить это, установите для параметра shouldScaleWithDistance значение false .
Создать орбитальный аппарат
Орбитер — это пространственный компонент пользовательского интерфейса. Он предназначен для присоединения к соответствующей пространственной панели, макету или другому объекту. Орбитер обычно содержит элементы навигации и контекстные действия, связанные с объектом, к которому он привязан. Например, если вы создали пространственную панель для отображения видеоконтента, вы можете добавить элементы управления воспроизведением видео внутрь орбитера.

Как показано в следующем примере, вызовите орбитер внутри 2D-макета в SpatialPanel , чтобы обернуть пользовательские элементы управления, такие как навигация. Это извлечет их из 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 ) } } } } Ключевые моменты кода
- Поскольку орбитеры являются пространственными компонентами пользовательского интерфейса, код можно повторно использовать в 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 ) } } Ключевые моменты кода
Используйте объем для размещения 3D-объекта в макете.
Чтобы разместить 3D-объект в макете, вам потребуется использовать составное подпространство, называемое объёмом. Вот пример того, как это сделать.

Subspace { SpatialPanel( SubspaceModifier.height(1500.dp).width(1500.dp), dragPolicy = MovePolicy(), resizePolicy = ResizePolicy(), ) { ObjectInAVolume(true) Box( Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Text( text = "Welcome", fontSize = 50.sp, ) } } }@OptIn(ExperimentalSubspaceVolumeApi::class) @Composable fun ObjectInAVolume(show3DObject: Boolean) { val session = checkNotNull(LocalSession.current) val scope = rememberCoroutineScope() if (show3DObject) { Subspace { Volume( modifier = SubspaceModifier .offset(volumeXOffset, volumeYOffset, volumeZOffset) // Relative position .scale(1.2f) // Scale to 120% of the size ) { parent -> scope.launch { // Load your 3D model here } } } } } Дополнительная информация
Добавить поверхность для изображений или видеоконтента
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. Для этого необходимо создать защищенную поверхность, которая выполняет рендеринг в защищенные графические буферы. Это предотвращает запись экрана или доступ к контенту со стороны незащищенных компонентов системы.
Чтобы создать защищенную поверхность, установите для параметра surfaceProtection значение SurfaceProtection.Protected в компонуемом объекте 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 = 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() } } } } Ключевые моменты кода
- Защищенная поверхность: настройка
surfaceProtection = SurfaceProtection.Protected в SpatialExternalSurface необходима для того, чтобы базовая Surface поддерживалась безопасными буферами, подходящими для содержимого DRM. - Конфигурация DRM: необходимо настроить
MediaItem , используя схему DRM (например, C.WIDEVINE_UUID ) и URI вашего сервера лицензий. ExoPlayer использует эту информацию для управления сеансом DRM. - Защищённый контент: при рендеринге на защищённой поверхности видеоконтент декодируется и отображается по защищённому пути, что способствует выполнению требований лицензирования контента. Это также предотвращает его появление на снимках экрана.
Добавить другие пространственные компоненты пользовательского интерфейса
Пространственные компоненты пользовательского интерфейса можно размещать в любом месте иерархии пользовательского интерфейса вашего приложения. Эти элементы можно повторно использовать в вашем двухмерном пользовательском интерфейсе, а их пространственные атрибуты будут видны только при включенных пространственных возможностях. Это позволяет добавлять возвышенности к меню, диалоговым окнам и другим компонентам без необходимости писать код дважды. Ознакомьтесь со следующими примерами пространственного пользовательского интерфейса, чтобы лучше понять, как использовать эти элементы.
Компонент пользовательского интерфейса | Когда включено пространственное моделирование | В 2D-среде |
|---|
SpatialDialog | Панель слегка отодвинется назад по оси Z, чтобы отобразить приподнятое диалоговое окно. | Возвращается к 2D- Dialog . |
SpatialPopup | Панель слегка отодвинется назад по оси Z, чтобы отобразить всплывающее окно. | Возвращается к 2D Popup . |
SpatialElevation | SpatialElevationLevel можно настроить для добавления высоты. | Показывает без пространственного возвышения. |
SpatialDialog
Это пример диалогового окна, которое открывается с небольшой задержкой. При использовании SpatialDialog диалоговое окно отображается на той же глубине по оси Z, что и пространственная панель, а при включенной пространственной проекции панель отодвигается назад на 125 дп. SpatialDialog можно использовать и без пространственной проекции; в этом случае SpatialDialog возвращается к своему двумерному аналогу 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") } } } } } Ключевые моменты кода
Создавайте пользовательские панели и макеты
Чтобы создать пользовательские панели, которые не поддерживаются Compose for XR, вы можете работать напрямую с экземплярами PanelEntity и графом сцены, используя API SceneCore .
Привязка орбитальных аппаратов к пространственным схемам и другим объектам
Вы можете привязать орбитер к любой сущности, объявленной в Compose. Для этого необходимо объявить орбитер в пространственной структуре элементов пользовательского интерфейса, таких как SpatialRow , SpatialColumn или SpatialBox . Орбитер привязывается к родительской сущности, ближайшей к месту его объявления.
Поведение орбитера определяется тем, где вы его объявляете:
- В 2D-макете, упакованном в
SpatialPanel (как показано в предыдущем фрагменте кода ), орбитер привязывается к этой SpatialPanel . - В
Subspace орбитер привязывается к ближайшей родительской сущности, которая представляет собой пространственную компоновку, в которой объявлен орбитер.
В следующем примере показано, как прикрепить орбитальный аппарат к пространственному ряду:
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-макета он привязывается к ближайшему родительскому объекту. В этом случае орбитер привязывается к верхней части
SpatialRow , в котором он объявлен. - Пространственные макеты, такие как
SpatialRow , SpatialColumn , SpatialBox , имеют связанные с ними сущности без содержимого. Поэтому орбитер, объявленный в пространственном макете, привязывается к этому макету.
Смотрите также