Självstudie: Stegvisa instruktioner för att skapa en ny Android-app med Hjälp av Azure Spatial Anchors

Den här självstudien visar hur du skapar en ny Android-app som integrerar ARCore-funktioner med Azure Spatial Anchors.

Förutsättningar

För att kunna följa den här självstudien måste du ha:

Komma igång

Starta Android Studio. I fönstret Välkommen till Android Studio klickar du på Starta ett nytt Android Studio-projekt.

  1. Välj Fil nytt> projekt.
  2. I fönstret Skapa nytt projekt går du till avsnittet Telefon och Surfplatta, väljer Tom aktivitet och klickar på Nästa.
  3. I fönstret Nytt projekt – tom aktivitet ändrar du följande värden:
    • Ändra namn, paketnamn och spara plats till önskade värden
    • Ange Språk är till Java
    • Ange lägsta API-nivå till API 26: Android 8.0 (Oreo)
    • Lämna de andra alternativen som de är
    • Klicka på Finish.
  4. Komponentinstallationsprogrammet körs. Efter viss bearbetning öppnar Android Studio IDE.

Android Studio - New Project

Prova det

Om du vill testa din nya app ansluter du din utvecklaraktiverade enhet till utvecklingsdatorn med en USB-kabel. Längst upp till höger i Android Studio väljer du din anslutna enhet och klickar på ikonen Kör "app" . Android Studio installerar appen på den anslutna enheten och startar den. Nu bör du se "Hello World!" som visas i appen som körs på enheten. Klicka på Kör-stoppa> "app". Android Studio - Run

Integrera ARCore

ARCore är Googles plattform för att skapa augmented reality-upplevelser, vilket gör det möjligt för din enhet att spåra sin position när den rör sig och bygger sin egen förståelse av den verkliga världen.

Ändra app\manifests\AndroidManifest.xml om du vill inkludera följande poster i rotnoden <manifest> . Det här kodfragmentet gör några saker:

  • Det gör att din app kan komma åt enhetens kamera.
  • Det säkerställer också att din app endast visas i Google Play Store för enheter som stöder ARCore.
  • Den konfigurerar Google Play Store för att ladda ned och installera ARCore, om den inte redan är installerad, när din app är installerad.
<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>

Ändra Gradle Scripts\build.gradle (Module: app) för att inkludera följande post. Den här koden ser till att din app riktar in sig på ARCore version 1.25. Efter den här ändringen kan du få ett meddelande från Gradle där du uppmanas att synkronisera: klicka på Synkronisera nu.

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

Integrera Scenform

Scenform gör det enkelt att återge realistiska 3D-scener i appar för förhöjd verklighet, utan att behöva lära sig OpenGL.

Ändra Gradle Scripts\build.gradle (Module: app) så att följande poster inkluderas. Med den här koden kan din app använda språkkonstruktioner från Java 8, vilket Sceneform kräver. Det säkerställer också att din app riktar in sig på Sceneform version 1.15. Efter den här ändringen kan du få ett meddelande från Gradle där du uppmanas att synkronisera: klicka på Synkronisera nu.

