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

Este tutorial mostrará como criar um novo aplicativo Android que integra a funcionalidade ARCore com as Âncoras Espaciais do Azure.

Pré-requisitos

Para concluir este tutorial, confirme que 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. Na janela Novo Projeto - Atividade Vazia, altere os seguintes valores:
    • Altere o nome, o nome do pacote e o local de salvamento para os valores desejados
    • Definir idioma é para Java
    • Defina o nível mínimo da API 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. Após algum processamento, o Android Studio abrirá o IDE.

Android Studio - New Project

Experimentar

Para testar seu novo aplicativo, conecte seu dispositivo habilitado para desenvolvedor à sua máquina de desenvolvimento com um cabo USB. No canto superior direito do Android Studio, selecione o seu dispositivo conectado e clique no ícone Executar 'aplicativo' . O Android Studio instala a aplicação no seu dispositivo ligado e inicia-a. Agora você deve ver "Hello World!" exibido no aplicativo em execução no seu dispositivo. Clique em Run-Stop> 'app'. Android Studio - Run

Integração do ARCore

ARCore é a plataforma do Google para construir experiências de Realidade Aumentada, permitindo que seu dispositivo acompanhe sua posição à medida que se move e constrói sua própria compreensão do mundo real.

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

  • Ele permitirá que seu aplicativo acesse a câmera do dispositivo.
  • Ele também garantirá que seu aplicativo só seja visível na Google Play Store para dispositivos compatíveis com ARCore.
  • Ele configurará a Google Play Store para baixar e instalar o ARCore, se ele ainda não estiver instalado, quando seu aplicativo estiver instalado.
<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 seguinte entrada. Esse código garantirá que seu aplicativo tenha como destino o ARCore versão 1.25. Após essa alteração, você pode receber uma notificação do Gradle solicitando a sincronização: clique em Sincronizar agora.

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

Integrando o Sceneform

O Sceneform simplifica a renderização de cenas 3D realistas em aplicativos de Realidade Aumentada, sem ter que aprender OpenGL.

Modifique Gradle Scripts\build.gradle (Module: app) para incluir as seguintes entradas. Esse código permitirá que seu aplicativo use construções de linguagem do Java 8, o que Sceneform exige. Ele também garantirá que seu aplicativo tenha como destino a Sceneform versão 1.15. Após essa alteração, você pode receber uma notificação do Gradle solicitando a sincronização: 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 o seu app\res\layout\activity_main.xmle substitua o elemento Hello Wolrd <TextView ... /> existente pelo seguinte ArFragment. Esse código fará com que o feed da câmera seja exibido na tela, permitindo que o ARCore rastreie a posição do dispositivo enquanto 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" />

Nota

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

Reimplante seu aplicativo no dispositivo para validá-lo mais uma vez. Desta vez, você deve ser solicitado para permissões de câmera. Uma vez aprovado, você verá a renderização do feed da câmera na tela.

Colocar um objeto no mundo real

Vamos criar & colocar um objeto usando seu 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 à sua MainActivity classe:

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

Em seguida, adicione o seguinte código ao seu app\java\<PackageName>\MainActivityonCreate() método. Esse código conectará um ouvinte, chamado handleTap(), que detetará quando o usuário tocar na tela do seu dispositivo. Se a torneira estiver em uma superfície do mundo real que já foi reconhecida pelo rastreamento do ARCore, o ouvinte executará.

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

Finalmente, adicione o seguinte handleTap() método, que irá amarrar tudo. Ele criará uma esfera e a colocará no local tocado. A esfera será inicialmente preta, uma vez que this.recommendedSessionProgress está definida para zero agora. Este 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. Desta vez, você pode se mover pelo dispositivo para que o ARCore comece a reconhecer seu ambiente. Em seguida, toque na tela para criar & coloque sua esfera preta sobre a superfície de sua escolha.

Anexar uma âncora espacial do Azure local

