Desenvolver a interface com o Jetpack Compose para XR

Com o Jetpack Compose para XR, você pode criar de forma declarativa a interface espacial e o layout usando conceitos conhecidos do Compose, como linhas e colunas. Isso permite estender a interface do Android para o espaço 3D ou criar aplicativos 3D imersivos completamente 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 as visualizações juntos ou trabalhar diretamente com a biblioteca SceneCore. Consulte nosso guia de trabalho com visualizações para mais detalhes.

Sobre subespaços e componentes espaciais

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

Sobre o Subspace

Ao desenvolver para o Android XR, você precisa adicionar um subspace ao app ou layout. Um subspace é uma partição do espaço 3D no seu app em que você pode colocar conteúdo 3D, criar layouts 3D e adicionar profundidade ao conteúdo 2D. Um subespaço é renderizado somente quando a espacialização está ativada. No Espaço da casa ou em dispositivos não XR, qualquer código nesse subspace é ignorado.

Há duas maneiras de criar um subespaço:

  • setSubspaceContent(): essa função cria um subespaço no nível do app. Ele pode ser chamado na atividade principal da mesma forma que você usa setContent(). Um subspace no nível do app não tem limite de altura, largura e profundidade, fornecendo essencialmente uma tela infinita para conteúdo espacial.
  • Subspace: esse elemento combinável pode ser colocado em qualquer lugar na hierarquia da interface do app, permitindo que você mantenha layouts para interfaces 2D e espaciais sem perder o contexto entre os arquivos. Isso facilita o compartilhamento de coisas como a arquitetura de apps existente entre XR e outros formatos sem precisar elevar o estado em toda a árvore de interface ou reprojetar o app.

Para mais informações, consulte Adicionar um subspace ao app.

Sobre os componentes espaciais

Combináveis de subespaço: esses componentes só podem ser renderizados em um subespaço. Elas precisam ser incluídas em Subspace ou setSubspaceContent antes de serem colocadas em um layout 2D. Um SubspaceModifier permite adicionar atributos como profundidade, deslocamento e posicionamento aos elementos combináveis do subespaço.

Outros componentes espaciais não precisam ser chamados em um subspace. Eles consistem em elementos 2D convencionais unidos 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 espaciais 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 o conteúdo do app. Por exemplo, é possível mostrar a reprodução de vídeo, imagens estáticas ou qualquer outro conteúdo em um painel espacial.

Exemplo de 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 um modificador de subespaço móvel funciona

Quando um usuário afasta um painel, por padrão, um modificador de subespaço móvel dimensiona o painel de forma semelhante à maneira como os painéis são redimensionados pelo sistema no espaço inicial. Todo o conteúdo das crianças herda esse comportamento. Para desativar essa opção, defina o parâmetro scaleWithDistance como false.

Criar um orbitador

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

Exemplo de um orbitador

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

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

Pontos principais sobre o código

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

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 abaixo 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

Use um volume para colocar um objeto 3D no layout

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

Exemplo de 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,
            )
        }
    }
}

@Composable
fun ObjectInAVolume(show3DObject: Boolean) {

Informações adicionais

Adicionar uma plataforma 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 o app pode desenhar conteúdo, como uma imagem ou um vídeo. O SpatialExternalSurface aceita 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:

@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 exibida para ambos os olhos.
    • SideBySide: o frame de imagem ou vídeo contém um par de imagens ou frames de vídeo dispostos 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, em que 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ó oferece suporte a superfícies retangulares.
  • Esse Surface não captura eventos de entrada.
  • Não é possível sincronizar as mudanças de StereoMode com a renderização do aplicativo 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 outros componentes de interface espacial

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

Componente da interface

Quando a espacialização está ativada

Em um ambiente 2D

SpatialDialog

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

Volta para 2D Dialog.

SpatialPopup

O painel vai ser empurrado ligeiramente para trás 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 caixa de diálogo que é aberta após um breve atraso. Quando SpatialDialog é usado, a caixa de diálogo aparece na mesma profundidade que o painel espacial, e o painel é empurrado para trás em 125dp quando a espacialização é ativada. SpatialDialog também pode ser usado quando a espacialização não está ativada. Nesse caso, SpatialDialog volta para a versão 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 têm suporte do Compose para XR, você pode trabalhar diretamente com PanelEntities e a cena usando as APIs SceneCore.

Fixar orbitadores em layouts espaciais e outras entidades

É possível ancorar um orbitador a qualquer entidade declarada no Compose. Isso envolve declarar um orbitador em um layout espacial de elementos da interface, como SpatialRow, SpatialColumn ou SpatialBox. O Orbiter é ancorado à entidade pai mais próxima de onde foi declarado.

O comportamento do orbitador é determinado por onde você o declara:

  • Em um layout 2D agrupado em um SpatialPanel (como mostrado em um snippet de código anterior), o orbitador é 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 orbitador a uma linha espacial:

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

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 orbitador é ancorado na parte de cima do SpatialRow em que ele é declarado.
  • Layouts espaciais, como SpatialRow, SpatialColumn, SpatialBox, têm entidades sem conteúdo associadas a eles. Portanto, um orbitador declarado em um layout espacial é ancorado a esse layout.

Veja também