Tutorial: Instruções passo a passo para criar uma nova aplicação Android com o Azure Spatial Anchors

Este tutorial irá mostrar-lhe como criar uma nova aplicação Android que integra a funcionalidade ARCore com o Azure Spatial Anchors.

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 Ficheiro ->Novo Projeto.
  2. Na janela Criar Novo Projeto , na secção Telemóvel e Tablet , selecione Atividade Vazia e clique em Seguinte.
  3. Na janela Novo Projeto – Atividade Vazia, altere os seguintes valores:
    • Altere o Nome, o Nome do pacote e a localização Guardar para os valores pretendidos
    • Definir idioma é para Java
    • Definir o nível mínimo da API como API 26: Android 8.0 (Oreo)
    • Deixe as outras opções tal como estão
    • Clique em Concluir.
  4. O Instalador de Componentes será executado. Após algum processamento, o Android Studio abrirá o IDE.

Android Studio - Novo Projeto

Experimentar

Para testar a nova aplicação, ligue o dispositivo compatível com programador ao seu computador de desenvolvimento com um cabo USB. No canto superior direito do Android Studio, selecione o seu dispositivo ligado e clique no ícone Executar "aplicação" . O Android Studio instala a aplicação no seu dispositivo ligado e inicia-a. Deverá agora ver "Hello World!" apresentado na aplicação em execução no seu dispositivo. Clique em Executar/>Parar 'aplicação'. Android Studio - Executar

Integrar o ARCore

O ARCore é a plataforma da Google para criar experiências de Realidade Aumentada, permitindo ao seu dispositivo controlar a sua posição à medida que se move e cria a sua própria compreensão do mundo real.

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

  • Permitirá que a sua aplicação aceda à câmara do seu dispositivo.
  • Também irá garantir que a sua aplicação só está visível na Google Play Store para dispositivos que suportem ARCore.
  • Irá configurar a Google Play Store para transferir e instalar o ARCore, se ainda não estiver instalado, quando a sua aplicação estiver instalada.
<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. Este código irá garantir que a sua aplicação tem como destino a versão ARCore 1.25. Após esta alteração, poderá receber uma notificação do Gradle a pedir-lhe para sincronizar: clique em Sincronizar agora.

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

Integrar o Sceneform

O Sceneform simplifica a composição de cenas 3D realistas em aplicações de Realidade Aumentada, sem ter de aprender OpenGL.

Modifique Gradle Scripts\build.gradle (Module: app) para incluir as seguintes entradas. Este código permitirá que a sua aplicação utilize construções de linguagem do Java 8, o que Sceneform requer. Também irá garantir que a sua aplicação tem como destino Sceneform a versão 1.15. Após esta alteração, poderá receber uma notificação do Gradle a pedir-lhe para sincronizar: 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. Este código fará com que o feed da câmara seja apresentado no ecrã, permitindo que o ARCore controle a posição do dispositivo à medida que 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 não processado da sua atividade principal, clique no botão "Código" ou "Dividir" no canto superior direito do Android Studio.

Volte a implementar a sua aplicação no seu dispositivo para validá-la mais uma vez. Desta vez, deverão ser-lhe pedidas permissões de câmara. Depois de aprovado, deverá ver a composição do feed da câmara no ecrã.

Colocar um objeto no mundo real

Vamos criar & um objeto com a sua aplicação. Primeiro, adicione as seguintes importações ao :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 método app\java\<PackageName>\MainActivityonCreate() . Este código irá ligar um serviço de escuta, denominado handleTap(), que irá detetar quando o utilizador tocar no ecrã no seu dispositivo. Se o toque estiver numa superfície do mundo real que já tenha sido reconhecida pelo controlo do ARCore, o serviço de escuta 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 seguinte handleTap() método, que irá associar tudo. Irá criar uma esfera e colocá-la na localização sob escuta. A esfera será inicialmente preta, uma vez que this.recommendedSessionProgress está definida como zero agora. Este valor será ajustado mais tarde.

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

Volte a implementar a sua aplicação no seu dispositivo para validá-la mais uma vez. Desta vez, pode deslocar-se no seu dispositivo para que o ARCore comece a reconhecer o seu ambiente. Em seguida, toque no ecrã para criar & a sua esfera preta sobre a superfície à sua escolha.

Anexar uma Âncora Espacial do Azure local

