Orte in der Nähe in AR auf Android anzeigen (Kotlin)

1. Hinweis

Zusammenfassung

In diesem Codelab erfahren Sie, wie Sie Daten aus der Google Maps Platform verwenden, um Orte in der Nähe in Augmented Reality (AR) auf Android-Geräten anzuzeigen.

2344909dd9a52c60.png

Vorbereitung

  • Grundkenntnisse der Android-Entwicklung mit Android Studio
  • Kotlin-Vorkenntnisse

Lerninhalte

  • Fordern Sie die Berechtigung des Nutzers an, auf die Kamera und den Standort des Geräts zuzugreifen.
  • Integrieren Sie die Places API, um Orte in der Nähe des Gerätestandorts abzurufen.
  • ARCore integrieren, um horizontale Ebenen zu finden, damit virtuelle Objekte mit Sceneform im 3D-Raum verankert und platziert werden können.
  • Erfassen Sie Informationen zur Position des Geräts im Raum mit SensorManager und verwenden Sie die Maps SDK for Android-Dienstprogrammbibliothek, um virtuelle Objekte in der richtigen Ausrichtung zu positionieren.

Voraussetzungen

2. Einrichten

Android Studio

In diesem Codelab wird Android 10.0 (API-Level 29) verwendet. Außerdem müssen die Google Play-Dienste in Android Studio installiert sein. Führen Sie die folgenden Schritte aus, um beide Abhängigkeiten zu installieren:

  1. Rufen Sie den SDK-Manager auf, indem Sie auf Tools > SDK-Manager klicken.

6c44a9cb9cf6c236.png

  1. Prüfen Sie, ob Android 10.0 installiert ist. Falls nicht, installieren Sie es, indem Sie das Kästchen neben Android 10.0 (Q) anklicken, dann auf OK und schließlich noch einmal auf OK im angezeigten Dialogfeld klicken.

368f17a974c75c73.png

  1. Installieren Sie schließlich die Google Play-Dienste, indem Sie den Tab SDK Tools aufrufen, das Kästchen neben Google Play Services anklicken, auf OK klicken und dann im angezeigten Dialogfeld noch einmal OK auswählen**.**

497a954b82242f4b.png

Erforderliche APIs

Aktivieren Sie in Schritt 3 des folgenden Abschnitts das Maps SDK for Android und die Places API für dieses Codelab.

Einstieg in die Google Maps Platform

Wenn Sie die Google Maps Platform noch nicht verwendet haben, folgen Sie der Anleitung für die ersten Schritte mit der Google Maps Platform oder sehen Sie sich die Playlist „Erste Schritte mit der Google Maps Platform“ an, um die folgenden Schritte auszuführen:

  1. Erstellen Sie ein Rechnungskonto.
  2. Projekt erstellen
  3. Aktivieren Sie die APIs und SDKs der Google Maps Platform, die im vorherigen Abschnitt aufgeführt sind.
  4. Generieren Sie einen API-Schlüssel.

Optional: Android Emulator

Wenn Sie kein ARCore-kompatibles Gerät haben, können Sie alternativ den Android-Emulator verwenden, um eine AR-Szene zu simulieren und den Standort Ihres Geräts zu fälschen. Da Sie in dieser Übung auch Sceneform verwenden, müssen Sie die Schritte unter „Emulator für Sceneform konfigurieren“ ausführen.

3. Schnelleinstieg

Damit Sie so schnell wie möglich loslegen können, finden Sie hier einige Startcodes, die Ihnen helfen, diesem Codelab zu folgen. Sie können direkt zur Lösung springen. Wenn Sie alle Schritte sehen möchten, lesen Sie einfach weiter.

Sie können das Repository klonen, wenn Sie git installiert haben.

git clone https://github.com/googlecodelabs/display-nearby-places-ar-android.git

Alternativ können Sie auf die Schaltfläche unten klicken, um den Quellcode herunterzuladen.

Öffnen Sie nach dem Abrufen des Codes das Projekt im Verzeichnis starter.

