Compartilhar via


Tutorial: Instruções passo a passo para criar um novo aplicativo Android usando Âncoras Espaciais do Azure

Este tutorial mostra como criar um novo aplicativo Android que se integra à funcionalidade de ARCore com Âncoras Espaciais do Azure.

Pré-requisitos

Para concluir este tutorial, verifique se você tem:

Introdução

Inicie o Android Studio. Na janela Bem-vindo ao Android Studio, clique em Iniciar um novo projeto do Android Studio.

  1. Selecione Arquivo->Novo Projeto.
  2. Na janela Criar Novo Projeto, na seção Telefone e Tablet, escolha Atividade Vazia e clique em Avançar.
  3. Em Novo Projeto, na janela Atividade Vazia, altere os seguintes valores:
    • Altere o Nome, Nome do pacote e Localização para salvar para os valores desejados.
    • Defina a Linguagem como Java.
    • Defina o Nível de API mínimo como API 26: Android 8.0 (Oreo).
    • Deixe as outras opções como estão.
    • Clique em Concluir.
  4. O Instalador do Componente será executado. Depois de algum processamento, o Android Studio abrirá o IDE.

Android Studio: novo projeto

Experimentá-lo

Para testar seu novo aplicativo, conecte o dispositivo de desenvolvedor habilitado para seu computador de desenvolvimento com um cabo USB. No canto superior direito do Android Studio selecione seu dispositivo conectado e selecione o ícone Executar «aplicativo» . Android Studio instala o aplicativo no dispositivo conectado e o inicia. Agora você deve ver "Olá, Mundo!" exibido no aplicativo em execução no seu dispositivo. Clique em Executar->Interromper 'aplicativo'. Android Studio: execução

Integração do ARCore

O ARCore é a plataforma do Google para a criação de experiências de realidade aumentada, permitindo que seu dispositivo controle sua própria posição conforme ele se move e cria seu próprio entendimento do mundo real.

Modifique app\manifests\AndroidManifest.xml para incluir as entradas a seguir dentro do nó <manifest> da raiz. Este snippet de código faz algumas coisas:

  • Ele permitirá que seu aplicativo acesse sua câmera do dispositivo.
  • Ele também garantirá que seu aplicativo só seja visível no Google Play Store para dispositivos que deem suporte a ARCore.
  • Ele também configurará o Google Play Store para baixar e instalar o ARCore, se ainda não estiver instalado, no ato da instalação do aplicativo.
<manifest ...>

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera.ar" />

    <application>
        ...
        <meta-data android:name="com.google.ar.core" android:value="required" />
        ...
    </application>

</manifest>

Modifique Gradle Scripts\build.gradle (Module: app) para incluir a entrada a seguir. Esse código assegurará que o aplicativo tenha o ARCore versão 1.25 como destino. Após essa alteração, você poderá receber uma notificação do Gradle solicitando que você sincronize: clique em Sincronizar agora.

dependencies {
    ...
    implementation 'com.google.ar:core:1.25.0'
    ...
}

Integração do Sceneform

O Sceneform simplifica a renderização de cenas 3D realistas em aplicativos de realidade aumentada, sem precisar aprender OpenGL.

Modifique Gradle Scripts\build.gradle (Module: app) para incluir as entradas a seguir. Este código permitirá que seu aplicativo use constructos de linguagem do Java 8, exigidos pelo Sceneform. Ele também garantirá que seu aplicativo seja destinado à versão 1.15 do Sceneform. Após essa alteração, você poderá receber uma notificação do Gradle solicitando que você sincronize: clique em Sincronizar agora.

android {
    ...

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    ...
    implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.15.0'
    ...
}

Abra seu app\res\layout\activity_main.xml e substitua o elemento <TextView ... /> de Olá Mundo existente com o seguinte ArFragment. Esse código fará com que o feed da câmera seja exibido na tela, permitindo que o ARCore acompanhe a posição do dispositivo conforme ele se move.

<fragment android:name="com.google.ar.sceneform.ux.ArFragment"
    android:id="@+id/ux_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Observação

Para ver o xml bruto da atividade principal, selecione o botão "Código" ou "Dividir" no canto superior direito do Android Studio.

Reimplante seu aplicativo no dispositivo para validá-lo mais uma vez. Dessa vez, deve ser solicitado que você forneça permissões de câmera. Após a aprovação, você deverá ver a renderização do feed da câmera na tela.

Colocar um objeto no mundo real

Vamos criar e dispor um objeto usando o aplicativo. Primeiro, adicione as seguintes importações ao seu app\java\<PackageName>\MainActivity:

