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

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

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

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

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

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

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

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

  • setSubspaceContent() : эта функция создает подпространство уровня приложения. Его можно вызвать в вашей основной активности так же, как вы используете setContent() . Подпространство уровня приложения не ограничено по высоте, ширине и глубине, по сути предоставляя бесконечный холст для пространственного контента.
  • Subspace : Этот компонуемый элемент можно разместить в любом месте иерархии пользовательского интерфейса вашего приложения, что позволяет вам поддерживать макеты для 2D и пространственного пользовательского интерфейса без потери контекста между файлами. Это упрощает обмен такими вещами, как существующая архитектура приложения, между XR и другими форм-факторами без необходимости поднимать состояние через все дерево пользовательского интерфейса или перестраивать архитектуру вашего приложения.

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

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

Компонуемые подпространства : эти компоненты могут быть визуализированы только в подпространстве. Они должны быть заключены в Subspace или setSubspaceContent() перед размещением в 2D-макете. 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 или setSubspaceContent() . Вызов их за пределами подпространства приводит к исключению.
  • Размер 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 = OrbiterEdge.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,
            )
        }
    }
}

@Composable
fun ObjectInAVolume(show3DObject: Boolean) {

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

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

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

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

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

Добавить другие пространственные компоненты пользовательского интерфейса

Пространственные компоненты пользовательского интерфейса могут быть размещены в любом месте иерархии пользовательского интерфейса вашего приложения. Эти элементы можно повторно использовать в вашем 2D-интерфейсе, а их пространственные атрибуты будут видны только при включенных пространственных возможностях. Это позволяет добавлять возвышение к меню, диалоговым окнам и другим компонентам без необходимости писать код дважды. Ознакомьтесь со следующими примерами пространственного пользовательского интерфейса, чтобы лучше понять, как использовать эти элементы.

Компонент пользовательского интерфейса

Когда включено пространственное позиционирование

В 2D среде

SpatialDialog

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

Возвращается к 2D- 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 . Орбитер привязывается к родительской сущности, ближайшей к месту, где вы его объявили.

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

  • В 2D-макете, упакованном в SpatialPanel (как показано в предыдущем фрагменте кода ), орбитер привязывается к этой SpatialPanel .
  • В Subspace орбитер привязывается к ближайшей родительской сущности, которая представляет собой пространственную компоновку, в которой объявлен орбитер.

В следующем примере показано, как прикрепить орбитер к пространственному ряду:

Subspace {
    SpatialRow {
        Orbiter(
            position = OrbiterEdge.Top,
            offset = EdgeOffset.inner(8.dp),
            shape = SpatialRoundedCornerShape(size = CornerSize(50))
        ) {
            Text(
                "Hello World!",
                style = MaterialTheme.typography.h2,
                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 , имеют связанные с ними сущности без содержания. Поэтому орбитер, объявленный в пространственном макете, привязывается к этому макету.

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