Tutorial: Schritt-für-Schritt-Anleitung zum Erstellen einer neuen Android-App mit Azure Spatial Anchors

In diesem Tutorial wird veranschaulicht, wie Sie eine neue Android-App erstellen, bei der ARCore-Funktionalität mit Azure Spatial Anchors integriert wird.

Voraussetzungen

Damit Sie dieses Tutorial ausführen können, benötigen Sie folgende Komponenten:

Erste Schritte

Starten Sie Android Studio. Klicken Sie im Fenster Welcome to Android Studio (Willkommen bei Android Studio) auf Start a new Android Studio project (Neues Android Studio-Projekt starten).

  1. Wählen Sie Datei>Neues Projekt aus.
  2. Wählen Sie im Fenster Create New Project (Neues Projekt erstellen) im Abschnitt Phone and Tablet (Telefon und Tablet) die Option Empty Activity (Leere Aktivität), und klicken Sie dann auf Next (Weiter).
  3. Ändern Sie im Fenster „Projekt – Leere Aktivität“ die folgenden Werte:
    • Ändern Sie Name, Paketname und Speicherort in die von Ihnen gewünschten Werte.
    • Legen Sie die Sprache auf Java fest.
    • Legen Sie die API-Mindestebene auf API 26: Android 8.0 (Oreo) fest.
    • Lassen Sie die anderen Optionen unverändert.
    • Klicken Sie auf Fertig stellen.
  4. Das Installationsprogramm für Komponenten (Component Installer) wird ausgeführt. Nach einigen Verarbeitungsschritten wird die IDE von Android Studio geöffnet.

Android Studio - New Project

Ausprobieren

Schließen Sie Ihr Entwicklergerät per USB-Kabel an Ihren Entwicklungscomputer an, um Ihre neue App zu testen. Wählen Sie oben rechts in Android Studio Ihr verbundenes Gerät aus, und klicken Sie auf das Symbol „App“ ausführen. Android Studio installiert die App auf Ihrem verbundenen Gerät und startet sie. Nun sollte „Hallo Welt!“ in der App angezeigt werden, die auf Ihrem Gerät ausgeführt wird. Klicken Sie auf Run>Stop 'app' (Ausführen > „App“ beenden). Android Studio - Run

Integrieren von ARCore

ARCore ist eine Google-Plattform für die Entwicklung von Augmented Reality-Umgebungen. Hiermit kann Ihr Gerät seine Position nachverfolgen, während es sich bewegt und ein eigenes Verständnis der realen Welt entwickelt.

Ändern Sie app\manifests\AndroidManifest.xml so, dass die folgenden Einträge im Stammknoten <manifest> enthalten sind. Mit diesem Codeausschnitt werden einige Schritte ausgeführt:

  • Es wird ermöglicht, dass Ihre App auf die Gerätekamera zugreifen kann.
  • Darüber hinaus wird sichergestellt, dass Ihre App im Google Play Store nur für Geräte sichtbar ist, die ARCore unterstützen.
  • Der Google Play Store wird für das Herunterladen und Installieren von ARCore konfiguriert, falls die Installation nicht bereits bei der Installation Ihrer App durchgeführt wurde.
<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>

Ergänzen Sie Gradle Scripts\build.gradle (Module: app) um den folgenden Eintrag. Mit diesem Code wird sichergestellt, dass Ihre App auf ARCore Version 1.25 ausgerichtet ist. Nach dieser Änderung erhalten Sie unter Umständen eine Benachrichtigung von Gradle, in der Sie zum Synchronisieren aufgefordert werden: Klicken Sie auf Sync now (Jetzt synchronisieren).

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

Integrieren von Sceneform

Sceneform erleichtert das Rendern von realistischen 3D-Szenen in Augmented Reality-Apps, ohne dass OpenGL erlernt werden muss.

Ergänzen Sie Gradle Scripts\build.gradle (Module: app) um die folgenden Einträge. Dieser Code ermöglicht für Ihre App die Verwendung von Sprachkonstrukten aus Java 8, die für Sceneform benötigt werden. Er stellt außerdem sicher, dass Ihre App auf Sceneform, Version 1.15, ausgerichtet ist. Nach dieser Änderung erhalten Sie unter Umständen eine Benachrichtigung von Gradle, in der Sie zum Synchronisieren aufgefordert werden: Klicken Sie auf Sync now (Jetzt synchronisieren).