import com.google.ar.core.HitResult;
import com.google.ar.core.Plane;
import com.google.ar.sceneform.AnchorNode;
import com.google.ar.sceneform.math.Vector3;
import com.google.ar.sceneform.rendering.Color;
import com.google.ar.sceneform.rendering.MaterialFactory;
import com.google.ar.sceneform.rendering.Renderable;
import com.google.ar.sceneform.rendering.ShapeFactory;
import com.google.ar.sceneform.ux.ArFragment;

import android.view.MotionEvent;

Em seguida, adicione as seguintes variáveis de membro na classe MainActivity:

private boolean tapExecuted = false;
private final Object syncTaps = new Object();
private ArFragment arFragment;
private AnchorNode anchorNode;
private Renderable nodeRenderable = null;
private float recommendedSessionProgress = 0f;

Depois, adicione o código a seguir ao método app\java\<PackageName>\MainActivity onCreate(). Esse código conectará um ouvinte chamado handleTap(), que detectará quando o usuário toca a tela em seu dispositivo. Se o toque ocorrer em uma superfície do mundo real que já tiver sido reconhecida pelo acompanhamento do ARCore, o ouvinte será executado.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    this.arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);
    this.arFragment.setOnTapArPlaneListener(this::handleTap);
}

Por fim, adicione o método handleTap() a seguir, que juntará tudo. Ele criará uma esfera e a colocará na localização tocada. A esfera inicialmente será preta, já que this.recommendedSessionProgress está definido como zero no momento. Esse valor será ajustado posteriormente.

protected void handleTap(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
    synchronized (this.syncTaps) {
        if (this.tapExecuted) {
            return;
        }

        this.tapExecuted = true;
    }

    this.anchorNode = new AnchorNode();
    this.anchorNode.setAnchor(hitResult.createAnchor());

    MaterialFactory.makeOpaqueWithColor(this, new Color(
            this.recommendedSessionProgress,
            this.recommendedSessionProgress,
            this.recommendedSessionProgress))
            .thenAccept(material -> {
                this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), material);
                this.anchorNode.setRenderable(nodeRenderable);
                this.anchorNode.setParent(arFragment.getArSceneView().getScene());
            });
}

Reimplante seu aplicativo no dispositivo para validá-lo mais uma vez. Neste momento, você pode mover em torno de seu dispositivo para fazer com que o ARCore comece a reconhecer o seu ambiente. Em seguida, toque na tela para criar e colocar seu círculo preto sobre a superfície de sua escolha.

Anexar uma âncora espacial do Azure local

Modifique Gradle Scripts\build.gradle (Module: app) para incluir a entrada a seguir. Este snippet de código de exemplo tem como destino o SDK das Âncoras Espaciais do Azure versão 2.10.2. Observe que o SDK versão 2.7.0 é atualmente a versão mínima com suporte e fazer referência a qualquer versão mais recente das Âncoras Espaciais do Azure também deve funcionar. É recomendável usar a versão mais recente do SDK de Âncoras Espaciais do Azure. Você pode encontrar as notas sobre a versão do SDK aqui.

dependencies {
    ...
    implementation 'com.microsoft.azure.spatialanchors:spatialanchors_jni:[2.10.2]'
    implementation 'com.microsoft.azure.spatialanchors:spatialanchors_java:[2.10.2]'
    ...
}

Se tiver como objetivo o SDK de Âncoras Espaciais do Azure 2.10.0 ou posterior, inclua a entrada a seguir na seção repositórios do arquivo settings.gradle do projeto. Isso incluirá a URL para o feed de pacotes Maven que hospeda pacotes do Android das Âncoras Espaciais do Azure para o SDK 2.10.0 ou posterior:

dependencyResolutionManagement {
    ...
    repositories {
        ...
        maven {
            url 'https://pkgs.dev.azure.com/aipmr/MixedReality-Unity-Packages/_packaging/Maven-packages/maven/v1'
        }
        ...
    }
}

Clique com o botão direito do mouse em app\java\<PackageName>->Nova->Classe Java. Defina o Nome como MyFirstApp e selecione Classe. Um arquivo chamado MyFirstApp.java será criado. Adicione a importação a seguir a ele:

import com.microsoft.CloudServices;

Defina android.app.Application como sua superclasse.

public class MyFirstApp extends android.app.Application {...

Depois, adicione o código a seguir dentro da nova classe MyFirstApp, o que assegura que as Âncoras Espaciais do Azure sejam inicializadas com o contexto do aplicativo.

    @Override
    public void onCreate() {
        super.onCreate();
        CloudServices.initialize(this);
    }

Agora, modifique app\manifests\AndroidManifest.xml para incluir a entrada a seguir dentro do nó <application> da raiz. Esse código se conectará à classe Aplicativo que você criou em seu aplicativo.