4. Projektübersicht

Sehen Sie sich den Code an, den Sie im vorherigen Schritt heruntergeladen haben. In diesem Repository sollte sich ein einzelnes Modul namens app befinden, das das Paket com.google.codelabs.findnearbyplacesar enthält.

AndroidManifest.xml

Die folgenden Attribute werden in der Datei AndroidManifest.xml deklariert, damit Sie die in diesem Codelab erforderlichen Funktionen verwenden können:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<!-- Sceneform requires OpenGL ES 3.0 or later. -->
<uses-feature
   android:glEsVersion="0x00030000"
   android:required="true" />

<!-- Indicates that app requires ARCore ("AR Required"). Ensures the app is visible only in the Google Play Store on devices that support ARCore. For "AR Optional" apps remove this line. -->
<uses-feature android:name="android.hardware.camera.ar" />

Für uses-permission, das angibt, welche Berechtigungen vom Nutzer erteilt werden müssen, bevor diese Funktionen verwendet werden können, werden die folgenden deklariert:

  • android.permission.INTERNET: Damit Ihre App Netzwerkoperationen ausführen und Daten über das Internet abrufen kann, z. B. Ortsinformationen über die Places API.
  • android.permission.CAMERA: Der Zugriff auf die Kamera ist erforderlich, damit Sie die Kamera des Geräts verwenden können, um Objekte in Augmented Reality anzuzeigen.
  • android.permission.ACCESS_FINE_LOCATION: Der Standortzugriff ist erforderlich, damit Sie Orte in der Nähe des Gerätestandorts abrufen können.

Für uses-feature, das angibt, welche Hardwarefunktionen für diese App erforderlich sind, wird Folgendes deklariert:

  • OpenGL ES-Version 3.0 ist erforderlich.
  • Ein ARCore-kompatibles Gerät ist erforderlich.

Außerdem werden dem Anwendungsobjekt die folgenden Metadaten-Tags hinzugefügt:

<application
  android:allowBackup="true"
  android:icon="@mipmap/ic_launcher"
  android:label="@string/app_name"
  android:roundIcon="@mipmap/ic_launcher_round"
  android:supportsRtl="true"
  android:theme="@style/AppTheme">
  
  <!-- 
     Indicates that this app requires Google Play Services for AR ("AR Required") and causes
     the Google Play Store to download and install Google Play Services for AR along with
     the app. For an "AR Optional" app, specify "optional" instead of "required". 
  -->

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

  <meta-data
     android:name="com.google.android.geo.API_KEY"
     android:value="@string/google_maps_key" />

  <!-- Additional elements here --> 

</application>

Der erste Metadateneintrag gibt an, dass ARCore für die Ausführung dieser App erforderlich ist. Der zweite Eintrag zeigt, wie Sie Ihren Google Maps Platform-API-Schlüssel für das Maps SDK for Android angeben.

build.gradle

In build.gradle werden die folgenden zusätzlichen Abhängigkeiten angegeben:

dependencies {
    // Maps & Location
    implementation 'com.google.android.gms:play-services-location:17.0.0'
    implementation 'com.google.android.gms:play-services-maps:17.0.0'
    implementation 'com.google.maps.android:maps-utils-ktx:1.7.0'

    // ARCore
    implementation "com.google.ar.sceneform.ux:sceneform-ux:1.15.0"

    // Retrofit
    implementation "com.squareup.retrofit2:retrofit:2.7.1"
    implementation "com.squareup.retrofit2:converter-gson:2.7.1"
}

