Desenvolver interfaces espaciais com o Jetpack Compose para XR

Dispositivos XR relevantes
Estas orientações ajudam você a criar experiências para esses tipos de dispositivos XR.
Headsets XR
Óculos XR com fio

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

Se você estiver espacializando um app baseado em visualizações do Android, terá várias opções de desenvolvimento. É possível usar APIs de interoperabilidade, usar o Compose e as visualizações 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, você precisará adicionar um subespaço ao app ou layout. Um subespaço é uma partição do espaço 3D no seu aplicativo em que você pode inserir conteúdo 3D, criar layouts 3D e adicionar profundidade ao conteúdo 2D. Um subespaço é renderizado apenas quando a espacialização está ativada. No Espaço Compacto ou em dispositivos não XR, qualquer código nesse subespaço é ignorado.

Há algumas maneiras de criar um subespaço:

  • Subspace: esse elemento combinável cria uma hierarquia de interface espacial nova e independente. Ele não herda a posição espacial, a orientação ou a escala de nenhum Subspace pai em que está aninhado. Subspace é vinculado automaticamente pela caixa de conteúdo recomendada do sistema.
  • PlanarEmbeddedSubspace: esse elemento combinável pode ser colocado na hierarquia da interface do app, permitindo que você mantenha layouts para interfaces 2D e espaciais. PlanarEmbeddedSubspace respeita as restrições e o posicionamento do pai. O conteúdo 3D colocado dentro dele é posicionado em relação a essa área definida em 2D.

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

Sobre componentes espacializados

Elementos combináveis de subespaço: esses componentes só podem ser renderizados em um subespaço. Eles precisam ser incluídos em Subspace antes de serem colocados em um layout 2D. Um SubspaceModifier permite adicionar atributos como profundidade, deslocamento e posicionamento aos elementos combináveis de 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 eles voltam para as contrapartes 2D.

Criar um painel espacial

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

Exemplo de um painel de interface espacial

Você pode usar SubspaceModifier para mudar o tamanho, o comportamento e o posicionamento do painel espacial, conforme mostrado no exemplo a seguir.

enableOnBackInvokedCallback="True"SpatialPanel

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

Principais pontos sobre o código

  • Como as APIs SpatialPanel são elementos combináveis de subespaço, é necessário chamar elas dentro de Subspace. A chamada delas fora de um subespaço gera uma exceção.
  • O tamanho do SpatialPanel foi definido usando as especificações height e width no SubspaceModifier. A omissão dessas especificações permite que o tamanho do painel seja determinado pelas medidas do conteúdo.
  • Permita que o usuário mova um painel adicionando um movable modificador de subespaço.
  • Permita que o usuário redimensione um painel adicionando um resizable modificador de subespaço.
  • Consulte nossas orientações de design de painel espacial para detalhes sobre tamanho e posicionamento. Consulte nossa documentação de referência para mais detalhes sobre a implementação do código.

Como o modificador movable funciona

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

Criar um orbitador

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

Exemplo de um orbiter

Conforme mostrado no exemplo a seguir, chame um orbitador dentro do layout 2D em um SpatialPanel para incluir controles do usuário, como navegação. Ao fazer isso, eles são extraídos do layout 2D e anexados ao painel espacial de acordo com a configuração.

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

Principais pontos sobre o código

  • Como os orbitadores 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.
  • Consulte 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 SpatialSpacer.

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

Principais pontos sobre o código

Adicionar um objeto 3D ao layout usando SpatialGltfModel

O Android XR oferece suporte ao formato glTF para modelos 3D, normalmente salvos como .glb arquivos. Para adicionar esses objetos ao layout, use o SpatialGltfModel combinável. Essa API simplifica o processo de carregamento de recursos e gerenciamento do estado deles.

Para mostrar um modelo, primeiro defina a origem e o estado dele usando rememberSpatialGltfModelState. É possível carregar modelos da pasta assets do app, de um URI ou raw data.

val modelState = rememberSpatialGltfModelState(
    source = SpatialGltfModelSource.fromPath(
        Paths.get("models/model_name.glb")
    )
)

Depois que o estado for definido, use o elemento combinável SpatialGltfModel para renderizá-lo em um subespaço.

