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 nenhumSubspacepai 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.PlanarEmbeddedSubspacerespeita 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.

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
SpatialPanelsão elementos combináveis de subespaço, é necessário chamar elas dentro deSubspace. A chamada delas fora de um subespaço gera uma exceção. - O tamanho do
SpatialPanelfoi definido usando as especificaçõesheightewidthnoSubspaceModifier. 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
movablemodificador de subespaço. - Permita que o usuário redimensione um painel adicionando um
resizablemodificador 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.

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.

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
SpatialRow,SpatialColumn,SpatialBoxeSpatialSpacersão todos elementos combináveis de subespaço e precisam ser colocados em um subespaço.- Use
SubspaceModifierpara personalizar o layout. - Para layouts com vários painéis em uma linha, recomendamos definir um raio de curva de 825 dp usando um
SubspaceModifierpara que os painéis envolvam o usuário. Consulte nossas orientações de design para detalhes.
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.statuspara 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.sizepara 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
SceneCoresubjacente. - 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
sizeAdaptercomunica as dimensões da entidade de volta ao sistema de layout do Compose.
Informações adicionais
- Consulte Adicionar modelos 3D ao app para entender melhor como carregar conteúdo 3D
em uma
SceneCoreEntity.
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
StereoModecomoMono,SideBySide, ouTopBottom, 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.
SpatialExternalSurfaceoferece suporte apenas a superfícies retangulares.- Este
Surfacenão captura eventos de entrada. - Não é possível sincronizar
StereoModemudanç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
MovePolicyse 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.ProtectedemSpatialExternalSurfaceé essencial para que aSurfacesubjacente seja apoiada por buffers seguros adequados para conteúdo DRM. - Configuração de DRM: é necessário configurar o
MediaItemcom 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 |
|---|---|---|
|
O painel vai recuar um pouco na profundidade z para mostrar uma caixa de diálogo elevada |
Volta para 2D |
|
O painel vai recuar um pouco na profundidade z para mostrar um pop-up elevado |
Volta para um 2D |
|
|
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
- Este é um exemplo de
SpatialDialog. O uso deSpatialPopupeSpatialElevationé muito semelhante. Consulte nossa referência da API para mais detalhes.
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 esseSpatialPanel. - 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
SpatialRowem que é declarado. - Layouts espaciais, como
SpatialRow,SpatialColumneSpatialBox, têm entidades sem conteúdo associadas a eles. Portanto, um orbitador declarado em um layout espacial é ancorado a esse layout.
Consulte também
- Adicionar modelos 3D ao app
- Desenvolver interface para aplicativos baseados em visualizações do Android
- Implementar o Material Design para XR