Hier finden Sie eine kurze Beschreibung der einzelnen Abhängigkeiten:

  • Die Bibliotheken mit der Gruppen-ID com.google.android.gms, nämlich play-services-location und play-services-maps, werden verwendet, um auf Standortinformationen des Geräts und auf Funktionen im Zusammenhang mit Google Maps zuzugreifen.
  • com.google.maps.android:maps-utils-ktx ist die Bibliothek mit Kotlin-Erweiterungen (KTX) für die Maps SDK for Android-Dienstprogrammbibliothek. Funktionen dieser Bibliothek werden verwendet, um virtuelle Objekte später im realen Raum zu positionieren.
  • com.google.ar.sceneform.ux:sceneform-ux ist die Sceneform-Bibliothek, mit der Sie realistische 3D-Szenen rendern können, ohne OpenGL lernen zu müssen.
  • Die Abhängigkeiten innerhalb der Gruppen-ID com.squareup.retrofit2 sind die Retrofit-Abhängigkeiten, mit denen Sie schnell einen HTTP-Client für die Interaktion mit der Places API schreiben können.

Projektstruktur

Hier finden Sie die folgenden Pakete und Dateien:

  • **api**: Dieses Paket enthält Klassen, die für die Interaktion mit der Places API über Retrofit verwendet werden.
  • **ar**: Dieses Paket enthält alle Dateien, die mit ARCore zusammenhängen.
  • **model**: Dieses Paket enthält eine einzelne Datenklasse Place, die verwendet wird, um einen einzelnen Ort zu kapseln, der von der Places API zurückgegeben wird.
  • MainActivity.kt: Dies ist die einzige Activity in Ihrer App, in der eine Karte und eine Kameraansicht angezeigt werden.

5. Szene einrichten

Sehen Sie sich die Kernkomponenten der App an, beginnend mit den Augmented-Reality-Elementen.

MainActivity enthält ein SupportMapFragment-Element, das für die Darstellung des Kartenobjekts zuständig ist, und eine Unterklasse von ArFragment – PlacesArFragment –, die für die Darstellung der Augmented Reality-Szene zuständig ist.

Augmented Reality-Einrichtung

Neben der Darstellung der Augmented Reality-Szene kümmert sich PlacesArFragment auch darum, die Kameraberechtigung vom Nutzer anzufordern, falls sie noch nicht erteilt wurde. Zusätzliche Berechtigungen können auch durch Überschreiben der Methode getAdditionalPermissions angefordert werden. Da Sie auch die Berechtigung zur Standortermittlung benötigen, geben Sie das an und überschreiben Sie die getAdditionalPermissions-Methode:

class PlacesArFragment : ArFragment() {

   override fun getAdditionalPermissions(): Array<String> =
       listOf(Manifest.permission.ACCESS_FINE_LOCATION)
           .toTypedArray()
}

Ausführen

Öffnen Sie den Gerüstcode im Verzeichnis starter in Android Studio. Wenn Sie in der Symbolleiste auf Ausführen > „app“ ausführen klicken und die App auf Ihrem Gerät oder Emulator bereitstellen, werden Sie zuerst aufgefordert, die Berechtigungen für Standort und Kamera zu aktivieren. Klicken Sie auf Zulassen. Daraufhin sollten Sie eine Kameraansicht und eine Kartenansicht nebeneinander sehen, wie hier:

e3e3073d5c86f427.png

Ebenen erkennen

Wenn Sie sich mit der Kamera in Ihrer Umgebung umsehen, werden Sie möglicherweise einige weiße Punkte auf horizontalen Oberflächen bemerken, ähnlich wie die weißen Punkte auf dem Teppich auf diesem Bild.

2a9b6ea7dcb2e249.png

Diese weißen Punkte sind Richtlinien von ARCore, die darauf hinweisen, dass eine horizontale Ebene erkannt wurde. Mit diesen erkannten Ebenen können Sie einen sogenannten „Anker“ erstellen, um virtuelle Objekte im realen Raum zu positionieren.

Weitere Informationen zu ARCore und dazu, wie die Umgebung erkannt wird

6. Orte in der Nähe abrufen

Als Nächstes müssen Sie auf den aktuellen Standort des Geräts zugreifen und ihn anzeigen. Anschließend rufen Sie mit der Places API Orte in der Nähe ab.

Maps-Einrichtung

Google Maps Platform-API-Schlüssel

