Desenvolver a interface com o Jetpack Compose para XR

Com o Jetpack Compose para XR, é possível criar de forma declarativa sua interface e layout espaciais usando conceitos conhecidos do Compose, como linhas e colunas. Isso permite estender sua interface do Android atual para o espaço 3D ou criar aplicativos 3D imersivos totalmente novos.

Se você estiver espacializando um app baseado em Android Views, terá várias opções de desenvolvimento. Você pode usar APIs de interoperabilidade, usar o Compose e o Views juntos ou trabalhar diretamente com a biblioteca SceneCore. Consulte nosso guia para trabalhar com visualizações para mais detalhes.

Sobre subespaços e componentes espacializados

Ao escrever seu app para Android XR, é importante entender os conceitos de subespaço e componentes espacializados.

Sobre o subespaço

Ao desenvolver para Android XR, é necessário adicionar um Subspace ao app ou layout. Um subespaço é uma partição do espaço 3D no seu app em que você pode inserir conteúdo 3D, criar layouts 3D e adicionar profundidade ao conteúdo 2D. Um subespaço só é renderizado quando a espacialização está ativada. No Espaço Compacto ou em dispositivos não XR, qualquer código dentro desse subespaço é ignorado.

Há duas maneiras de criar um subespaço:

  • Subspace: esse elemento combinável pode ser colocado em qualquer lugar na hierarquia da interface do app, permitindo manter layouts para interfaces 2D e espaciais sem perder o contexto entre os arquivos. Isso facilita o compartilhamento de elementos como arquitetura de app entre XR e outros formatos sem precisar elevar o estado por toda a árvore de UI ou reestruturar o app.
  • ApplicationSubspace: essa função cria um subespaço no nível do app e precisa ser colocada no nível mais alto da hierarquia da interface espacial do aplicativo. ApplicationSubspace renderiza conteúdo espacial com VolumeConstraints opcional. Ao contrário de Subspace, ApplicationSubspace não pode ser aninhado em outro Subspace ou ApplicationSubspace.

Para mais informações, consulte Adicionar um subespaço ao seu app.

Sobre componentes espacializados

Combináveis do subespaço: esses componentes só podem ser renderizados em um subespaço. Eles precisam estar dentro de Subspace ou setSubspaceContent() antes de serem colocados em um layout 2D. Um SubspaceModifier permite adicionar atributos como profundidade, deslocamento e posicionamento aos elementos combináveis do subespaço.

Outros componentes espacializados não precisam ser chamados dentro de um subespaço. Eles consistem em elementos 2D convencionais envolvidos em um contêiner espacial. Esses elementos podem ser usados em layouts 2D ou 3D, se definidos para ambos. Quando a espacialização não está ativada, os recursos espacializados são ignorados e voltam para as versões 2D.

Criar um painel espacial

Um SpatialPanel é um elemento combinável de subespaço que permite mostrar conteúdo do app. Por exemplo, você pode exibir reprodução de vídeo, imagens estáticas ou qualquer outro conteúdo em um painel espacial.

Exemplo de um painel de interface espacial

Use SubspaceModifier para mudar o tamanho, o comportamento e o posicionamento do painel espacial, conforme mostrado no exemplo a seguir.

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

Pontos principais sobre o código

  • Como as APIs SpatialPanel são combináveis de subespaço, é necessário chamá-las dentro de Subspace. Chamar esses métodos fora de um subespaço gera uma exceção.
  • O tamanho do SpatialPanel foi definido usando as especificações height e width no SubspaceModifier. Se você omitir essas especificações, o tamanho do painel será determinado pelas medidas do conteúdo.
  • Permita que o usuário redimensione ou mova o painel adicionando os modificadores movable ou resizable.
  • Consulte nossas orientações de design de painel espacial para detalhes sobre dimensionamento e posicionamento. Consulte nossa documentação de referência para mais detalhes sobre a implementação de código.

Como funciona um modificador de subespaço móvel