    <application
        android:name=".MyFirstApp"
        ...
    </application>

De volta em app\java\<PackageName>\MainActivity, adicione as seguintes importações a ele:

import android.view.MotionEvent;
import android.util.Log;

import com.google.ar.sceneform.ArSceneView;
import com.google.ar.sceneform.Scene;
import com.microsoft.azure.spatialanchors.CloudSpatialAnchor;
import com.microsoft.azure.spatialanchors.CloudSpatialAnchorSession;
import com.microsoft.azure.spatialanchors.SessionLogLevel;

Em seguida, adicione as seguintes variáveis de membro na classe MainActivity:

private float recommendedSessionProgress = 0f;

private ArSceneView sceneView;
private CloudSpatialAnchorSession cloudSession;
private boolean sessionInitialized = false;

Em seguida, adicionaremos o método initializeSession() a seguir dentro da classe mainActivity. Quando chamado, ele garantirá que uma sessão das Âncoras Espaciais do Azure seja criada e inicializada corretamente durante a inicialização do aplicativo. Esse código garante que a sessão sceneview passada para a sessão ASA por meio da chamada cloudSession.setSession não seja nula por ter o retorno antecipado.

private void initializeSession() {
    if (sceneView.getSession() == null) {
        //Early return if the ARCore Session is still being set up
        return;
    }

    if (this.cloudSession != null) {
        this.cloudSession.close();
    }
    this.cloudSession = new CloudSpatialAnchorSession();
    this.cloudSession.setSession(sceneView.getSession());
    this.cloudSession.setLogLevel(SessionLogLevel.Information);
    this.cloudSession.addOnLogDebugListener(args -> Log.d("ASAInfo", args.getMessage()));
    this.cloudSession.addErrorListener(args -> Log.e("ASAError", String.format("%s: %s", args.getErrorCode().name(), args.getErrorMessage())));

    sessionInitialized = true;
}

Como initializeSession() pode fazer um retorno antecipado se a sessão sceneView ainda não estiver configurada (ou seja, se sceneView.getSession() for nulo), adicionaremos uma chamada onUpdate para garantir que a sessão ASA seja inicializada depois que a sessão sceneView for criada.

private void scene_OnUpdate(FrameTime frameTime) {
    if (!sessionInitialized) {
        //retry if initializeSession did an early return due to ARCore Session not yet available (i.e. sceneView.getSession() == null)
        initializeSession();
    }
}

Agora, vamos conectar o método initializeSession() e o método scene_OnUpdate(...) ao seu método onCreate(). Além disso, nós garantiremos que quadros do feed da câmera sejam enviados ao SDK das Âncoras Espaciais do Azure para processamento.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    this.arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);
    this.arFragment.setOnTapArPlaneListener(this::handleTap);

    this.sceneView = arFragment.getArSceneView();
    Scene scene = sceneView.getScene();
    scene.addOnUpdateListener(frameTime -> {
        if (this.cloudSession != null) {
            this.cloudSession.processFrame(sceneView.getArFrame());
        }
    });
    scene.addOnUpdateListener(this::scene_OnUpdate);
    initializeSession();
}

Por fim, adicione o código a seguir ao método handleTap(). Ele anexará uma âncora espacial do Azure local à esfera preta que estamos colocando no mundo real.

protected void handleTap(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
    synchronized (this.syncTaps) {
        if (this.tapExecuted) {
            return;
        }

        this.tapExecuted = true;
    }

    this.anchorNode = new AnchorNode();
    this.anchorNode.setAnchor(hitResult.createAnchor());
    CloudSpatialAnchor cloudAnchor = new CloudSpatialAnchor();
    cloudAnchor.setLocalAnchor(this.anchorNode.getAnchor());

    MaterialFactory.makeOpaqueWithColor(this, new Color(
            this.recommendedSessionProgress,
            this.recommendedSessionProgress,
            this.recommendedSessionProgress))
            .thenAccept(material -> {
                this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), material);
                this.anchorNode.setRenderable(nodeRenderable);
                this.anchorNode.setParent(arFragment.getArSceneView().getScene());
            });
}

Reimplante o aplicativo mais uma vez. Mova seu dispositivo, toque na tela e posicione uma esfera preta. Neste momento, no entanto, seu código criará e anexará uma âncora espacial do Azure local à esfera.

Antes de avançar, será necessário criar uma conta das Âncoras Espaciais do Azure para obter o Identificador, a Chave e o Domínio da conta, caso você ainda não os tenha. Siga a seção a seguir para obtê-los.

Criar um recurso Âncoras Espaciais

Vá para o Portal do Azure.

No painel esquerdo, clique em Criar um recurso.

Use a caixa de pesquisa para Âncoras Espaciais.

Uma captura de tela mostrando os resultados de uma pesquisa de Âncoras Espaciais.