SpatialGltfModel(state = modelState, modifier = SubspaceModifier)

Principais pontos sobre o código

  • Carregamento assíncrono: o modelo é carregado de forma assíncrona. Durante a composição inicial, o tamanho intrínseco pode ser zero. O layout é medido novamente quando o modelo está pronto.
  • Estado de controle: use SpatialGltfModelState.status para consultar o status de carregamento ou controlar animações.
  • Dimensionamento e escalonamento: por padrão, o tamanho do layout corresponde à caixa delimitadora do recurso. É possível substituir isso por um SubspaceModifier.size para dimensionar o modelo de maneira uniforme para caber nos limites especificados.

Usar uma SceneCoreEntity para colocar entidades no layout

O elemento combinável SceneCoreEntity conecta as bibliotecas Jetpack SceneCore e Compose para XR para que você possa usar entidades criadas com o SceneCore em layouts do Compose. Isso permite criar entidades de nível inferior e componentes personalizados, permitindo que o Compose dimensione, posicione, redefina o pai, adicione filhos e aplique modificadores a essas entidades.

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

Principais pontos sobre o código

  • Bloco de fábrica: o bloco de fábrica é onde você inicializa a entidade SceneCore subjacente.
  • Bloco de atualização: use o bloco de atualização para modificar as propriedades da entidade em resposta a mudanças no estado do Compose.
  • Adaptação de tamanho: o sizeAdapter comunica as dimensões da entidade de volta ao sistema de layout do Compose.

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 a Surface em que o app pode desenhar conteúdo, como uma imagem ou vídeo. SpatialExternalSurface oferece suporte a 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 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() }
        }
    }
}

Principais pontos 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 visualização do olho esquerdo, e a imagem ou o frame à direita representa a visualizaçã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 visualização do olho esquerdo, e a imagem ou o frame na parte de baixo representa a visualização do olho direito.
  • SpatialExternalSurface oferece suporte apenas a superfícies retangulares.
  • Este Surface não captura eventos de entrada.
  • Não é possível sincronizar StereoMode mudanças 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 uma MovePolicy se houver outros painéis no layout.

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

SpatialExternalSurface também oferece suporte à reprodução de streams de vídeo protegidos por DRM. Para ativar isso, crie uma superfície segura que seja renderizada em buffers gráficos protegidos. Isso impede que o conteúdo seja gravado na tela ou acessado por componentes de sistema não seguros.

Para criar uma superfície segura, defina o SpatialExternalSurfaceProtection parâmetro como SpatialExternalSurfaceProtection.Protected no SpatialExternalSurface elemento combinável. Além disso, é necessário configurar o Exoplayer da Media3 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 stream 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() }
        }
    }
}

Principais pontos sobre o código

  • Superfície protegida: definir surfaceProtection = SpatialExternalSurfaceProtection.Protected em SpatialExternalSurface é essencial para que a Surface subjacente seja apoiada por buffers seguros adequados para conteúdo DRM.
  • Configuração de DRM: é necessário configurar o MediaItem com o esquema de DRM (por exemplo, C.WIDEVINE_UUID) e o URI do servidor de licenças. 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 mostrado 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 de interface espacial

Os componentes de interface espacial podem ser colocados em qualquer lugar na hierarquia da interface do aplicativo. Esses elementos podem ser reutilizados na 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 a necessidade de escrever o código duas vezes. Consulte os exemplos a seguir de interface espacial para entender melhor como usar esses elementos.

Componente da interface

Quando a espacialização está ativada

Em 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 2D Popup.

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

Principais pontos 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 PanelEntity instâncias e o gráfico de cena usando as SceneCore APIs.

Orbitadores de âncora para painéis e layouts espaciais

É possível ancorar um orbitador a SpatialPanels e componentes de layout espacial declarados no Compose. Isso envolve declarar um orbitador em um layout espacial de elementos de interface, como SpatialRow, SpatialColumn ou SpatialBox. O orbitador é ancorado ao pai mais próximo de onde você o declarou.

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

  • Em um layout 2D incluído em um SpatialPanel (conforme 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 = 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)
            )
        }
    }
}

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

Consulte também