Modifique Gradle Scripts\build.gradle (Module: app) para incluir a seguinte entrada. Este fragmento de código de exemplo destina-se à versão 2.10.2 do SDK do Azure Spatial Anchors. Tenha em atenção que a versão 2.7.0 do SDK é a versão mínima suportada e que a referência a qualquer versão mais recente do Azure Spatial Anchors também deve funcionar. Recomendamos que utilize a versão mais recente do SDK do Azure Spatial Anchors. 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 estiver a filtrar o SDK do Azure Spatial Anchors 2.10.0 ou posterior, inclua a seguinte entrada na secção repositórios do ficheiro do settings.gradle projeto. Isto incluirá o URL para o feed de pacotes do Maven que aloja 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 rato em app\java\<PackageName>->New-Java Class (ClasseNew-Java>). Defina Nomecomo MyFirstApp e selecione Classe. Será criado um ficheiro chamado MyFirstApp.java . Adicione a seguinte importação ao mesmo:

import com.microsoft.CloudServices;

Defina android.app.Application como a sua superclasse.

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

Em seguida, adicione o seguinte código dentro da nova MyFirstApp classe, que irá garantir que o Azure Spatial Anchors é inicializado com o contexto da sua aplicação.

    @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> . Este código irá ligar a classe Aplicação que criou na sua aplicação.

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

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

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 chamada, irá garantir que uma sessão do Azure Spatial Anchors é criada e inicializada corretamente durante o arranque da sua aplicação. Este código garante que a sessão de vista de cena transmitida para a sessão do ASA através da cloudSession.setSession chamada não é nula por ter uma devolução antecipada.

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

Uma initializeSession() vez que poderia fazer uma devolução antecipada 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 do ASA é inicializada assim que a sessão sceneView é 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 initializeSession() método e scene_OnUpdate(...) ao seu onCreate() método. Além disso, vamos garantir que os fotogramas do feed da câmara são enviados para o SDK do Spatial Anchors 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 seguinte código ao método handleTap() . Vai anexar uma Âncora Espacial do Azure local à esfera negra que estamos a colocar 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());
            });
}

Volte a implementar a sua aplicação. Mova-se no seu dispositivo, toque no ecrã e coloque uma esfera preta. Desta vez, porém, o seu código irá criar e anexar uma Âncora Espacial do Azure local à sua esfera.

Antes de continuar, terá de criar uma conta do Azure Spatial Anchors para obter o Identificador da conta, a Chave e o Domínio, se ainda não as tiver. Siga a secção seguinte para as obter.

Criar um recurso de Âncoras Espaciais

Aceda ao Portal do Azure.

No painel esquerdo, selecione Criar um recurso.

Utilize a caixa de pesquisa para procurar Âncoras Espaciais.

Captura de ecrã a mostrar os resultados de uma pesquisa de Âncoras Espaciais.

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

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

  • Introduza um nome de recurso exclusivo com carateres alfanuméricos normais.

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

  • Crie um grupo de recursos ao selecionar Criar novo. Dê-lhe o nome myResourceGroup e, em seguida, selecione OK.

    Um grupo de recursos é um contentor lógico no qual os recursos do Azure, como aplicações Web, bases de dados e contas de armazenamento, são implementados e geridos. Por exemplo, pode optar por eliminar todo o grupo de recursos num único passo simples mais tarde.

  • Selecione uma localização (região) na qual colocar o recurso.

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

Captura de ecrã do painel Âncoras Espaciais para criar um recurso.

Após a criação do recurso, o portal do Azure mostra que a implementação está concluída.

Captura de ecrã a mostrar que a implementação de recursos está concluída.

Selecione Ir para recurso. Agora pode ver as propriedades do recurso.

Copie o valor do ID da Conta do recurso para um editor de texto para utilização posterior.

Captura de ecrã do painel de propriedades do recurso.

Copie também o valor do Domínio de Conta do recurso para um editor de texto para utilização posterior.

Captura de ecrã a mostrar o valor de domínio da conta do recurso.

Em Definições, selecione Chave de Acesso. Copie o valor chave primária , Chave de Conta, para um editor de texto para utilizar mais tarde.

Captura de ecrã do painel Chaves da conta.

Carregar a âncora local para a cloud