Sie haben bereits einen Google Maps Platform-API-Schlüssel erstellt, um die Places API abzufragen und das Maps SDK for Android zu verwenden. Öffnen Sie die Datei gradle.properties und ersetzen Sie den String "YOUR API KEY HERE" durch den API-Schlüssel, den Sie erstellt haben.

Gerätestandort auf der Karte anzeigen

Nachdem Sie den API-Schlüssel hinzugefügt haben, fügen Sie der Karte einen Orientierungshelfer hinzu, damit Nutzer sehen, wo sie sich auf der Karte befinden. Rufen Sie dazu die Methode setUpMaps auf und legen Sie innerhalb des mapFragment.getMapAsync-Aufrufs googleMap.isMyLocationEnabled auf true. fest. Dadurch wird der blaue Punkt auf der Karte angezeigt.

private fun setUpMaps() {
   mapFragment.getMapAsync { googleMap ->
       googleMap.isMyLocationEnabled = true
       // ...
   }
}

Aktuellen Standort abrufen

Wenn Sie den Standort des Geräts abrufen möchten, müssen Sie die Klasse FusedLocationProviderClient verwenden. Eine Instanz davon wurde bereits in der Methode onCreate von MainActivity abgerufen. Um dieses Objekt zu verwenden, füllen Sie die Methode getCurrentLocation aus, die ein Lambda-Argument akzeptiert, damit dem Aufrufer dieser Methode ein Standort übergeben werden kann.

Um diese Methode abzuschließen, können Sie auf die Property lastLocation des Objekts FusedLocationProviderClient zugreifen und dann addOnSuccessListener hinzufügen:

fusedLocationClient.lastLocation.addOnSuccessListener { location ->
    currentLocation = location
    onSuccess(location)
}.addOnFailureListener {
    Log.e(TAG, "Could not get location")
}

Die Methode getCurrentLocation wird in der Lambda-Funktion aufgerufen, die in getMapAsync in der Methode setUpMaps bereitgestellt wird, aus der die Orte in der Nähe abgerufen werden.

Orte-Netzwerkanruf starten

Beachten Sie, dass im Methodenaufruf getNearbyPlaces die folgenden Parameter an die Methode placesServices.nearbyPlaces übergeben werden: ein API-Schlüssel, der Standort des Geräts, ein Radius in Metern (der auf 2 km festgelegt ist) und ein Ortstyp (der derzeit auf park festgelegt ist).

val apiKey = "YOUR API KEY"
placesService.nearbyPlaces(
   apiKey = apiKey,
   location = "${location.latitude},${location.longitude}",
   radiusInMeters = 2000,
   placeType = "park"
)

Um den Netzwerkaufruf abzuschließen, übergeben Sie den API-Schlüssel, den Sie in der Datei gradle.properties definiert haben. Das folgende Code-Snippet ist in Ihrer build.gradle-Datei unter der Konfiguration android > defaultConfig definiert:

android {
   defaultConfig {
       resValue "string", "google_maps_key", (project.findProperty("GOOGLE_MAPS_API_KEY") ?: "")
   }
}

Dadurch wird der Stringressourcenwert google_maps_key zur Build-Zeit verfügbar.

Um den Netzwerkaufruf abzuschließen, können Sie diese String-Ressource einfach über getString für das Context-Objekt lesen.

val apiKey = this.getString(R.string.google_maps_key)

7. Orte in AR

Bisher haben Sie Folgendes getan:

  1. Beim ersten Ausführen der App wurden Kamera- und Standortberechtigungen vom Nutzer angefordert.
  2. ARCore einrichten, um horizontale Ebenen zu erfassen
  3. Maps SDK mit Ihrem API-Schlüssel einrichten
  4. Aktuellen Standort des Geräts erhalten
  5. Orte in der Nähe (insbesondere Parks) mit der Places API abgerufen

Im letzten Schritt dieser Übung müssen Sie die abgerufenen Orte in Augmented Reality positionieren.

Szenenanalyse

