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 comVolumeConstraints
opcional. Ao contrário deSubspace
,ApplicationSubspace
não pode ser aninhado em outroSubspace
ouApplicationSubspace
.
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.
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 deSubspace
. Chamar esses métodos fora de um subespaço gera uma exceção. - O tamanho do
SpatialPanel
foi definido usando as especificaçõesheight
ewidth
noSubspaceModifier
. 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
ouresizable
. - 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.
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
.
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
SpatialRow
,SpatialColumn
,SpatialBox
eSpatialLayoutSpacer
são todos elementos combináveis de subespaço e precisam ser colocados em um subespaço.- Use
SubspaceModifier
para personalizar seu layout. - Para layouts com vários painéis em uma linha, recomendamos definir um raio de curva de 825 dp usando um
SubspaceModifier
para que os painéis envolvam o usuário. Consulte nossas orientações de design para mais detalhes.
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.
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
- Consulte Adicionar modelos 3D ao seu app para entender melhor como carregar conteúdo 3D em um volume.
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
comoMono
,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 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
emSpatialExternalSurface
é essencial para que oSurface
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 |
---|---|---|
|
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 |
|
|
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
- Este é um exemplo de
SpatialDialog
. UsarSpatialPopup
eSpatialElevation
é muito parecido. 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 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 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 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
eSpatialBox
, têm entidades sem conteúdo associadas a eles. Portanto, um orbiter declarado em um layout espacial é ancorado a ele.
Veja também
- Adicionar modelos 3D ao app
- Desenvolver UI para apps baseados em visualizações do Android
- Implementar o Material Design para XR