Assim que tiver o Identificador, a Chave e o Domínio da conta do Azure Spatial Anchors, podemos voltar app\java\<PackageName>\MainActivityao , adicionar as seguintes importações ao mesmo:

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, este código permitirá que a sua aplicação monitorize o progresso que o SDK do Spatial Anchors do Azure faz à medida que recolhe fotogramas do feed da câmara. Tal como acontece, a cor da sua esfera começará a mudar do seu preto original para cinzento. Em seguida, ficará branco assim que forem recolhidos fotogramas suficientes para submeter a sua âncora à cloud. Em segundo lugar, este código fornecerá as credenciais necessárias para comunicar com o back-end da cloud. Eis onde irá configurar a sua aplicação para utilizar o Identificador da conta, a Chave e o Domínio. Copiou-os para 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, este método aguardará assíncronamente até que sejam recolhidos fotogramas suficientes do seu dispositivo. Assim que isso acontecer, mudará a cor da sua esfera para amarelo e, em seguida, começará a carregar o Seu Âncora Espacial do Azure local para a cloud. Assim que o carregamento for concluído, o código devolverá 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());
}

Finalmente, vamos juntar tudo. No seu handleTap() método, adicione o seguinte código. Invocará o seu uploadCloudAnchorAsync() método assim que a sua esfera for criada. Assim que o método for devolvido, o código abaixo efetuará uma atualização final para a sua esfera, alterando a 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;
                                }
                            });
                });
            });
}

Volte a implementar a sua aplicação. Mova-se no seu dispositivo, toque no ecrã e coloque a sua esfera. Desta vez, porém, a sua esfera mudará a sua cor de preto para branco, à medida que as molduras da câmara são recolhidas. Assim que tivermos molduras suficientes, a esfera ficará amarela e o carregamento da cloud será iniciado. Certifique-se de que o telemóvel está ligado à Internet. Quando o carregamento terminar, a esfera ficará azul. Opcionalmente, pode monitorizar a Logcat janela no Android Studio para ver as mensagens de registo que a sua aplicação está a enviar. Exemplos de mensagens que seriam registadas incluem o progresso da sessão durante a captura de frames e o identificador de âncora que a cloud devolve assim que o carregamento estiver concluído.

Nota

Se não estiver a ver o valor da alteração (nos registos de depuração recommendedSessionProgress referidos como Session progress) certifique-se de que está a mover e a rodar o telemóvel à volta da esfera que colocou.

Localize a sua âncora espacial na cloud

Assim que a sua âncora for carregada para a cloud, 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:

  • Remova a nossa esfera azul existente do ecrã.
  • Inicialize novamente a nossa sessão de Âncoras Espaciais do Azure. Esta ação irá garantir que a âncora que vamos localizar provém da cloud em vez da âncora local que criámos.
  • Emita uma consulta para a âncora que carregamos para a cloud.
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 ligar o código que será invocado quando a âncora para a qual estamos a consultar estiver localizada. No seu initializeSession() método, adicione o seguinte código. Este fragmento irá criar & uma esfera verde assim que a âncora espacial da cloud estiver localizada. Também ativará o toque no ecrã novamente, para que possa repetir todo o cenário mais uma vez: criar outra âncora local, carregá-la e localizá-la 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();
}

Já está! Volte a implementar a sua aplicação uma última vez para experimentar todo o cenário ponto a ponto. Mova-se pelo seu dispositivo e coloque a sua esfera preta. Em seguida, continue a mover o dispositivo para capturar as molduras da câmara até a esfera ficar amarela. A âncora local será carregada e a sua esfera ficará azul. Por fim, toque no ecrã mais uma vez, para que a âncora local seja removida e, em seguida, iremos consultar a respetiva contrapartida da cloud. Continue a mover o dispositivo até que a âncora espacial na cloud esteja localizada. Deverá aparecer uma esfera verde na localização correta e pode enxaguar & novamente todo o cenário.

Juntar tudo

Eis como deve ser o aspeto do ficheiro de classe completo MainActivity , depois de todos os diferentes elementos terem sido reunidos. Pode utilizá-lo como referência para comparar com o seu próprio ficheiro e identificar se ainda existem diferenças.

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>

}

Passos seguintes

Neste tutorial, viu como criar uma nova aplicação Android que integra a funcionalidade ARCore com as Âncoras Espaciais do Azure. Para saber mais sobre a biblioteca de Âncoras Espaciais do Azure, avance para o nosso guia sobre como criar e localizar âncoras.