Wyświetlanie w pobliżu miejsc AR w Androidzie (Kotlin)

1. Zanim zaczniesz

Streszczenie

Dzięki nim dowiesz się, jak używać danych z Google Maps Platform do wyświetlania w Androidzie miejsc w pobliżu w rzeczywistości rozszerzonej (AR).

2344909dd9a52c60.png

Wymagania wstępne

  • Podstawowe informacje o programowaniu na Androida za pomocą Android Studio
  • Znajomość Kotlina

Czego się nauczysz:

  • Poproś użytkownika o dostęp do aparatu i lokalizacji.
  • Zintegruj interfejs Places API, aby pobierać pobliskie miejsca w pobliżu urządzenia.
  • Zintegruj narzędzie ARCore, aby znaleźć poziome powierzchnie płaszczyzn, dzięki którym można zakotwiczyć i umieścić obiekty wirtualne w przestrzeni 3D za pomocą Sceneform.
  • Zbierz informacje o pozycji urządzenia w przestrzeni kosmicznej, korzystając z narzędzia SensorManager, i użyj biblioteki SDK pakietu SDK na Androida do umieszczenia wirtualnych obiektów we właściwym nagłówku.

Czego potrzebujesz

2. Konfiguracja

Android Studio,

To ćwiczenie wymaga korzystania z Androida 10.0 (poziom API 29) i wymaga zainstalowania Usług Google Play w Android Studio. Aby zainstalować obie te zależności:

  1. Przejdź do Menedżera SDK, do którego można uzyskać dostęp, klikając Narzędzia > Menedżer pakietów SDK.

6c44a9cb9cf6c236.png

  1. Sprawdź, czy masz zainstalowanego Androida 10.0. Jeśli nie, zainstaluj go, zaznaczając pole wyboru obok Androida 10.0 (Q), a następnie kliknij OK i jeszcze raz kliknij OK w wyświetlonym oknie.

368f17a974c75c73.png

  1. Na koniec zainstaluj Usługi Google Play. Aby to zrobić, otwórz kartę Narzędzia SDK, zaznacz pole wyboru obok pozycji Usługi Google Play, kliknij OK, a następnie ponownie kliknij OK w wyświetlonym oknie**.

497a954b82242f4b.png

Wymagane interfejsy API

W kroku 3 tej sekcji włącz te ćwiczenia z programowania: Maps SDK for Android i Places API.

Pierwsze kroki z Google Maps Platform

Jeśli korzystasz z Google Maps Platform po raz pierwszy, zapoznaj się z przewodnikiem dla początkujących po Google Maps Platform lub obejrzyj tę playlistę, aby poznać te wskazówki:

  1. Utwórz konto rozliczeniowe.
  2. Utwórz projekt.
  3. Włącz interfejsy API i pakiety SDK Google Maps Platform (znajdziesz je w poprzedniej sekcji).
  4. Wygeneruj klucz interfejsu API.

Opcjonalnie: emulator Androida

Jeśli nie masz urządzenia z obsługą ARCore, możesz też za pomocą emulatora Androida symulować scenę AR oraz fałszywą lokalizację urządzenia. Biorąc pod uwagę, że w tym ćwiczeniu użyjesz funkcji Sceneform, musisz też wykonać czynności opisane w sekcji „Konfigurowanie emulatora pod kątem obsługi narzędzia Sceneform”."

3. Szybki start

Aby ułatwić Ci rozpoczęcie tego ćwiczenia, skorzystaj z tego kodu, który ułatwia rozpoczęcie ćwiczeń z programowania. Zachęcamy do przejścia do rozwiązania, ale jeśli chcesz zapoznać się ze wszystkimi krokami, czytaj dalej.

Możesz skopiować repozytorium, jeśli masz zainstalowane oprogramowanie git.

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

Możesz też kliknąć przycisk poniżej, aby pobrać kod źródłowy.

Po pobraniu kodu otwórz projekt znajdujący się w katalogu starter.

4. Omówienie projektu

Przejrzyj kod pobrany z poprzedniego kroku. W repozytorium znajdziesz pojedynczy moduł o nazwie app, który zawiera pakiet com.google.codelabs.findnearbyplacesar.

AndroidManifest.xml

W pliku AndroidManifest.xml zawarte są te atrybuty, aby umożliwić Ci korzystanie z funkcji wymaganych w tym ćwiczeniu z programowania:

<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" />