Selecione Âncoras Espaciais e clique em Criar.

No painel Conta de Âncoras Espaciais, faça o seguinte:

  • Insira um nome de recurso exclusivo usando caracteres alfanuméricos normais.

  • Selecione a assinatura que você deseja anexar o recurso.

  • Crie um grupo de recursos, selecionando Criar novo. Nomeie-o myResourceGroup e clique em OK.

    Um grupo de recursos é um contêiner lógico no qual os recursos do Azure, como aplicativos Web, bancos de dados e contas de armazenamento, são implantados e gerenciados. Por exemplo, é possível excluir posteriormente todo o grupo de recursos com uma única etapa simples.

  • Selecione um local (região) para criar o recurso.

  • Selecione Criar para começar a criar o recurso.

Uma captura de tela do painel de Âncoras Espaciais para a criação de um recurso.

Depois que o recurso for criado, o portal do Azure mostrará que a implantação foi concluída.

Uma captura de tela mostrando que a implantação do recurso foi concluída.

Selecione Ir para o recurso. Agora você pode ver as propriedades do recurso.

Copie o valor da ID da Conta do recurso em um editor de texto para usar posteriormente.

Uma captura de tela do painel de propriedades do recurso.

Copie também o valor do Domínio de Contas do recurso em um editor de texto para usar posteriormente.

Uma captura de tela mostrando o valor do domínio de contas do recurso.

Em Configurações, selecione Chave de Acesso. Copie o valor da Chave primária, Chave de Conta, em um editor de texto para usar posteriormente.

Uma captura de tela do painel Chaves da conta.

Carregar sua âncora local na nuvem

Depois que você tiver o Identificador, a Chave e o Domínio de conta das Âncoras Espaciais do Azure, poderemos voltar para app\java\<PackageName>\MainActivity e adicionar as seguintes importações a ela:

import com.microsoft.azure.spatialanchors.SessionLogLevel;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

Em seguida, adicione as seguintes variáveis de membro na classe MainActivity:

private boolean sessionInitialized = false;

private String anchorId = null;
private boolean scanningForUpload = false;
private final Object syncSessionProgress = new Object();
private ExecutorService executorService = Executors.newSingleThreadExecutor();

Agora, adicione o código a seguir ao método initializeSession(). Primeiro, este código permitirá que seu aplicativo monitore o progresso que o SDK das Âncoras Espaciais do Azure faz conforme ele coleta quadros do feed da câmera. Conforme ele o faz, a cor de sua esfera começará a se alterar de seu preto original para cinza. Em seguida, ela se tornará branca depois que quadros suficientes tiverem sido coletados para enviar sua âncora para a nuvem. Em segundo lugar, esse código fornecerá as credenciais necessárias para se comunicar com o back-end de nuvem. É aqui que você vai configurar seu aplicativo para usar o Identificador, a Chave e o Domínio de conta. Você os copiou em um editor de texto ao configurar o recurso Âncoras Espaciais.

private void initializeSession() {
    if (sceneView.getSession() == null) {
        //Early return if the ARCore Session is still being set up
        return;
    }

    if (this.cloudSession != null) {
        this.cloudSession.close();
    }
    this.cloudSession = new CloudSpatialAnchorSession();
    this.cloudSession.setSession(sceneView.getSession());
    this.cloudSession.setLogLevel(SessionLogLevel.Information);
    this.cloudSession.addOnLogDebugListener(args -> Log.d("ASAInfo", args.getMessage()));
    this.cloudSession.addErrorListener(args -> Log.e("ASAError", String.format("%s: %s", args.getErrorCode().name(), args.getErrorMessage())));

    sessionInitialized = true;

    this.cloudSession.addSessionUpdatedListener(args -> {
        synchronized (this.syncSessionProgress) {
            this.recommendedSessionProgress = args.getStatus().getRecommendedForCreateProgress();
            Log.i("ASAInfo", String.format("Session progress: %f", this.recommendedSessionProgress));
            if (!this.scanningForUpload) {
                return;
            }
        }

        runOnUiThread(() -> {
            synchronized (this.syncSessionProgress) {
                MaterialFactory.makeOpaqueWithColor(this, new Color(
                        this.recommendedSessionProgress,
                        this.recommendedSessionProgress,
                        this.recommendedSessionProgress))
                        .thenAccept(material -> {
                            this.nodeRenderable.setMaterial(material);
                        });
            }
        });
    });

    this.cloudSession.getConfiguration().setAccountId(/* Copy your account Identifier in here */);
    this.cloudSession.getConfiguration().setAccountKey(/* Copy your account Key in here */);
    this.cloudSession.getConfiguration().setAccountDomain(/* Copy your account Domain in here */);
    this.cloudSession.start();
}