android {
    ...

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

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

Öppna ditt app\res\layout\activity_main.xml, och ersätt det befintliga Hello Wolrd-elementet <TextView ... /> med följande ArFragment. Den här koden gör att kameraflödet visas på skärmen så att ARCore kan spåra enhetens position när den flyttas.

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

Kommentar

Om du vill se den råa XML-koden för din huvudaktivitet klickar du på knappen "Kod" eller "Dela" längst upp till höger i Android Studio.

Distribuera om din app till enheten för att verifiera den igen. Den här gången bör du bli tillfrågad om kamerabehörigheter. När den har godkänts bör du se kameraflödesåtergivningen på skärmen.

Placera ett objekt i den verkliga världen

Nu ska vi skapa och placera ett objekt med hjälp av din app. Lägg först till följande importer i :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;

Lägg sedan till följande medlemsvariabler i klassen MainActivity :

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

Lägg sedan till följande kod i din app\java\<PackageName>\MainActivityonCreate() metod. Den här koden ansluter en lyssnare med namnet handleTap(), som identifierar när användaren trycker på skärmen på enheten. Om kranen råkar finnas på en verklig yta som redan har identifierats av ARCores spårning, körs lyssnaren.

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

Lägg slutligen till följande handleTap() metod som binder ihop allt. Den skapar en sfär och placerar den på den knackade platsen. Sfären blir till en början svart, eftersom this.recommendedSessionProgress den är inställd på noll just nu. Det här värdet justeras senare.

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

Distribuera om din app till enheten för att verifiera den igen. Den här gången kan du flytta runt på enheten för att få ARCore att börja känna igen din miljö. Tryck sedan på skärmen för att skapa och placera den svarta sfären över valfri yta.

Bifoga en lokal Azure Spatial Anchor

Ändra Gradle Scripts\build.gradle (Module: app) för att inkludera följande post. Det här exempelkodfragmentet riktar sig till Azure Spatial Anchors SDK version 2.10.2. Observera att SDK version 2.7.0 för närvarande är den lägsta versionen som stöds, och att referera till en senare version av Azure Spatial Anchors bör också fungera. Vi rekommenderar att du använder den senaste versionen av Azure Spatial Anchors SDK. Du hittar viktig information om SDK här.

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

Om du riktar in dig på Azure Spatial Anchors SDK 2.10.0 eller senare inkluderar du följande post i avsnittet lagringsplatser i projektets settings.gradle fil. Detta inkluderar URL:en till Maven-paketflödet som är värd för Azure Spatial Anchors Android-paket för SDK 2.10.0 eller senare:

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

Högerklicka på app\java\<PackageName>->New-Java> Class. Ange Namn till MyFirstApp och välj Klass. En fil med namnet MyFirstApp.java skapas. Lägg till följande import i den:

import com.microsoft.CloudServices;

Definiera android.app.Application som dess superklass.

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

Lägg sedan till följande kod i den nya MyFirstApp klassen, vilket säkerställer att Azure Spatial Anchors initieras med programmets kontext.

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

app\manifests\AndroidManifest.xml Ändra nu för att inkludera följande post i rotnoden<application>. Den här koden ansluter den programklass som du skapade till din app.

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

Tillbaka i app\java\<PackageName>\MainActivitylägger du till följande importer i den:

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;

Lägg sedan till följande medlemsvariabler i klassen MainActivity :

private float recommendedSessionProgress = 0f;

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

Nu ska vi lägga till följande initializeSession() metod i klassen mainActivity . När den har anropats ser den till att en Azure Spatial Anchors-session skapas och initieras korrekt under starten av din app. Den här koden ser till att scenvisningssessionen som skickas till ASA-sessionen via anropet cloudSession.setSession inte är null genom att ha tidig retur.

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

Eftersom initializeSession() kan göra en tidig retur om sceneView-sessionen ännu inte har konfigurerats (dvs. om sceneView.getSession() är null) lägger vi till ett onUpdate-anrop för att se till att ASA-sessionen initieras när sceneView-sessionen har skapats.

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

Nu ska vi koppla in din initializeSession() metod och scene_OnUpdate(...) din onCreate() metod. Dessutom ser vi till att ramar från kameraflödet skickas till Azure Spatial Anchors SDK för bearbetning.

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

Lägg slutligen till följande kod i din handleTap() metod. Den kopplar en lokal Azure Spatial Anchor till den svarta sfär som vi placerar i den verkliga världen.

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

Distribuera om din app en gång till. Flytta runt enheten, tryck på skärmen och placera en svart sfär. Men den här gången kommer koden att skapa och koppla en lokal Azure Spatial Anchor till din sfär.

Innan du fortsätter måste du skapa ett Azure Spatial Anchors-konto för att hämta kontoidentifieraren, nyckeln och domänen om du inte redan har dem. Följ följande avsnitt för att hämta dem.

Skapa en Spatial Anchors-resurs

Gå till Azure-portalen.

Välj Skapa en resurs i den vänstra rutan.

Använd sökrutan för att söka efter Spatial Anchors.

Screenshot showing the results of a search for Spatial Anchors.

Välj Spatial Anchors och välj sedan Skapa.

Gör följande i fönstret Spatial Anchors-konto :

  • Ange ett unikt resursnamn med hjälp av vanliga alfanumeriska tecken.

  • Välj den prenumeration som du vill koppla resursen till.

  • Skapa en resursgrupp genom att välja Skapa ny. Ge den namnet myResourceGroup och välj sedan OK.

    En resursgrupp är en logisk container där Azure-resurser, till exempel webbappar, databaser och lagringskonton, distribueras och hanteras. Du kan exempelvis välja att ta bort hela resursgruppen i ett enkelt steg längre fram.

  • Välj en plats (region) där du vill placera resursen.

  • Välj Skapa för att börja skapa resursen.

Screenshot of the Spatial Anchors pane for creating a resource.

När resursen har skapats visar Azure-portalen att distributionen är klar.

Screenshot showing that the resource deployment is complete.

Välj Gå till resurs. Nu kan du visa resursegenskaperna.

Kopiera resursens konto-ID-värde till en textredigerare för senare användning.

Screenshot of the resource properties pane.

Kopiera även resursens kontodomänvärde till en textredigerare för senare användning.

Screenshot showing the resource's account domain value.

Under Inställningar väljer du Åtkomstnyckel. Kopiera värdet Primärnyckel , Kontonyckel, till en textredigerare för senare användning.

Screenshot of the Keys pane for the account.

Ladda upp din lokala fästpunkt till molnet

När du har ditt Azure Spatial Anchors-konto identifierare, nyckel och domän kan vi gå tillbaka till app\java\<PackageName>\MainActivityoch lägga till följande importer i det:

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;

Lägg sedan till följande medlemsvariabler i klassen MainActivity :

private boolean sessionInitialized = false;

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

Lägg nu till följande kod i din initializeSession() metod. Först gör den här koden det möjligt för din app att övervaka förloppet som Azure Spatial Anchors SDK gör när den samlar in ramar från kameraflödet. Som det gör, kommer färgen på din sfär att börja förändras från sin ursprungliga svarta, till grå. Sedan blir den vit när tillräckligt många bildrutor samlas in för att skicka din fästpunkt till molnet. För det andra anger den här koden de autentiseringsuppgifter som krävs för att kommunicera med molnserverdelen. Här konfigurerar du appen så att den använder kontoidentifierare, nyckel och domän. Du kopierade dem till en textredigerare när du konfigurerade Spatial Anchors-resursen.

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

Lägg sedan till följande uploadCloudAnchorAsync() metod i klassen mainActivity . När den här metoden har anropats väntar den asynkront tills tillräckligt många bildrutor samlas in från enheten. Så snart det händer växlar det färgen på din sfär till gul och sedan börjar den ladda upp din lokala Azure Spatial Anchor till molnet. När uppladdningen är klar returnerar koden en fästpunktsidentifierare.

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

Äntligen ska vi koppla ihop allt. Lägg till följande kod i din handleTap() metod. Den anropar din uploadCloudAnchorAsync() metod så snart sfären har skapats. När metoden har returnerats utför koden nedan en sista uppdatering av sfären och ändrar dess färg till blå.

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

Distribuera om din app en gång till. Flytta runt enheten, tryck på skärmen och placera sfären. Den här gången ändrar dock din sfär sin färg från svart till vit, eftersom kameraramar samlas in. När vi har tillräckligt med ramar blir sfären gul och molnuppladdningen startar. Kontrollera att telefonen är ansluten till Internet. När uppladdningen är klar blir sfären blå. Du kan också övervaka fönstret Logcat i Android Studio för att visa de loggmeddelanden som appen skickar. Exempel på meddelanden som loggas är sessionsförloppet under bildfångstet och fästpunktsidentifieraren som molnet returnerar när uppladdningen är klar.

Kommentar

Om du inte ser värdet för recommendedSessionProgress (i dina felsökningsloggar som Session progresskallas ) ändring kontrollerar du att du både flyttar och roterar telefonen runt den sfär som du har placerat.

Leta upp ditt rumsliga molnfästpunkt

När fästpunkten har laddats upp till molnet är vi redo att försöka hitta den igen. Först ska vi lägga till följande importer i koden.

import java.util.concurrent.Executors;

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

Sedan ska vi lägga till följande kod i din handleTap() metod. Den här koden kommer att:

  • Ta bort vår befintliga blå sfär från skärmen.
  • Initiera vår Azure Spatial Anchors-session igen. Den här åtgärden säkerställer att fästpunkten som vi ska hitta kommer från molnet i stället för det lokala fästpunkt som vi skapade.
  • Utfärda en fråga för fästpunkten som vi laddade upp till molnet.
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;
                                }
                            });
                });
            });
}