android {
    ...

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

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

Öffnen Sie die Datei app\res\layout\activity_main.xml, und ersetzen Sie das vorhandene Hello World-Element <TextView ... /> durch das folgende ArFragment-Element. Mit diesem Code wird der Kamerafeed auf Ihrem Bildschirm angezeigt, sodass ARCore die Position Ihres Geräts nachverfolgen kann, während es sich bewegt.

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

Hinweis

Klicken Sie rechts oben rechts in Android Studio auf die Schaltfläche „Code“ oder „Teilen“, um die unformatierte XML-Datei Ihrer Hauptaktivität anzuzeigen.

Führen Sie für Ihre App eine erneute Bereitstellung auf Ihrem Gerät durch, um noch einmal eine Überprüfung zu durchlaufen. Bei diesem Durchlauf sollten Sie eine Abfrage in Bezug auf Kameraberechtigungen erhalten. Nach der Genehmigung sollten Sie sehen können, dass Ihr Kamerafeed auf Ihrem Bildschirm gerendert wird.

Platzieren eines Objekts in der realen Welt

Wir erstellen über Ihre App jetzt ein Objekt und platzieren es. Fügen Sie zunächst die folgenden Importe für app\java\<PackageName>\MainActivity hinzu:

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;

Fügen Sie anschließend die folgenden Membervariablen in Ihrer MainActivity-Klasse hinzu:

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

Fügen Sie als Nächstes der onCreate()-Methode von app\java\<PackageName>\MainActivity den folgenden Code hinzu. Mit diesem Code wird der Listener handleTap() eingebunden, der erkennt, wenn der Benutzer auf den Bildschirm Ihres Geräts tippt. Wenn das Tippen auf einer Oberfläche der realen Welt erfolgt, die von der Nachverfolgungsfunktion von ARCore bereits erkannt wurde, wird der Listener ausgeführt.

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

Fügen Sie abschließend die folgende handleTap()-Methode hinzu, mit der alle Komponenten zusammengefügt werden. Es wird eine Kugel erstellt und an der Position platziert, auf die getippt wurde. Die Kugel ist anfänglich schwarz, da this.recommendedSessionProgress derzeit auf Null festgelegt ist. Dieser Wert wird später angepasst.

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

Führen Sie für Ihre App eine erneute Bereitstellung auf Ihrem Gerät durch, um noch einmal eine Überprüfung zu durchlaufen. Sie können Ihr Gerät nun bewegen, damit ARCore mit der Erkennung Ihrer Umgebung beginnen kann. Tippen Sie dann auf den Bildschirm, um die schwarze Kugel zu erstellen und auf der Oberfläche Ihrer Wahl zu platzieren.

Anfügen eines lokalen Azure-Raumankers

Ergänzen Sie Gradle Scripts\build.gradle (Module: app) um den folgenden Eintrag. Dieser Beispielcodeausschnitt gilt für die SDK-Version 2.10.2 von Azure Spatial Anchors. Beachten Sie, dass die SDK-Version 2.7.0 die aktuell unterstützte Mindestversion ist. Der Verweis auf neuere Versionen von Azure Spatial Anchors sollte ebenfalls funktionieren. Es wird empfohlen, die neueste Version des Azure Spatial Anchors SDK zu verwenden. Die SDK-Versionshinweise finden Sie hier.

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

Wenn Sie Azure Spatial Anchors SDK 2.10.0 oder höher als Ziel verwenden, fügen Sie den folgenden Eintrag in den Repositoryabschnitt der Datei settings.gradle Ihres Projekts ein. Dies umfasst die URL zum Maven-Paketfeed, der Android-Pakete für Azure Spatial Anchors für SDK 2.10.0 oder höher hostet:

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

Klicken Sie mit der rechten Maustaste auf app\java\<PackageName>>New>Java Class (Neu > Java-Klasse). Legen Sie Name auf MyFirstApp fest, und wählen Sie Klasse aus. Eine Datei mit dem Namen MyFirstApp.java wird erstellt. Fügen Sie ihr den folgenden Import hinzu:

import com.microsoft.CloudServices;

Definieren Sie android.app.Application als ihre übergeordnete Klasse.

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

Fügen Sie anschließend den folgenden Code in der neuen MyFirstApp-Klasse hinzu, um sicherzustellen, dass Azure Spatial Anchors mit dem Kontext Ihrer Anwendung initialisiert wird.

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

Ändern Sie app\manifests\AndroidManifest.xml nun so, dass der folgende Eintrag im Stammknoten <application> enthalten ist. Mit diesem Code wird die Anwendungsklasse eingebunden, die Sie in Ihrer App erstellt haben.

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

Wechseln Sie zurück zu app\java\<PackageName>\MainActivity, und fügen Sie die folgenden Importe ein:

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;

Fügen Sie anschließend die folgenden Membervariablen in Ihrer MainActivity-Klasse hinzu:

private float recommendedSessionProgress = 0f;

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

Fügen Sie als Nächstes Ihrer mainActivity-Klasse die folgende initializeSession()-Methode hinzu. Nach dem Aufrufen wird sichergestellt, dass eine Azure Spatial Anchors-Sitzung erstellt und beim Starten Ihrer App richtig initialisiert wird. Dieser Code stellt sicher, dass die sceneview-Sitzung, die über den Aufruf von cloudSession.setSession an die ASA-Sitzung übergeben wird, nicht NULL ist, indem eine frühe Rückgabe erfolgt.

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

Da initializeSession() eine frühe Rückgabe ausführen kann, wenn die sceneView-Sitzung noch nicht eingerichtet ist (d. h., wenn sceneView.getSession() NULL ist), fügen wir einen onUpdate-Aufruf hinzu, um sicherzustellen, dass die ASA-Sitzung initialisiert wird, nachdem die sceneView-Sitzung erstellt wurde.

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

Nun binden wir Ihre initializeSession()- und scene_OnUpdate(...)-Methode in Ihre onCreate()-Methode ein. Außerdem stellen wir sicher, dass Frames Ihres Kamerafeeds zur Verarbeitung an das Azure Spatial Anchors SDK gesendet werden.

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

Fügen Sie abschließend Ihrer handleTap()-Methode den folgenden Code hinzu. An die schwarze Kugel, die wir in der realen Welt platzieren, wird ein lokaler Azure-Raumanker angefügt.

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

Führen Sie für Ihre App noch einmal die erneute Bereitstellung durch. Bewegen Sie Ihr Gerät, tippen Sie auf den Bildschirm, und platzieren Sie eine schwarze Kugel. Dieses Mal wird mit Ihrem Code aber ein lokaler Azure-Raumanker erstellt und an Ihre Kugel angefügt.

Vor dem Fortfahren müssen Sie ein Azure Spatial Anchors-Konto erstellen, um den Kontobezeichner, den Schlüssel und die Domäne zu erhalten, falls Sie noch nicht darüber verfügen. Befolgen Sie die Anleitung im folgenden Abschnitt, um diese Angaben zu erhalten.

Erstellen einer Spatial Anchors-Ressource

Öffnen Sie das Azure-Portal.

Wählen Sie im linken Bereich Ressource erstellen aus.

Suchen Sie über das Suchfeld nach Spatial Anchors.

Screenshot showing the results of a search for Spatial Anchors.

Wählen Sie Spatial Anchors und dann Erstellen aus.

Führen Sie im Bereich Spatial Anchors-Konto die folgenden Schritte aus:

  • Geben Sie einen eindeutigen Ressourcennamen in regulären alphanumerischen Zeichen ein.

  • Wählen Sie das Abonnement aus, an das die Ressource angefügt werden soll.

  • Erstellen Sie eine Ressourcengruppe durch Auswählen von Neu erstellen. Nennen Sie sie myResourceGroup, und wählen Sie OK aus.

    Eine Ressourcengruppe ist ein logischer Container, in dem Azure-Ressourcen wie Web-Apps, Datenbanken und Speicherkonten bereitgestellt und verwaltet werden. Sie können z.B. die gesamte Ressourcengruppe später in einem einfachen Schritt löschen.

  • Wählen Sie einen Standort (Region) für die Ressource aus.

  • Wählen Sie Erstellen aus, um mit der Ressourcenerstellung zu beginnen.

Screenshot of the Spatial Anchors pane for creating a resource.

Nachdem die Ressource erstellt wurde, zeigt das Azure-Portal an, dass die Bereitstellung abgeschlossen ist.

Screenshot showing that the resource deployment is complete.

Wählen Sie Zu Ressource wechseln aus. Nun können Sie die Ressourceneigenschaften anzeigen.

Kopieren Sie den Wert für Konto-ID der Ressource zur späteren Verwendung in einen Text-Editor.

Screenshot of the resource properties pane.

Kopieren Sie außerdem den Wert für Kontodomäne der Ressource zur späteren Verwendung in einen Text-Editor.

Screenshot showing the resource's account domain value.

Wählen Sie unter Einstellungen die Option Zugriffsschlüssel aus. Kopieren Sie die Werte für Primärschlüssel und Kontoschlüssel zur späteren Verwendung in einen Text-Editor.

Screenshot of the Keys pane for the account.

Hochladen Ihres lokalen Ankers in die Cloud

Wenn Ihnen der Azure Spatial Anchors-Kontobezeichner, der Schlüssel und die Domäne vorliegen, können Sie zurück zu app\java\<PackageName>\MainActivity wechseln und die folgenden Importe hinzufügen:

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;

Fügen Sie anschließend die folgenden Membervariablen in Ihrer MainActivity-Klasse hinzu:

private boolean sessionInitialized = false;

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

Fügen Sie als Nächstes der initializeSession()-Methode den folgenden Code hinzu. Erstens ermöglicht dieser Code Ihrer App die Überwachung des Fortschritts, den das Azure Spatial Anchors SDK beim Erfassen der Frames aus Ihrem Kamerafeed macht. Während dieses Vorgangs beginnt sich die Farbe Ihrer Kugel von Schwarz in Grau zu ändern. Die Farbe ändert sich schließlich in Weiß, wenn genügend Frames für das Übermitteln Ihres Ankers in die Cloud erfasst wurden. Zweitens werden mit diesem Code die Anmeldeinformationen bereitgestellt, die für die Kommunikation mit dem Cloud-Back-End benötigt werden. Hier konfigurieren Sie Ihre App für die Verwendung Ihres Kontobezeichners, des Schlüssels und der Domäne. Diese haben Sie beim Einrichten der Spatial Anchors-Ressource in einen Text-Editor kopiert.

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

Fügen Sie als Nächstes Ihrer mainActivity-Klasse die folgende uploadCloudAnchorAsync()-Methode hinzu. Nach dem Aufrufen wartet diese Methode asynchron, bis von Ihrem Gerät genügend Frames erfasst wurden. Sobald dieser Zustand erreicht ist, wird die Farbe Ihrer Kugel in Gelb geändert, und dann wird mit dem Hochladen Ihres lokalen Azure-Raumankers in die Cloud begonnen. Nach Abschluss des Uploadvorgangs gibt der Code einen Ankerbezeichner zurück.

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

Im letzten Schritt werden alle Komponenten zusammengefügt. Fügen Sie in Ihrer handleTap()-Methode den folgenden Code hinzu. Hiermit wird Ihre uploadCloudAnchorAsync()-Methode aufgerufen, nachdem Ihre Kugel erstellt wurde. Nach der Methodenrückgabe wird die Kugel mit dem unten angegebenen Code noch ein letztes Mal aktualisiert, indem ihre Farbe in Blau geändert wird.

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

Führen Sie für Ihre App noch einmal die erneute Bereitstellung durch. Bewegen Sie Ihr Gerät, tippen Sie auf den Bildschirm, und platzieren Sie Ihre Kugel. Dieses Mal wird die Farbe der Kugel aber von Schwarz in Weiß geändert, wenn Kameraframes erfasst werden. Sobald genügend Frames vorhanden sind, ändert sich die Farbe der Kugel in Gelb, und der Upload in die Cloud beginnt. Vergewissern Sie sich, dass Ihr Telefon mit dem Internet verbunden ist. Nach Abschluss des Uploadvorgangs ändert sich die Farbe der Kugel in Blau. Optional können Sie das Fenster Logcat in Android Studio überwachen, um die Protokollnachrichten anzuzeigen, die von Ihrer App gesendet werden. Beispiele für Nachrichten, die protokolliert würden, umfassen den Sitzungsfortschritt während der Frameserfassung und den Ankerbezeichner, den die Cloud nach Abschluss des Uploads zurückgibt.

Hinweis

Wenn keine Veränderungen des Werts von recommendedSessionProgress (in Ihren Debugprotokollen als Session progress bezeichnet) angezeigt werden, stellen Sie sicher, dass Sie ihr Telefon um die platzierte Kugel herum sowohl bewegen als auch drehen.

Suchen nach Ihrem Raumanker für die Cloud

Nachdem Ihr Anker in die Cloud hochgeladen wurde, können wir erneut versuchen, ihn zu finden. Fügen Sie Ihrem Code zunächst die folgenden Importe hinzu.

import java.util.concurrent.Executors;

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

Fügen Sie anschließend der handleTap()-Methode den folgenden Code hinzu. Dieser Code bewirkt Folgendes:

  • Entfernen der vorhandenen blauen Kugel vom Bildschirm
  • Erneutes Initialisieren der Azure Spatial Anchors-Sitzung Mit dieser Aktion wird sichergestellt, dass der zu suchende Anker aus der Cloud stammt und dass es sich nicht um den von uns erstellten lokalen Anker handelt.
  • Führen Sie eine Abfrage für den Anker aus, den wir in die Cloud hochgeladen haben.
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;
                                }
                            });
                });
            });
}

