Разработка пользовательского интерфейса с помощью Jetpack Compose для XR

С помощью Jetpack Compose for XR вы можете декларативно создавать пространственный пользовательский интерфейс и макеты, используя знакомые концепции Compose, такие как строки и столбцы. Это позволяет расширить существующий пользовательский интерфейс Android в трехмерное пространство или создавать совершенно новые иммерсивные 3D-приложения.

Если вы занимаетесь пространственной интеграцией существующего приложения Android на основе Views, у вас есть несколько вариантов разработки. Вы можете использовать API для обеспечения взаимодействия, использовать Compose и Views вместе или работать напрямую с библиотекой SceneCore. Подробнее см. в нашем руководстве по работе с Views .

О подпространствах и пространственно-ориентированных компонентах

При разработке приложения для Android XR важно понимать концепции подпространства и пространственных компонентов .

О подпространстве

При разработке для Android XR вам потребуется добавить в приложение или макет Subspace ). Подпространство — это раздел трёхмерного пространства внутри приложения, где вы можете размещать трёхмерный контент, создавать трёхмерные макеты и добавлять глубину к двухмерному контенту. Подпространство отображается только при включенной пространственной визуализации. В главном пространстве (Home Space) или на устройствах без поддержки XR любой код внутри этого подпространства игнорируется.

Существует два способа создания подпространства:

  • Subspace : Этот компонуемый элемент можно разместить в любом месте иерархии пользовательского интерфейса вашего приложения, что позволяет сохранять макеты для 2D и пространственного интерфейса без потери контекста между файлами. Это упрощает обмен такими элементами, как существующая архитектура приложения, между 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
        )
    }
}

Основные моменты, касающиеся кода.

  • SpatialRow , SpatialColumn , SpatialBox и SpatialLayoutSpacer — это компоненты, компонуемые в подпространстве, и их необходимо размещать внутри подпространства.
  • Используйте SubspaceModifier для настройки раскладки.
  • Для макетов с несколькими панелями в ряд мы рекомендуем установить радиус кривизны 825dp с помощью SubspaceModifier , чтобы панели окружали пользователя. Подробности см. в наших рекомендациях по дизайну .

Используйте SceneCoreEntity для размещения объектов в вашем макете.

Для размещения 3D-объекта в макете вам потребуется использовать компонуемый подпространственный объект, называемый SceneCoreEntity . Вот пример того, как это сделать.

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.
    }
}

Дополнительная информация

Добавьте поверхность для размещения изображений или видеоконтента.

SpatialExternalSurface — это компонуемый подпространство объект, который создает и управляет Surface , на которую ваше приложение может отображать контент, например, изображение или видео . SpatialExternalSurface поддерживает как стереоскопический, так и моноскопический контент.

В этом примере показано, как загрузить стереоскопическое видео в режиме «бок о бок» с помощью Media3 Exoplayer и SpatialExternalSurface :

Для видео с углом обзора 180 или 360 градусов используйте SpatialExternalSurface180Hemisphere и SpatialExternalSurface360Sphere .

@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, чтобы отобразить диалоговое окно с увеличенным экраном.

Переключается на двухмерный Dialog .

SpatialPopup

Панель немного сдвинется назад по оси Z, чтобы отобразить приподнятое всплывающее окно.

В случае появления всплывающего окна в 2D-формате, происходит переход к следующему Popup .

SpatialElevation

SpatialElevationLevel позволяет добавлять отметки высоты.

Отображается без учета пространственного возвышения.

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")
                }
            }
        }
    }
}

Основные моменты, касающиеся кода.

Создавайте пользовательские панели и макеты.

Для создания пользовательских панелей, не поддерживаемых Compose for XR, вы можете работать напрямую с экземплярами PanelEntity и графом сцены, используя API SceneCore .

Привязывайте орбитальные аппараты к пространственным конфигурациям и другим объектам.

Вы можете привязать орбитер к любому объекту, объявленному в Compose. Это включает в себя объявление орбитера в пространственной компоновке элементов пользовательского интерфейса, таких как SpatialRow , SpatialColumn или SpatialBox . Орбитер привязывается к ближайшему родительскому объекту, расположенному рядом с местом его объявления.

Поведение орбитального аппарата определяется местом его объявления:

  • В двухмерной компоновке, заключенной в 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)
            )
        }
    }
}

Основные моменты, касающиеся кода.

  • Когда вы объявляете орбитер вне двумерной структуры, орбитер привязывается к ближайшему родительскому объекту. В данном случае орбитер привязывается к верхней части SpatialRow , в котором он объявлен.
  • Пространственные макеты, такие как SpatialRow , SpatialColumn , SpatialBox , содержат сущности, не имеющие содержимого. Поэтому объект-орбитер, объявленный в пространственном макете, привязывается к этому макету.

См. также