Em seguida, adicione o método uploadCloudAnchorAsync() a seguir dentro da classe mainActivity. Quando chamado, esse método aguardará assincronamente até que quadros suficientes tenham sido coletados do seu dispositivo. Assim que isso acontecer, ele mudará a cor de sua esfera para amarelo e, em seguida, iniciará o upload de sua âncora espacial do Azure local para a nuvem. Depois que o upload for concluído, o código retornará um identificador da âncora.

private CompletableFuture<String> uploadCloudAnchorAsync(CloudSpatialAnchor anchor) {
    synchronized (this.syncSessionProgress) {
        this.scanningForUpload = true;
    }


    return CompletableFuture.runAsync(() -> {
        try {
            float currentSessionProgress;
            do {
                synchronized (this.syncSessionProgress) {
                    currentSessionProgress = this.recommendedSessionProgress;
                }
                if (currentSessionProgress < 1.0) {
                    Thread.sleep(500);
                }
            }
            while (currentSessionProgress < 1.0);

            synchronized (this.syncSessionProgress) {
                this.scanningForUpload = false;
            }
            runOnUiThread(() -> {
                MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.YELLOW))
                        .thenAccept(yellowMaterial -> {
                            this.nodeRenderable.setMaterial(yellowMaterial);
                        });
            });

            this.cloudSession.createAnchorAsync(anchor).get();
        } catch (InterruptedException | ExecutionException e) {
            Log.e("ASAError", e.toString());
            throw new RuntimeException(e);
        }
    }, executorService).thenApply(ignore -> anchor.getIdentifier());
}

Por fim, vamos interligar tudo. Em seu método handleTap(), adicione o código a seguir. Ele invocará o método uploadCloudAnchorAsync() assim que a esfera for criada. Depois que o método retornar, o código a seguir executará uma atualização final à esfera, alterando sua cor para azul.

protected void handleTap(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
    synchronized (this.syncTaps) {
        if (this.tapExecuted) {
            return;
        }

        this.tapExecuted = true;
    }

    this.anchorNode = new AnchorNode();
    this.anchorNode.setAnchor(hitResult.createAnchor());
    CloudSpatialAnchor cloudAnchor = new CloudSpatialAnchor();
    cloudAnchor.setLocalAnchor(this.anchorNode.getAnchor());

    MaterialFactory.makeOpaqueWithColor(this, new Color(
            this.recommendedSessionProgress,
            this.recommendedSessionProgress,
            this.recommendedSessionProgress))
            .thenAccept(material -> {
                this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), material);
                this.anchorNode.setRenderable(nodeRenderable);
                this.anchorNode.setParent(arFragment.getArSceneView().getScene());
            });


    uploadCloudAnchorAsync(cloudAnchor)
            .thenAccept(id -> {
                this.anchorId = id;
                Log.i("ASAInfo", String.format("Cloud Anchor created: %s", this.anchorId));
                runOnUiThread(() -> {
                    MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.BLUE))
                            .thenAccept(blueMaterial -> {
                                this.nodeRenderable.setMaterial(blueMaterial);
                                synchronized (this.syncTaps) {
                                    this.tapExecuted = false;
                                }
                            });
                });
            });
}

Reimplante o aplicativo mais uma vez. Mova seu dispositivo, toque na tela e posicione a esfera. Neste momento, no entanto, a cor da esfera mudará de preto para branco, conforme os quadros da câmera forem coletados. Assim que tivermos quadros suficientes, a esfera ficará amarela e o upload para a nuvem começará. Certifique-se de que o telefone está conectado à Internet. Depois que o upload for concluído, a esfera ficará azul. Opcionalmente, você pode monitorar a janela Logcat no Android Studio para ver as mensagens de log que o aplicativo está enviando. Exemplos de mensagens que seriam registradas incluem o progresso da sessão durante a captura de quadros e o identificador de âncora que a nuvem retorna após a conclusão do upload.

Observação

Se você não estiver vendo o valor de recommendedSessionProgress (em seus logs de depuração chamados de Session progress) mudar, verifique se você está movendo e girando seu telefone em torno da esfera que você colocou.

Localizar a âncora espacial de nuvem

Após a âncora ser carregada para a nuvem, estamos prontos para tentar localizá-la novamente. Primeiro, vamos adicionar as importações a seguir ao seu código.

import java.util.concurrent.Executors;

import com.microsoft.azure.spatialanchors.AnchorLocateCriteria;
import com.microsoft.azure.spatialanchors.LocateAnchorStatus;

Depois, vamos adicionar o código a seguir ao método handleTap(). Esse código vai:

  • Remover nossa esfera azul existente da tela.
  • Inicializar nossa sessão de Âncoras Espaciais do Azure novamente. Esta ação assegurará que a âncora que vamos localizar venha da nuvem em vez de provir da âncora de local que criamos.
  • Emita uma consulta para a âncora que é carregada para a nuvem.