Quando um usuário move um painel para longe, por padrão, um modificador de subespaço móvel dimensiona o painel de maneira semelhante a como os painéis são redimensionados pelo sistema no espaço inicial. Todo o conteúdo infantil herda esse comportamento. Para desativar isso, defina o parâmetro scaleWithDistance como false.

Criar um orbiter

Um orbitador é um componente espacial da interface. Ele foi projetado para ser anexado a um painel espacial, layout ou outra entidade correspondente. Um orbiter geralmente contém navegação e itens de ação contextual relacionados à entidade a que está ancorado. Por exemplo, se você criou um painel espacial para mostrar conteúdo de vídeo, é possível adicionar controles de reprodução de vídeo dentro de um orbiter.

Exemplo de um orbiter

Como mostrado no exemplo a seguir, chame um orbiter dentro do layout 2D em um SpatialPanel para encapsular controles do usuário, como navegação. Isso extrai os elementos do layout 2D e os anexa ao painel espacial de acordo com sua configuração.

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

Pontos principais sobre o código

  • Como os orbitadores são componentes espaciais da interface, o código pode ser reutilizado em layouts 2D ou 3D. Em um layout 2D, o app renderiza apenas o conteúdo dentro do orbiter e ignora o próprio orbiter.
  • Confira nossas orientações de design para mais informações sobre como usar e criar orbitais.

Adicionar vários painéis espaciais a um layout espacial

É possível criar vários painéis espaciais e colocá-los em um layout espacial usando SpatialRow, SpatialColumn, SpatialBox e SpatialLayoutSpacer.

Exemplo de vários painéis espaciais em um layout espacial

O exemplo de código a seguir mostra como fazer isso.

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

Pontos principais sobre o código

Usar um volume para colocar um objeto 3D no layout

Para colocar um objeto 3D no seu layout, use um elemento combinável de subespaço chamado de volume. Confira um exemplo de como fazer isso.

Exemplo de um objeto 3D em um layout

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) {

Informações adicionais

Adicionar uma superfície para conteúdo de imagem ou vídeo

Um SpatialExternalSurface é um elemento combinável de subespaço que cria e gerencia o Surface em que seu app pode mostrar conteúdo, como uma imagem ou um vídeo. O SpatialExternalSurface é compatível com conteúdo estereoscópico ou monoscópico.

Este exemplo demonstra como carregar vídeos estereoscópicos lado a lado usando o ExoPlayer da Media3 e o 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() }
        }
    }
}

Pontos principais sobre o código

  • Defina StereoMode como Mono, SideBySide ou TopBottom, dependendo do tipo de conteúdo que você está renderizando:
    • Mono: o frame de imagem ou vídeo consiste em uma única imagem idêntica mostrada aos dois olhos.
    • SideBySide: o frame de imagem ou vídeo contém um par de imagens ou frames de vídeo organizados lado a lado, em que a imagem ou o frame à esquerda representa a visão do olho esquerdo, e a imagem ou o frame à direita representa a visão do olho direito.
    • TopBottom: o frame de imagem ou vídeo contém um par de imagens ou frames de vídeo empilhados verticalmente. A imagem ou o frame na parte de cima representa a visão do olho esquerdo, e a imagem ou o frame na parte de baixo representa a visão do olho direito.
  • O SpatialExternalSurface só é compatível com superfícies retangulares.
  • Esse Surface não captura eventos de entrada.
  • Não é possível sincronizar mudanças de StereoMode com a renderização de aplicativos ou a decodificação de vídeo.
  • Esse elemento combinável não pode ser renderizado na frente de outros painéis. Portanto, não use modificadores móveis se houver outros painéis no layout.

Adicionar uma plataforma para conteúdo de vídeo protegido por DRM

O SpatialExternalSurface também é compatível com a reprodução de streams de vídeo protegidos por DRM. Para ativar esse recurso, crie uma superfície segura que renderize para buffers gráficos protegidos. Isso impede que o conteúdo seja gravado na tela ou acessado por componentes do sistema não seguros.