ARCore kann die reale Umgebung über die Kamera des Geräts erfassen, indem in jedem Bildframe interessante und eindeutige Punkte, sogenannte Feature-Punkte, erkannt werden. Wenn diese Feature-Punkte gruppiert sind und auf einer gemeinsamen horizontalen Ebene liegen, z. B. auf Tischen und Böden, kann ARCore diese Funktion der App als horizontale Ebene zur Verfügung stellen.

Wie Sie bereits gesehen haben, werden weiße Punkte angezeigt, um den Nutzer zu führen, wenn eine Ebene erkannt wurde.

2a9b6ea7dcb2e249.png

Anker hinzufügen

Sobald eine Ebene erkannt wurde, können Sie ein Objekt namens Anker daran anfügen. Mit einem Anker können Sie virtuelle Objekte platzieren und dafür sorgen, dass sie an derselben Position im Raum bleiben. Ändern Sie den Code so, dass ein Anker angehängt wird, sobald eine Ebene erkannt wurde.

In setUpAr ist ein OnTapArPlaneListener an das PlacesArFragment angehängt. Dieser Listener wird aufgerufen, wenn in der AR-Szene auf eine Ebene getippt wird. In diesem Aufruf können Sie ein Anchor- und ein AnchorNode-Objekt aus dem bereitgestellten HitResult im Listener erstellen:

arFragment.setOnTapArPlaneListener { hitResult, _, _ ->
   val anchor = hitResult.createAnchor()
   anchorNode = AnchorNode(anchor)
   anchorNode?.setParent(arFragment.arSceneView.scene)
   addPlaces(anchorNode!!)
}

Im AnchorNode-Objekt werden untergeordnete Knotenobjekte (PlaceNode-Instanzen) in der Szene angehängt, was im addPlaces-Methodenaufruf erfolgt.

Ausführen

Wenn Sie die App mit den oben genannten Änderungen ausführen, schauen Sie sich um, bis ein Flugzeug erkannt wird. Tippe auf die weißen Punkte, die ein Flugzeug anzeigen. Nun sollten auf der Karte Markierungen für alle Parks in Ihrer Nähe angezeigt werden. Die virtuellen Objekte sind jedoch an dem erstellten Anker fixiert und nicht relativ zu den Parks platziert.

f93eb87c98a0098d.png

Im letzten Schritt korrigieren Sie dies mithilfe der Maps SDK for Android-Dienstprogrammbibliothek und des SensorManager auf dem Gerät.

8. Orte positionieren

Damit das Symbol für den virtuellen Ort in Augmented Reality in der richtigen Richtung positioniert werden kann, benötigen Sie zwei Informationen:

  • Wo der geografische Norden ist
  • Der Winkel zwischen Norden und jedem Ort

Norden bestimmen

Norden kann mithilfe der auf dem Gerät verfügbaren Positionssensoren (geomagnetisch und Beschleunigungsmesser) bestimmt werden. Mit diesen beiden Sensoren können Sie Echtzeitinformationen zur Position des Geräts im Raum erfassen. Weitere Informationen zu Positionssensoren finden Sie unter Ausrichtung des Geräts berechnen.

Um auf diese Sensoren zuzugreifen, müssen Sie zuerst eine SensorManager abrufen und dann eine SensorEventListener für diese Sensoren registrieren. Diese Schritte werden bereits in den Lebenszyklusmethoden von MainActivity ausgeführt:

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   // ...
   sensorManager = getSystemService()!!
   // ...
}

override fun onResume() {
   super.onResume()
   sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)?.also {
       sensorManager.registerListener(
           this,
           it,
           SensorManager.SENSOR_DELAY_NORMAL
       )
   }
   sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)?.also {
       sensorManager.registerListener(
           this,
           it,
           SensorManager.SENSOR_DELAY_NORMAL
       )
   }
}

override fun onPause() {
   super.onPause()
   sensorManager.unregisterListener(this)
}

In der Methode onSensorChanged wird ein SensorEvent-Objekt bereitgestellt, das Details zu den Daten eines bestimmten Sensors enthält, wenn sich diese im Laufe der Zeit ändern. Fügen Sie dieser Methode den folgenden Code hinzu:

override fun onSensorChanged(event: SensorEvent?) {
   if (event == null) {
       return
   }
   if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) {
       System.arraycopy(event.values, 0, accelerometerReading, 0, accelerometerReading.size)
   } else if (event.sensor.type == Sensor.TYPE_MAGNETIC_FIELD) {
       System.arraycopy(event.values, 0, magnetometerReading, 0, magnetometerReading.size)
   }

   // Update rotation matrix, which is needed to update orientation angles.
   SensorManager.getRotationMatrix(
       rotationMatrix,
       null,
       accelerometerReading,
       magnetometerReading
   )
   SensorManager.getOrientation(rotationMatrix, orientationAngles)
}

Mit dem Code oben wird der Sensortyp geprüft und je nach Typ wird der entsprechende Sensorwert aktualisiert (entweder der Beschleunigungsmesser- oder der Magnetometerwert). Anhand dieser Sensormessungen kann nun der Wert für die Anzahl der Grad von Norden relativ zum Gerät bestimmt werden (d.h. der Wert von orientationAngles[0]).

Sphärische Überschrift

Nachdem Norden bestimmt wurde, muss als Nächstes der Winkel zwischen Norden und jedem Ort ermittelt werden. Anhand dieser Informationen werden die Orte dann in der Augmented Reality in der richtigen Richtung positioniert.

Zur Berechnung des Kurses verwenden Sie die Maps SDK for Android-Dienstprogrammbibliothek, die einige Hilfsfunktionen zur Berechnung von Entfernungen und Kursen über sphärische Geometrie enthält. Weitere Informationen zur Bibliothek

Als Nächstes verwenden Sie die Methode sphericalHeading in der Dienstprogrammbibliothek, mit der die Richtung zwischen zwei LatLng-Objekten berechnet wird. Diese Informationen werden in der getPositionVector-Methode benötigt, die in Place.kt definiert ist. Diese Methode gibt letztendlich ein Vector3-Objekt zurück, das dann von jedem PlaceNode als seine lokale Position im AR-Raum verwendet wird.

Ersetzen Sie die Überschriftendefinition in dieser Methode durch Folgendes:

val heading = latLng.sphericalHeading(placeLatLng)

Dadurch sollte die folgende Methodendefinition entstehen:

fun Place.getPositionVector(azimuth: Float, latLng: LatLng): Vector3 {
   val placeLatLng = this.geometry.location.latLng
   val heading = latLng.sphericalHeading(placeLatLng)
   val r = -2f
   val x = r * sin(azimuth + heading).toFloat()
   val y = 1f
   val z = r * cos(azimuth + heading).toFloat()
   return Vector3(x, y, z)
}

Lokale Position

Der letzte Schritt, um Orte in AR richtig auszurichten, besteht darin, das Ergebnis von getPositionVector zu verwenden, wenn PlaceNode-Objekte der Szene hinzugefügt werden. Rufen Sie addPlaces in MainActivity auf, direkt unter der Zeile, in der das übergeordnete Element für jedes placeNode festgelegt ist (direkt unter placeNode.setParent(anchorNode)). Legen Sie den localPosition des placeNode auf das Ergebnis des Aufrufs von getPositionVector fest:

val placeNode = PlaceNode(this, place)
placeNode.setParent(anchorNode)
placeNode.localPosition = place.getPositionVector(orientationAngles[0], currentLocation.latLng)

Standardmäßig wird mit der Methode getPositionVector der y-Abstand des Knotens auf 1 Meter festgelegt, wie durch den y-Wert in der Methode getPositionVector angegeben. Wenn Sie diesen Abstand anpassen möchten, z. B. auf 2 Meter, können Sie den Wert entsprechend ändern.

Durch diese Änderung sollten hinzugefügte PlaceNode-Objekte jetzt in der richtigen Richtung ausgerichtet sein. Führen Sie die App jetzt aus, um das Ergebnis zu sehen.

9. Glückwunsch

Herzlichen Glückwunsch, dass Sie es bis hierher geschafft haben!

Weitere Informationen