protected void handleTap(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
    synchronized (this.syncTaps) {
        if (this.tapExecuted) {
            return;
        }

        this.tapExecuted = true;
    }

    if (this.anchorId != null) {
        this.anchorNode.getAnchor().detach();
        this.anchorNode.setParent(null);
        this.anchorNode = null;
        initializeSession();
        AnchorLocateCriteria criteria = new AnchorLocateCriteria();
        criteria.setIdentifiers(new String[]{this.anchorId});
        cloudSession.createWatcher(criteria);
        return;
    }

    this.anchorNode = new AnchorNode();
    this.anchorNode.setAnchor(hitResult.createAnchor());
    CloudSpatialAnchor cloudAnchor = new CloudSpatialAnchor();
    cloudAnchor.setLocalAnchor(this.anchorNode.getAnchor());

    MaterialFactory.makeOpaqueWithColor(this, new Color(
            this.recommendedSessionProgress,
            this.recommendedSessionProgress,
            this.recommendedSessionProgress))
            .thenAccept(material -> {
                this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), material);
                this.anchorNode.setRenderable(nodeRenderable);
                this.anchorNode.setParent(arFragment.getArSceneView().getScene());
            });


    uploadCloudAnchorAsync(cloudAnchor)
            .thenAccept(id -> {
                this.anchorId = id;
                Log.i("ASAInfo", String.format("Cloud Anchor created: %s", this.anchorId));
                runOnUiThread(() -> {
                    MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.BLUE))
                            .thenAccept(blueMaterial -> {
                                this.nodeRenderable.setMaterial(blueMaterial);
                                synchronized (this.syncTaps) {
                                    this.tapExecuted = false;
                                }
                            });
                });
            });
}

Agora, vamos conectar o código que será invocado quando a âncora que estamos consultando for localizada. Em seu método initializeSession(), adicione o código a seguir. Este snippet de código criará uma esfera verde e a posicionará quando a âncora espacial de nuvem for localizada. Ele também habilitará o toque na tela novamente, portanto, você poderá repetir o cenário completo mais uma vez: criar outra âncora local, fará o upload dela e a localizará novamente.

private void initializeSession() {
    if (sceneView.getSession() == null) {
        //Early return if the ARCore Session is still being set up
        return;
    }

    if (this.cloudSession != null) {
        this.cloudSession.close();
    }
    this.cloudSession = new CloudSpatialAnchorSession();
    this.cloudSession.setSession(sceneView.getSession());
    this.cloudSession.setLogLevel(SessionLogLevel.Information);
    this.cloudSession.addOnLogDebugListener(args -> Log.d("ASAInfo", args.getMessage()));
    this.cloudSession.addErrorListener(args -> Log.e("ASAError", String.format("%s: %s", args.getErrorCode().name(), args.getErrorMessage())));

    sessionInitialized = true;

    this.cloudSession.addSessionUpdatedListener(args -> {
        synchronized (this.syncSessionProgress) {
            this.recommendedSessionProgress = args.getStatus().getRecommendedForCreateProgress();
            Log.i("ASAInfo", String.format("Session progress: %f", this.recommendedSessionProgress));
            if (!this.scanningForUpload) {
                return;
            }
        }

        runOnUiThread(() -> {
            synchronized (this.syncSessionProgress) {
                MaterialFactory.makeOpaqueWithColor(this, new Color(
                        this.recommendedSessionProgress,
                        this.recommendedSessionProgress,
                        this.recommendedSessionProgress))
                        .thenAccept(material -> {
                            this.nodeRenderable.setMaterial(material);
                        });
            }
        });
    });

    this.cloudSession.addAnchorLocatedListener(args -> {
        if (args.getStatus() == LocateAnchorStatus.Located) {
            runOnUiThread(() -> {
                this.anchorNode = new AnchorNode();
                this.anchorNode.setAnchor(args.getAnchor().getLocalAnchor());
                MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.GREEN))
                        .thenAccept(greenMaterial -> {
                            this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), greenMaterial);
                            this.anchorNode.setRenderable(nodeRenderable);
                            this.anchorNode.setParent(arFragment.getArSceneView().getScene());

                            this.anchorId = null;
                            synchronized (this.syncTaps) {
                                this.tapExecuted = false;
                            }
                        });
            });
        }
    });

    this.cloudSession.getConfiguration().setAccountId(/* Copy your account Identifier in here */);
    this.cloudSession.getConfiguration().setAccountKey(/* Copy your account Key in here */);
    this.cloudSession.getConfiguration().setAccountDomain(/* Copy your account Domain in here */);
    this.cloudSession.start();
}