Para criar uma superfície segura, defina o parâmetro surfaceProtection como SurfaceProtection.Protected no elemento combinável SpatialExternalSurface. Além disso, configure o Media3 Exoplayer com as informações de DRM adequadas para processar a aquisição de licenças de um servidor de licenças.

O exemplo a seguir demonstra como configurar SpatialExternalSurface e ExoPlayer para reproduzir um fluxo de vídeo protegido por 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() }
        }
    }
}

Pontos principais sobre o código

  • Superfície protegida: definir surfaceProtection = SurfaceProtection.Protected em SpatialExternalSurface é essencial para que o Surface subjacente seja apoiado por buffers seguros adequados para conteúdo de DRM.
  • Configuração de DRM: você precisa configurar o MediaItem com o esquema de DRM (por exemplo, C.WIDEVINE_UUID) e o URI do servidor de licença. O ExoPlayer usa essas informações para gerenciar a sessão de DRM.
  • Conteúdo seguro: ao renderizar em uma superfície protegida, o conteúdo de vídeo é decodificado e exibido em um caminho seguro, o que ajuda a atender aos requisitos de licenciamento de conteúdo. Isso também impede que o conteúdo apareça em capturas de tela.

Adicionar outros componentes espaciais da interface

Os componentes de UI espacial podem ser colocados em qualquer lugar na hierarquia da interface do aplicativo. Esses elementos podem ser reutilizados na sua interface 2D, e os atributos espaciais só ficam visíveis quando os recursos espaciais estão ativados. Isso permite adicionar elevação a menus, caixas de diálogo e outros componentes sem precisar escrever seu código duas vezes. Confira os exemplos a seguir de interface espacial para entender melhor como usar esses elementos.

Componente da interface

Quando a espacialização está ativada

Em um ambiente 2D

SpatialDialog

O painel vai recuar um pouco na profundidade Z para mostrar uma caixa de diálogo elevada.

Volta para 2D Dialog.

SpatialPopup

O painel vai recuar um pouco na profundidade Z para mostrar um pop-up elevado.

Volta para um Popup 2D.

SpatialElevation

SpatialElevationLevel pode ser definido para adicionar elevação.

Mostra sem elevação espacial.

SpatialDialog

Este é um exemplo de uma caixa de diálogo que é aberta após um pequeno atraso. Quando SpatialDialog é usado, a caixa de diálogo aparece na mesma profundidade z do painel espacial, e o painel é empurrado para trás em 125 dp quando a espacialização está ativada. SpatialDialog também pode ser usado quando a espacialização não está ativada. Nesse caso, SpatialDialog volta para a contraparte 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")
                }
            }
        }
    }
}

Pontos principais sobre o código

Criar painéis e layouts personalizados

Para criar painéis personalizados que não são compatíveis com o Compose para XR, trabalhe diretamente com instâncias PanelEntity e o gráfico de cena usando as APIs SceneCore.

Ancorar orbitadores a layouts espaciais e outras entidades

É possível fixar um orbiter a qualquer entidade declarada no Compose. Isso envolve declarar um orbiter em um layout espacial de elementos da interface, como SpatialRow, SpatialColumn ou SpatialBox. O orbiter se ancora na entidade principal mais próxima de onde você o declarou.

O comportamento do orbiter é determinado pelo local em que você o declara:

  • Em um layout 2D envolvido em um SpatialPanel (como mostrado em um snippet de código anterior), o orbiter é ancorado a esse SpatialPanel.
  • Em um Subspace, o orbitador é ancorado à entidade pai mais próxima, que é o layout espacial em que o orbitador é declarado.

O exemplo a seguir mostra como ancorar um orbiter a uma linha espacial:

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

Pontos principais sobre o código

  • Quando você declara um orbitador fora de um layout 2D, ele é ancorado à entidade pai mais próxima. Nesse caso, o orbiter se ancora na parte de cima do SpatialRow em que ele é declarado.
  • Layouts espaciais, como SpatialRow, SpatialColumn e SpatialBox, têm entidades sem conteúdo associadas a eles. Portanto, um orbiter declarado em um layout espacial é ancorado a ele.

Veja também