Nu ska vi koppla koden som ska anropas när fästpunkten som vi frågar efter finns. initializeSession() Lägg till följande kod i din metod. Det här kodfragmentet skapar och placerar en grön sfär när molnområdets fästpunkt finns. Det aktiverar också skärmtryckning igen, så att du kan upprepa hela scenariot igen: skapa en annan lokal fästpunkt, ladda upp den och leta upp den igen.

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

Det var allt! Distribuera om din app en sista gång för att prova hela scenariot från slutpunkt till slutpunkt. Flytta runt enheten och placera din svarta sfär. Fortsätt sedan att flytta enheten för att fånga kameraramar tills sfären blir gul. Din lokala fästpunkt laddas upp och sfären blir blå. Tryck slutligen på skärmen en gång till så att din lokala fästpunkt tas bort och sedan frågar vi efter dess molnmotsvarighet. Fortsätt att flytta runt enheten tills din molnlokala fästpunkt finns. En grön sfär bör visas på rätt plats och du kan skölja och upprepa hela scenariot igen.

Sätta ihop allt

Så här ska den fullständiga MainActivity klassfilen se ut efter att alla olika element har sammanställts. Du kan använda den som referens för att jämföra med din egen fil och upptäcka om du kan ha några skillnader kvar.

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ästa steg

I den här självstudien har du sett hur du skapar en ny Android-app som integrerar ARCore-funktioner med Azure Spatial Anchors. Om du vill veta mer om Azure Spatial Anchors-biblioteket fortsätter du till vår guide om hur du skapar och hittar fästpunkter.