É isso! Reimplante seu aplicativo uma última vez para experimentar o cenário completo de ponta a ponta. Mova seu dispositivo e posicione sua esfera preta. Em seguida, continuaremos movendo seu dispositivo para capturar quadros da câmera até que a esfera fique amarela. A âncora local será carregada e a esfera ficará azul. Por fim, toque em sua tela mais uma vez para que sua âncora local seja removida e, em seguida, consultaremos em busca de seu equivalente de nuvem. Continue movendo seu dispositivo até que a âncora espacial de nuvem seja localizada. Uma esfera verde deve aparecer na localização correta e você pode limpar e repetir o cenário completo.

Juntando tudo

Vemos aqui como deve ser a aparência do arquivo de classe MainActivity completo após todos os elementos diferentes terem sido colocados juntos. Você pode usar isso como uma referência para comparar com seu próprio arquivo e identificar se você tem alguma diferença restante.

package com.example.myfirstapp;

import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;

import androidx.appcompat.app.AppCompatActivity;

import com.google.ar.core.HitResult;
import com.google.ar.core.Plane;
import com.google.ar.sceneform.AnchorNode;
import com.google.ar.sceneform.ArSceneView;
import com.google.ar.sceneform.FrameTime;
import com.google.ar.sceneform.Scene;
import com.google.ar.sceneform.math.Vector3;
import com.google.ar.sceneform.rendering.Color;
import com.google.ar.sceneform.rendering.MaterialFactory;
import com.google.ar.sceneform.rendering.Renderable;
import com.google.ar.sceneform.rendering.ShapeFactory;
import com.google.ar.sceneform.ux.ArFragment;

