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

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

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

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

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

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

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

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

  • Subspace : этот компонуемый объект можно разместить в любом месте иерархии пользовательского интерфейса вашего приложения, что позволяет поддерживать макеты для двухмерного и пространственного пользовательского интерфейса без потери контекста между файлами. Это упрощает обмен такими элементами, как существующая архитектура приложения, между XR и другими форм-факторами без необходимости переносить состояние по всему дереву пользовательского интерфейса или перестраивать архитектуру приложения.
  • ApplicationSubspace : эта функция создаёт только подпространство уровня приложения и должна располагаться на самом верхнем уровне пространственной иерархии пользовательского интерфейса вашего приложения. ApplicationSubspace визуализирует пространственный контент с необязательными VolumeConstraints . В отличие от Subspace , ApplicationSubspace не может быть вложен в другое Subspace или ApplicationSubspace .

Для получения дополнительной информации см. раздел Добавление подпространства в ваше приложение .

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

Компонуемые элементы подпространства : эти компоненты могут быть отрисованы только в подпространстве. Перед размещением в 2D-макете они должны быть заключены в Subspace или setSubspaceContent() . SubspaceModifier позволяет добавлять к компонуемым элементам подпространства атрибуты, такие как глубина, смещение и позиционирование .

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

Ключевые моменты кода

  • Поскольку API SpatialPanel являются компонуемыми подпространствами, их необходимо вызывать внутри Subspace . Вызов вне подпространства приведёт к исключению.
  • Размер SpatialPanel задаётся с помощью спецификаций height и width SubspaceModifier . Если эти спецификации не заданы, размер панели определяется размерами её содержимого.
  • Разрешите пользователю изменять размер или перемещать панель, добавляя movable или resizable модификаторы.
  • Подробную информацию о размерах и расположении см. в нашем руководстве по проектированию пространственных панелей . Более подробную информацию о реализации кода см. в нашей справочной документации .

Как работает модификатор подвижного подпространства

Когда пользователь перемещает панель от себя, модификатор перемещаемого подпространства по умолчанию масштабирует панель аналогично тому, как система изменяет размер панелей в домашнем пространстве . Всё дочернее содержимое наследует это поведение. Чтобы отключить это, установите для параметра scaleWithDistance значение false .

Создать орбитальный аппарат

Орбитер — это пространственный компонент пользовательского интерфейса. Он предназначен для присоединения к соответствующей пространственной панели, макету или другому объекту. Орбитер обычно содержит элементы навигации и контекстные действия, связанные с объектом, к которому он привязан. Например, если вы создали пространственную панель для отображения видеоконтента, вы можете добавить элементы управления воспроизведением видео внутрь орбитера.

Пример орбитального аппарата

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

Ключевые моменты кода

  • Поскольку орбитеры являются пространственными компонентами пользовательского интерфейса, код можно повторно использовать в 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 , чтобы панели окружали пользователя. Подробнее см. в нашем руководстве по дизайну .

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

Чтобы разместить 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) {

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

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

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. Для этого необходимо создать защищенную поверхность, которая выполняет рендеринг в защищенные графические буферы. Это предотвращает запись экрана или доступ к контенту со стороны незащищенных компонентов системы.

Чтобы создать защищенную поверхность, установите для параметра 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 , имеют связанные с ними сущности без содержимого. Поэтому орбитер, объявленный в пространственном макете, привязывается к этому макету.

Смотрите также