Modifique Gradle Scripts\build.gradle (Module: app) para incluir a seguinte entrada. Este trecho de código de exemplo tem como alvo o SDK do Azure Spatial Anchors versão 2.10.2. Observe que a versão 2.7.0 do SDK é atualmente a versão mínima com suporte e a referência a qualquer versão mais recente das Âncoras Espaciais do Azure também deve funcionar. Recomendamos usar a versão mais recente do SDK do Azure Spatial Anchors. Você pode encontrar as notas de 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 você estiver visando o SDK 2.10.0 ou posterior do Azure Spatial Anchors, inclua a seguinte entrada na seção de repositórios settings.gradle do arquivo do seu projeto. Isso incluirá a URL para o feed de pacotes Maven que hospeda pacotes Android do Azure Spatial Anchors para 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>-New-Java>> Class. Defina Nome como MyFirstApp e selecione Classe. Um arquivo chamado MyFirstApp.java será criado. Adicione a seguinte importação a ele:

import com.microsoft.CloudServices;

Defina android.app.Application como sua superclasse.

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

Em seguida, adicione o seguinte código dentro da nova MyFirstApp classe, o que garantirá que o Azure Spatial Anchors seja inicializado com o contexto do seu aplicativo.

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

Agora, modifique app\manifests\AndroidManifest.xml para incluir a seguinte entrada dentro do nó raiz <application> . Esse código conectará a classe Application que você criou ao seu aplicativo.

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

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

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 à sua MainActivity classe:

private float recommendedSessionProgress = 0f;

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

Em seguida, vamos adicionar o seguinte initializeSession() método dentro da sua mainActivity classe. Uma vez chamado, ele garantirá que uma sessão do Azure Spatial Anchors seja criada e inicializada corretamente durante a inicialização do seu aplicativo. Esse código garante que a sessão sceneview passada para a sessão ASA por meio da cloudSession.setSession chamada não seja nula por ter 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() podemos fazer um retorno antecipado se a sessão sceneView ainda não estiver configurada (ou seja, se sceneView.getSession() for nula), adicionamos uma chamada onUpdate para garantir que a sessão ASA seja inicializada assim 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 ligar o seu método e ao seu initializeSession()onCreate() métodoscene_OnUpdate(...). Além disso, garantiremos que os quadros do feed da câmera sejam enviados para o SDK do Azure Spatial Anchors 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();
}

Finalmente, adicione o seguinte código ao seu handleTap() método. Ele anexará uma Âncora Espacial do Azure local à esfera negra 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 seu aplicativo mais uma vez. Mova-se pelo dispositivo, toque no ecrã e coloque uma esfera preta. Desta vez, porém, seu código criará e anexará uma Âncora Espacial do Azure local à sua esfera.

Antes de prosseguir, você precisará criar uma conta do Azure Spatial Anchors para obter o Identificador, a Chave e o Domínio da conta, se ainda não os tiver. Siga a seção a seguir para obtê-los.

Criar um recurso de Âncoras Espaciais

Aceda ao portal do Azure.

No painel esquerdo, selecione Criar um recurso.

Use a caixa de pesquisa para procurar Âncoras Espaciais.

Screenshot showing the results of a search for Spatial Anchors.

Selecione Âncoras espaciais e, em seguida, selecione Criar.

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

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

  • Selecione a subscrição à qual pretende anexar o recurso.

  • Crie um grupo de recursos selecionando Criar novo. Nomeie-o myResourceGroup e selecione 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, pode optar por eliminar todo o grupo de recursos num único passo simples mais tarde.

  • Selecione um local (região) no qual colocar o recurso.

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

Screenshot of the Spatial Anchors pane for creating a resource.

Depois que o recurso é criado, o portal do Azure mostra que sua implantação foi concluída.

Screenshot showing that the resource deployment is complete.

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

Copie o valor de ID de conta do recurso em um editor de texto para uso posterior.

Screenshot of the resource properties pane.

Copie também o valor Domínio da Conta do recurso em um editor de texto para uso posterior.

Screenshot showing the resource's account domain value.

Em Configurações, selecione Chave de acesso. Copie o valor da chave primária, Chave de conta, em um editor de texto para uso posterior.

Screenshot of the Keys pane for the account.

Carregue sua âncora local na nuvem