import com.microsoft.azure.spatialanchors.AnchorLocateCriteria;
import com.microsoft.azure.spatialanchors.CloudSpatialAnchor;
import com.microsoft.azure.spatialanchors.CloudSpatialAnchorSession;
import com.microsoft.azure.spatialanchors.LocateAnchorStatus;
import com.microsoft.azure.spatialanchors.SessionLogLevel;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MainActivity extends AppCompatActivity {

    private boolean tapExecuted = false;
    private final Object syncTaps = new Object();
    private ArFragment arFragment;
    private AnchorNode anchorNode;
    private Renderable nodeRenderable = null;
    private float recommendedSessionProgress = 0f;

    private ArSceneView sceneView;
    private CloudSpatialAnchorSession cloudSession;
    private boolean sessionInitialized = false;

    private String anchorId = null;
    private boolean scanningForUpload = false;
    private final Object syncSessionProgress = new Object();
    private ExecutorService executorService = Executors.newSingleThreadExecutor();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        this.arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);
        this.arFragment.setOnTapArPlaneListener(this::handleTap);

        this.sceneView = arFragment.getArSceneView();
        Scene scene = sceneView.getScene();
        scene.addOnUpdateListener(frameTime -> {
            if (this.cloudSession != null) {
                this.cloudSession.processFrame(sceneView.getArFrame());
            }
        });
        scene.addOnUpdateListener(this::scene_OnUpdate);
        initializeSession();
    }

    // <scene_OnUpdate>
    private void scene_OnUpdate(FrameTime frameTime) {
        if (!sessionInitialized) {
            //retry if initializeSession did an early return due to ARCore Session not yet available (i.e. sceneView.getSession() == null)
            initializeSession();
        }
    }
    // </scene_OnUpdate>

    // <initializeSession>
    private void initializeSession() {
        if (sceneView.getSession() == null) {
            //Early return if the ARCore Session is still being set up
            return;
        }

        if (this.cloudSession != null) {
            this.cloudSession.close();
        }
        this.cloudSession = new CloudSpatialAnchorSession();
        this.cloudSession.setSession(sceneView.getSession());
        this.cloudSession.setLogLevel(SessionLogLevel.Information);
        this.cloudSession.addOnLogDebugListener(args -> Log.d("ASAInfo", args.getMessage()));
        this.cloudSession.addErrorListener(args -> Log.e("ASAError", String.format("%s: %s", args.getErrorCode().name(), args.getErrorMessage())));

        sessionInitialized = true;

        this.cloudSession.addSessionUpdatedListener(args -> {
            synchronized (this.syncSessionProgress) {
                this.recommendedSessionProgress = args.getStatus().getRecommendedForCreateProgress();
                Log.i("ASAInfo", String.format("Session progress: %f", this.recommendedSessionProgress));
                if (!this.scanningForUpload) {
                    return;
                }
            }

            runOnUiThread(() -> {
                synchronized (this.syncSessionProgress) {
                    MaterialFactory.makeOpaqueWithColor(this, new Color(
                            this.recommendedSessionProgress,
                            this.recommendedSessionProgress,
                            this.recommendedSessionProgress))
                            .thenAccept(material -> {
                                this.nodeRenderable.setMaterial(material);
                            });
                }
            });
        });

        this.cloudSession.addAnchorLocatedListener(args -> {
            if (args.getStatus() == LocateAnchorStatus.Located) {
                runOnUiThread(() -> {
                    this.anchorNode = new AnchorNode();
                    this.anchorNode.setAnchor(args.getAnchor().getLocalAnchor());
                    MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.GREEN))
                            .thenAccept(greenMaterial -> {
                                this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), greenMaterial);
                                this.anchorNode.setRenderable(nodeRenderable);
                                this.anchorNode.setParent(arFragment.getArSceneView().getScene());

                                this.anchorId = null;
                                synchronized (this.syncTaps) {
                                    this.tapExecuted = false;
                                }
                            });
                });
            }
        });

        this.cloudSession.getConfiguration().setAccountId(/* Copy your account Identifier in here */);
        this.cloudSession.getConfiguration().setAccountKey(/* Copy your account Key in here */);
        this.cloudSession.getConfiguration().setAccountDomain(/* Copy your account Domain in here */);
        this.cloudSession.start();
    }
    // </initializeSession>

    // <handleTap>
    protected void handleTap(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
        synchronized (this.syncTaps) {
            if (this.tapExecuted) {
                return;
            }

            this.tapExecuted = true;
        }

        if (this.anchorId != null) {
            this.anchorNode.getAnchor().detach();
            this.anchorNode.setParent(null);
            this.anchorNode = null;
            initializeSession();
            AnchorLocateCriteria criteria = new AnchorLocateCriteria();
            criteria.setIdentifiers(new String[]{this.anchorId});
            cloudSession.createWatcher(criteria);
            return;
        }

        this.anchorNode = new AnchorNode();
        this.anchorNode.setAnchor(hitResult.createAnchor());
        CloudSpatialAnchor cloudAnchor = new CloudSpatialAnchor();
        cloudAnchor.setLocalAnchor(this.anchorNode.getAnchor());

        MaterialFactory.makeOpaqueWithColor(this, new Color(
                this.recommendedSessionProgress,
                this.recommendedSessionProgress,
                this.recommendedSessionProgress))
                .thenAccept(material -> {
                    this.nodeRenderable = ShapeFactory.makeSphere(0.1f, new Vector3(0.0f, 0.15f, 0.0f), material);
                    this.anchorNode.setRenderable(nodeRenderable);
                    this.anchorNode.setParent(arFragment.getArSceneView().getScene());
                });


        uploadCloudAnchorAsync(cloudAnchor)
                .thenAccept(id -> {
                    this.anchorId = id;
                    Log.i("ASAInfo", String.format("Cloud Anchor created: %s", this.anchorId));
                    runOnUiThread(() -> {
                        MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.BLUE))
                                .thenAccept(blueMaterial -> {
                                    this.nodeRenderable.setMaterial(blueMaterial);
                                    synchronized (this.syncTaps) {
                                        this.tapExecuted = false;
                                    }
                                });
                    });
                });
    }
    // </handleTap>

    // <uploadCloudAnchorAsync>
    private CompletableFuture<String> uploadCloudAnchorAsync(CloudSpatialAnchor anchor) {
        synchronized (this.syncSessionProgress) {
            this.scanningForUpload = true;
        }


        return CompletableFuture.runAsync(() -> {
            try {
                float currentSessionProgress;
                do {
                    synchronized (this.syncSessionProgress) {
                        currentSessionProgress = this.recommendedSessionProgress;
                    }
                    if (currentSessionProgress < 1.0) {
                        Thread.sleep(500);
                    }
                }
                while (currentSessionProgress < 1.0);

                synchronized (this.syncSessionProgress) {
                    this.scanningForUpload = false;
                }
                runOnUiThread(() -> {
                    MaterialFactory.makeOpaqueWithColor(this, new Color(android.graphics.Color.YELLOW))
                            .thenAccept(yellowMaterial -> {
                                this.nodeRenderable.setMaterial(yellowMaterial);
                            });
                });

                this.cloudSession.createAnchorAsync(anchor).get();
            } catch (InterruptedException | ExecutionException e) {
                Log.e("ASAError", e.toString());
                throw new RuntimeException(e);
            }
        }, executorService).thenApply(ignore -> anchor.getIdentifier());
    }
    // </uploadCloudAnchorAsync>

}

Próximas etapas

Neste tutorial, você viu como criar um novo aplicativo Android que se integra à funcionalidade de ARCore com Âncoras Espaciais do Azure. Para saber mais sobre a biblioteca de Âncoras Espaciais do Azure, veja nosso guia sobre como criar e localizar âncoras.