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

Codelab 
 Изучите основы Android XR: Часть 1 — Режимы и пространственные панели
  О подпространствах и пространственных компонентах
 При написании приложения для 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иwidthSubspaceModifier. Если эти спецификации не заданы, размер панели определяется размерами её содержимого.
-  Разрешите пользователю перемещать панель, добавив 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, имеют связанные с ними сущности без содержимого. Поэтому орбитер, объявленный в пространственном макете, привязывается к этому макету.
 Смотрите также