Depois de ter o Identificador, a Chave e o Domínio da conta do Azure Spatial Anchors, podemos voltar a , app\java\<PackageName>\MainActivityadicionar as seguintes importações:

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 à sua MainActivity classe:

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 seguinte código ao seu initializeSession() método. Primeiro, esse código permitirá que seu aplicativo monitore o progresso que o SDK de Âncoras Espaciais do Azure faz à medida que coleta quadros do feed da câmera. À medida que o faz, a cor da sua esfera começará a mudar do seu preto original para cinzento. Em seguida, ele ficará branco assim que quadros suficientes forem 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 da nuvem. Aqui é onde você configurará seu aplicativo para usar seu identificador de conta, chave e domínio. 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 seguinte uploadCloudAnchorAsync() método dentro da sua mainActivity classe. Uma vez chamado, esse método aguardará assincronamente até que quadros suficientes sejam coletados do seu dispositivo. Assim que isso acontecer, ele alternará a cor da sua esfera para amarelo e, em seguida, começará a carregar sua Âncora Espacial do Azure local na nuvem. Quando o carregamento terminar, o código retornará um identificador de â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 juntar tudo. No seu handleTap() método, adicione o seguinte código. Ele invocará seu uploadCloudAnchorAsync() método assim que sua esfera for criada. Assim que o método retornar, o código abaixo executará uma atualização final para sua 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 seu aplicativo mais uma vez. Mova-se pelo dispositivo, toque no ecrã e coloque a sua esfera. Desta vez, porém, sua esfera mudará sua cor de preto para branco, à medida que os quadros da câmera são coletados. Quando tivermos quadros suficientes, a esfera ficará amarela e o upload na nuvem será iniciado. Certifique-se de que o telemóvel está ligado à Internet. Quando o carregamento terminar, a sua esfera ficará azul. Opcionalmente, você pode monitorar a Logcat janela no Android Studio para exibir as mensagens de log que seu 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 quando o carregamento é concluído.

Nota

Se você não estiver vendo o valor de (em seus logs de depuração referidos como Session progress) mudança, certifique-se de recommendedSessionProgress que você está movendo e girando seu telefone em torno da esfera que você colocou.

Localize sua âncora espacial na nuvem

Assim que a âncora for carregada na nuvem, estamos prontos para tentar localizá-la novamente. Primeiro, vamos adicionar as seguintes importações ao seu código.

import java.util.concurrent.Executors;

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

Em seguida, vamos adicionar o seguinte código ao seu handleTap() método. Este código irá:

  • Remova a nossa esfera azul existente do ecrã.
  • Inicialize nossa sessão de Âncoras Espaciais do Azure novamente. Essa ação garantirá que a âncora que vamos localizar venha da nuvem em vez da âncora local que criamos.
  • Emita uma consulta para a âncora que carregamos 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 estiver localizada. Dentro do seu initializeSession() método, adicione o seguinte código. Este trecho criará uma esfera verde assim que a âncora espacial da nuvem estiver localizada. Ele também permitirá o toque na tela novamente, para que você possa repetir todo o cenário mais uma vez: crie outra âncora local, carregue-a e localize-a 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();
}

Está feito! Reimplante seu aplicativo uma última vez para experimentar todo o cenário de ponta a ponta. Mova-se pelo seu dispositivo e coloque a sua esfera preta. Em seguida, continue movendo seu dispositivo para capturar quadros da câmera até que a esfera fique amarela. Sua âncora local será carregada e sua esfera ficará azul. Por fim, toque na tela mais uma vez, para que sua âncora local seja removida e, em seguida, consultaremos sua contraparte na nuvem. Continue movendo seu dispositivo até que sua âncora espacial na nuvem esteja localizada. Uma esfera verde deve aparecer no local correto, e você pode enxaguar ou repetir todo o cenário novamente.

Juntando tudo

Aqui está como o arquivo de classe completo MainActivity deve parecer, depois de todos os diferentes elementos terem sido montados. Você pode usá-lo como uma referência para comparar com seu próprio arquivo e identificar se você pode ter alguma diferença.

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óximos passos

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