W przypadku zasady uses-permission, która określa uprawnienia, które użytkownik musi przyznać, aby można było korzystać z tych funkcji, są deklarowane:

  • android.permission.INTERNET– dzięki temu aplikacja może wykonywać działania sieciowe i pobierać dane, takie jak informacje o miejscach w interfejsie Places API.
  • android.permission.CAMERA – wymagany jest dostęp do aparatu, aby można było używać kamery urządzenia do wyświetlania obiektów w rzeczywistości rozszerzonej.
  • android.permission.ACCESS_FINE_LOCATION – potrzebny jest dostęp do lokalizacji, by pobierać miejsca w pobliżu według lokalizacji urządzenia.

W przypadku obiektu uses-feature określasz, które funkcje sprzętowe są potrzebne do działania tej aplikacji, deklarowane są te elementy:

  • Wymagana jest wersja OpenGL ES w wersji 3.0.
  • Urządzenie obsługujące ARCore jest wymagane.

Dodatkowo w obiekcie aplikacji są dodawane te tagi metadanych:

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

Pierwszy wpis w metatagu zawiera informację, że uruchomienie tej aplikacji jest konieczne, a drugi jest sposób przekazania klucza interfejsu API Google Maps Platform do pakietu SDK Maps na Androida.

build.gradle

W build.gradle określono te dodatkowe zależności:

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

Oto krótki opis każdej z zależności:

  • Biblioteki z identyfikatorem grupy com.google.android.gms, play-services-location i play-services-maps, są używane do uzyskiwania dostępu do informacji o lokalizacji urządzenia i dostępu do funkcji związanych z Mapami Google.
  • com.google.maps.android:maps-utils-ktx to biblioteka Kotlin (KTX) dla Maps SDK for Android Utility Library. Za pomocą tej biblioteki możesz umieścić obiekty wirtualne w przestrzeni wirtualnej później.
  • com.google.ar.sceneform.ux:sceneform-ux to biblioteka Sceneform, która umożliwia renderowanie realistycznych scen 3D bez konieczności poznawania technologii OpenGL.
  • Zależności w identyfikatorze grupy com.squareup.retrofit2 to zależności Retrofit, które umożliwiają szybkie zapisywanie klienta HTTP w celu interakcji z interfejsem Places API.

Struktura projektu

Tutaj znajdziesz te pakiety i pliki:

  • **api—**pakiet zawiera klasy używane do interakcji z interfejsem Places API za pomocą Retrofit.
  • **ar –**ten pakiet zawiera wszystkie pliki powiązane z ARCore.
  • **model—**pakiet zawiera pojedynczą klasę danych Place, która służy do hermetyzacji pojedynczego miejsca zwróconego przez interfejs Places API.
  • MainActivity.kt – pojedynczy Activity znajdujący się w aplikacji, który wyświetla mapę i widok kamery.

5. Konfigurowanie sceny

Dowiedz się więcej o podstawowych elementach aplikacji, zaczynając od elementów w rzeczywistości rozszerzonej.

Obiekt MainActivity zawiera obiekt SupportMapFragment, który obsługuje wyświetlanie obiektu mapy, oraz podklasę ArFragmentPlacesArFragment, która obsługuje scenę w rzeczywistości rozszerzonej.

Konfiguracja rzeczywistości rozszerzonej

Oprócz wyświetlania sceny w rzeczywistości rozszerzonej aplikacja PlacesArFragment obsługuje też prośby użytkownika o dostęp do aparatu, jeśli nie zostało to jeszcze zrobione. Możesz też poprosić o dodatkowe uprawnienia, zastępując metodę getAdditionalPermissions. Jeśli potrzebujesz też dostępu do lokalizacji, określ go i zastąp metodę getAdditionalPermissions:

class PlacesArFragment : ArFragment() {

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

Uruchom

Otwórz szkielet kodu w katalogu starter w Android Studio. Jeśli na pasku narzędzi klikniesz Uruchom > Uruchom „app'”, a następnie wdrożysz aplikację na urządzeniu lub w emulatorze, zobaczysz prośbę o włączenie dostępu do lokalizacji i aparatu. Kliknij Zezwól. Zobaczysz wtedy widok z aparatu i widok mapy obok siebie:

e3e3073d5c86f427.png

Wykrywanie samolotów

Gdy patrzysz w otoczenie, możesz zauważyć kilka białych kropek na poziomych powierzchniach, podobnych do białych kropek na dywanu.

2a9b6ea7dcb2e249.png

Te białe kropki to wskazówki opracowane przez ARCore, które informują o wykryciu płaszczyzny poziomej. Wykryte samoloty umożliwiają tworzenie tzw. &kotwic, dzięki czemu możesz umieszczać obiekty wirtualne w przestrzeni wirtualnej.

Więcej informacji o ARCore i sposobie, w jaki rozumie środowisko w pobliżu, znajdziesz w jego podstawowych koncepcjach.

6. Znajdź miejsca w pobliżu

Następnie trzeba uzyskać dostęp do bieżącej lokalizacji urządzenia i wyświetlić je, a następnie pobrać miejsca w pobliżu za pomocą interfejsu Places API.

Konfiguracja Map

Klucz interfejsu API Google Maps Platform

Wcześniej utworzyliśmy klucz interfejsu API Google Maps Platform, aby umożliwić wysyłanie zapytań do interfejsu Places API i możliwość korzystania z pakietu Maps SDK na Androida. Możesz otworzyć plik gradle.properties i zastąpić ciąg "YOUR API KEY HERE" utworzonym kluczem interfejsu API.

Wyświetlaj lokalizację urządzenia na mapie

Po dodaniu klucza interfejsu API dodaj pomocnika na mapie, aby pomóc w określaniu orientacji użytkowników względem tej mapy. Aby to zrobić, przejdź do metody setUpMaps i w trakcie wywołania mapFragment.getMapAsync ustaw googleMap.isMyLocationEnabled na true.. W ten sposób na mapie wyświetli się niebieska kropka.

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

Uzyskiwanie bieżącej lokalizacji

Aby poznać lokalizację urządzenia, musisz użyć klasy FusedLocationProviderClient. Uzyskanie takiego wystąpienia zostało już wykonane za pomocą metody onCreate metody MainActivity. Aby skorzystać z tego obiektu, wypełnij metodę getCurrentLocation, która akceptuje lambdę, tak by lokalizacja mogła zostać przekazana do wywołującego tę metodę.

Aby ukończyć tę metodę, przejdź do właściwości lastLocation obiektu FusedLocationProviderClient i dodaj addOnSuccessListener w ten sposób:

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

Metoda getCurrentLocation jest wywoływana z elementu lambda podanego w metodzie getMapAsync w metodzie setUpMaps, z której pobierane są miejsca w pobliżu.

Zainicjuj połączenie sieciowe

W wywołaniu metody getNearbyPlaces pamiętaj, że do metody placesServices.nearbyPlaces przekazywane są te parametry – klucz interfejsu API, lokalizacja urządzenia, promień w metrach (2 km) i typ miejsca (obecnie ustawiony na park).

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

Aby wywołać wywołanie sieciowe, przejdź do klucza interfejsu API zdefiniowanego w pliku gradle.properties. W pliku build.gradle w konfiguracji android > defaultConfig jest zdefiniowany ten fragment kodu:

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

Dzięki temu wartość zasobu ciągu znaków google_maps_key będzie dostępna podczas kompilacji.

Aby zakończyć wywołanie sieciowe, możesz po prostu odczytać ten zasób ciągu znaków w obiekcie getString w obiekcie Context.

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

7. Miejsca w AR

Do tej pory zostały przez Ciebie wykonane następujące czynności:

  1. Poprosił użytkownika o dostęp do aparatu i lokalizacji przy pierwszym uruchomieniu aplikacji
  2. Skonfiguruj ARCore, aby zacząć śledzić samoloty w poziomie
  3. Konfigurowanie pakietu SDK Map za pomocą klucza interfejsu API
  4. Otrzymano aktualną lokalizację urządzenia
  5. Pobrano informacje o miejscach w pobliżu (w szczególności parkach) przy użyciu interfejsu Places API.

Ostatnim krokiem na koniec ćwiczenia jest umieszczenie miejsc w rzeczywistości rozszerzonej.

Zrozumienie scenerii

ARCore jest w stanie zrozumieć rzeczywisty świat dzięki kamerze urządzenia, wykrywając ciekawe i odrębne punkty nazywane punktami cech w każdej klatce. Gdy te punkty cech są zgrupowane i wyglądają na leżące na wspólnej płaszczyźnie poziomej, takiej jak tabele i piętra, ARCore może udostępnić tę aplikację jako platformę poziomą.

Jak widzisz, ARCore pomaga kierować użytkownika po wykryciu samolotu za pomocą białych kropek.

2a9b6ea7dcb2e249.png

Dodawanie kotwic

Po wykryciu samolotu możesz dołączyć do niego obiekt o nazwie kotwica. Za pomocą kotwicy możesz umieścić obiekty wirtualne i zagwarantować, że pozostaną tam w tym samym miejscu. Możesz zmienić kod, aby dołączyć go po wykryciu samolotu.

W: setUpAr do elementu PlacesArFragment jest dołączony OnTapArPlaneListener. Ten detektor jest wywoływany za każdym razem, gdy na scenie AR jest klikany samolot. Podczas tej rozmowy możesz utworzyć Anchor i AnchorNode z podanego HitResult w detektorze:

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

AnchorNode to miejsce, w którym dołączasz obiekty węzłów podrzędnych – PlaceNode – w scenie, która jest przetwarzana w wywołaniu metody addPlaces.

Uruchom

Jeśli uruchamiasz aplikację z tymi modyfikacjami, rozejrzyj się wokół i poczekaj, aż zostanie wykryty samolot. Kliknij białe kropki wskazujące samolot. Gdy to zrobisz, na mapie powinny się pojawić znaczniki wszystkich pobliskich parków. Można jednak zauważyć, że obiekty wirtualne utknęły na utworzonej kotwicy i nie zostały umieszczone względem miejsca w parku.

F93eb87c98a0098d.png

W ostatnim kroku naprawisz to, używając biblioteki SDK Maps na Androida i narzędzia SensorManager na urządzeniu.

8. Pozycjonowanie miejsc

Aby móc umieścić ikonę wirtualnego miejsca w rzeczywistości rozszerzonej na dokładnym nagłówku, musisz podać dwie informacje:

  • Gdzie jest północ
  • Kąt między północą a poszczególnymi miejscami

Określanie kierunku na północ

Północ można określić za pomocą czujników pozycji (geomagnetyczne i akcelerometr) dostępnych w urządzeniu. Te dwa czujniki zbierają w czasie rzeczywistym informacje o pozycji urządzenia w przestrzeni kosmicznej. Więcej informacji o czujnikach położenia znajdziesz w artykule Obliczanie orientacji urządzenia.

Aby uzyskać dostęp do tych czujników, musisz uzyskać SensorManager, a następnie zarejestrować SensorEventListener dla tych czujników. Te kroki są już wykonane za pomocą metod cyklu życia MainActivity&#39:

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

W metodzie onSensorChanged udostępniany jest obiekt SensorEvent zawierający szczegółowe informacje o danym punkcie odniesienia w miarę jego upływu czasu. Możesz dodać kod do tej metody:

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

Powyższy kod sprawdza typ czujnika i w zależności od typu aktualizuje odpowiednie odczytu z czujnika (akcelerometr lub magnetometr). Na podstawie tych odczytów z czujników można teraz określić, ile stopni w kierunku północnym jest w porównaniu z urządzeniem (wartość orientationAngles[0]).

Nagłówek sferyczny

Gdy już ustalisz kierunek północny, kolejnym krokiem jest określenie kąta między północą i każdym miejscem oraz wykorzystanie tych informacji do położenia miejsc w odpowiednim kierunku w rzeczywistości rozszerzonej.

Do obliczenia nagłówka użyj pakietu SDK Map Google na Androida, który zawiera szereg funkcji pomocnych w obliczaniu odległości i nagłówków za pomocą geometrii sferycznej. Więcej informacji znajdziesz w tym omówieniu biblioteki.

Następnie użyjesz metody sphericalHeading w bibliotece narzędzi, która oblicza nagłówek lub wartość 2 obiektów LatLng. Te informacje są potrzebne w ramach metody getPositionVector zdefiniowanej w narzędziu Place.kt. Ta metoda spowoduje zwrócenie obiektu Vector3, który będzie używany przez każdy obiekt PlaceNode jako jego pozycję lokalną w przestrzeni AR.

Możesz zastąpić definicję nagłówka tą metodą:

val heading = latLng.sphericalHeading(placeLatLng)

Powinno to spowodować podanie następującej metody:

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

Pozycja lokalna

Ostatnim krokiem do poprawnej orientacji miejsc w AR jest użycie wyniku getPositionVector podczas dodawania obiektów PlaceNode do sceny. Przejdź do sekcji addPlaces w MainActivity, tuż pod wierszem, w którym znajduje się element nadrzędny (każdy z nich poniżej placeNode.setParent(anchorNode)). Ustaw localPosition urządzenia placeNode jako wynik wywołania getPositionVector:

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

Domyślnie metoda getPositionVector ustawia odległość y węzła na 1 metr, określoną przez wartość y w metodzie getPositionVector. Jeśli chcesz dostosować tę odległość, powiedz na przykład 2 metry, zmień wartość w razie potrzeby.

Po tej zmianie dodane obiekty PlaceNode powinny być teraz umieszczone we właściwym nagłówku. Teraz uruchom aplikację, aby sprawdzić wynik.

9. Gratulacje

Gratulujemy! Tak daleko!

Więcej informacji