Nun binden wir den Code ein, der aufgerufen wird, wenn der abzufragende Anker gesucht und gefunden wird. Fügen Sie in Ihrer initializeSession()-Methode den folgenden Code hinzu. Mit diesem Codeausschnitt wird eine grüne Kugel erstellt und platziert, nachdem der Raumanker für die Cloud gefunden wurde. Auch das Tippen auf den Bildschirm wird wieder aktiviert, sodass Sie das gesamte Szenario wiederholen können: weiteren lokalen Anker erstellen, Upload durchführen und anschließend danach suchen.

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

Das ist alles! Führen Sie für Ihre App ein letztes Mal die erneute Bereitstellung durch, um das gesamte Szenario auszuprobieren. Bewegen Sie Ihr Gerät, und platzieren Sie Ihre schwarze Kugel. Fahren Sie dann mit dem Umherbewegen Ihres Geräts fort, um Kameraframes zu erfassen, bis sich die Farbe der Kugel in Gelb ändert. Ihr lokaler Anker wird hochgeladen, und Ihre Kugel nimmt die blaue Farbe an. Tippen Sie abschließend noch einmal auf Ihren Bildschirm, damit Ihr lokaler Anker entfernt wird. Anschließend führen wir eine Abfrage nach der Entsprechung in der Cloud durch. Fahren Sie mit dem Umherbewegen Ihres Geräts fort, bis Ihr Raumanker für die Cloud gefunden wurde. Eine grüne Kugel sollte an der richtigen Position angezeigt werden, und Sie können das gesamte Szenario noch einmal durchführen.

Zusammenfügen der Bestandteile

Nachdem alle Einzelkomponenten miteinander kombiniert wurden, sollte die fertige MainActivity-Klassendatei wie hier gezeigt aussehen. Sie können diese Datei als Referenz verwenden und mit Ihrer eigenen Datei vergleichen, um mögliche Diskrepanzen zu ermitteln.

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>

}

Nächste Schritte

In diesem Tutorial haben Sie gelernt, wie Sie eine neue Android-App erstellen, bei der ARCore-Funktionalität mit Azure Spatial Anchors integriert wird. Weitere Informationen zur Azure Spatial Anchors-Bibliothek erhalten Sie, indem Sie unseren Leitfaden zum Erstellen und Suchen nach Ankern weiter durcharbeiten.

Create and locate anchors using Azure Spatial Anchors (Erstellen und Suchen nach Ankern mit Azure Spatial Anchors)