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

1. Zanim zaczniesz

Streszczenie

Z tego ćwiczenia w Codelabs dowiesz się, jak używać danych z Google Maps Platform do wyświetlania miejsc w pobliżu w rzeczywistości rozszerzonej (AR) na Androidzie.

2344909dd9a52c60.png

Wymagania wstępne

  • Podstawowa wiedza na temat tworzenia aplikacji na Androida w Android Studio
  • znajomość języka Kotlin,

Czego się nauczysz

  • Poproś użytkownika o zezwolenie na dostęp do aparatu i lokalizacji urządzenia.
  • Zintegruj z Places API, aby pobierać pobliskie miejsca w pobliżu lokalizacji urządzenia.
  • Przeprowadź integrację z ARCore, aby znajdować poziome powierzchnie płaskie, dzięki czemu wirtualne obiekty można zakotwiczać i umieszczać w przestrzeni 3D za pomocą Sceneform.
  • Zbieraj informacje o położeniu urządzenia w przestrzeni za pomocą interfejsu SensorManager i używaj biblioteki narzędziowej pakietu Maps SDK na Androida, aby umieszczać obiekty wirtualne we właściwym kierunku.

Czego potrzebujesz

2. Konfiguracja

Android Studio

W tym laboratorium kodowania używamy Androida 10.0 (poziom interfejsu API 29) i wymagamy, aby w Android Studio były zainstalowane Usługi Google Play. Aby zainstalować oba te komponenty, wykonaj te czynności:

  1. Otwórz Menedżera SDK, klikając Narzędzia > Menedżer SDK.

6c44a9cb9cf6c236.png

  1. Sprawdź, czy masz zainstalowany system Android 10.0. Jeśli nie, zainstaluj go, zaznaczając pole wyboru obok opcji Android 10.0 (Q), a następnie kliknij OK i ponownie OK w wyświetlonym oknie.

368f17a974c75c73.png

  1. Na koniec zainstaluj Usługi Google Play. W tym celu otwórz kartę SDK Tools, zaznacz pole wyboru obok pozycji Google Play services, kliknij OK, a potem jeszcze raz wybierz OK w wyświetlonym oknie**.**

497a954b82242f4b.png

Wymagane interfejsy API

W kroku 3 w sekcji poniżej włącz Pakiet SDK Map Google na Androidainterfejs Places API na potrzeby tego laboratorium.

Pierwsze kroki z Google Maps Platform

Jeśli nie korzystasz jeszcze z Google Maps Platform, wykonaj te czynności, korzystając z przewodnika Wprowadzenie do Google Maps Platform lub z playlisty Wprowadzenie do Google Maps Platform:

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

Opcjonalnie: Android Emulator

Jeśli nie masz urządzenia obsługującego ARCore, możesz użyć emulatora Androida, aby symulować scenę AR i fałszować lokalizację urządzenia. W tym ćwiczeniu będziesz też używać Sceneform, więc musisz wykonać czynności opisane w sekcji „Konfigurowanie emulatora do obsługi Sceneform”.

3. Szybki start

Aby jak najszybciej rozpocząć pracę, przygotowaliśmy kod początkowy, który pomoże Ci w tym samouczku. Możesz przejść od razu do rozwiązania, ale jeśli chcesz zobaczyć wszystkie kroki, czytaj dalej.

Jeśli masz zainstalowany program git, możesz sklonować repozytorium.

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 otrzymaniu kodu otwórz projekt znajdujący się w katalogu starter.

4. Opis projektu

Zapoznaj się z kodem pobranym w poprzednim kroku. W tym repozytorium powinien znajdować się jeden moduł o nazwie app, który zawiera pakiet com.google.codelabs.findnearbyplacesar.

AndroidManifest.xml

W pliku AndroidManifest.xml zadeklarowano te atrybuty, aby umożliwić Ci korzystanie z funkcji wymaganych w tym laboratorium:

<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 elementu uses-permission, który określa, jakie uprawnienia musi przyznać użytkownik, zanim będzie mógł korzystać z odpowiednich funkcji, zadeklarowano te uprawnienia:

  • android.permission.INTERNET– dzięki temu aplikacja może wykonywać operacje sieciowe i pobierać dane z internetu, np. informacje o miejscach za pomocą interfejsu Places API.
  • android.permission.CAMERA – wymagany jest dostęp do aparatu, aby można było używać aparatu urządzenia do wyświetlania obiektów w rzeczywistości rozszerzonej.
  • android.permission.ACCESS_FINE_LOCATION– dostęp do lokalizacji jest potrzebny, aby można było pobierać informacje o miejscach w pobliżu urządzenia.

W przypadku elementu uses-feature, który określa, jakie funkcje sprzętowe są potrzebne tej aplikacji, zadeklarowano te elementy:

  • Wymagana jest wersja OpenGL ES 3.0.
  • Wymagane jest urządzenie obsługujące ARCore.

Dodatkowo w obiekcie aplikacji dodawane są 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 metadanych wskazuje, że ARCore jest wymagany do uruchomienia tej aplikacji, a drugi określa, jak przekazać klucz interfejsu API Google Maps Platform do pakietu SDK Map na Androida.

build.gradle

W pliku 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 zależności:

  • Biblioteki z identyfikatorem grupy com.google.android.gms, czyli play-services-locationplay-services-maps, są używane do uzyskiwania dostępu do informacji o lokalizacji urządzenia i funkcji związanych z Mapami Google.
  • com.google.maps.android:maps-utils-ktx to biblioteka rozszerzeń Kotlin (KTX) do biblioteki narzędziowej pakietu Maps SDK na Androida. Funkcja ta będzie używana w tej bibliotece do późniejszego pozycjonowania wirtualnych obiektów w przestrzeni rzeczywistej.
  • com.google.ar.sceneform.ux:sceneform-ux to biblioteka Sceneform, która umożliwia renderowanie realistycznych scen 3D bez konieczności uczenia się OpenGL.
  • Zależności w identyfikatorze grupy com.squareup.retrofit2 to zależności Retrofit, które umożliwiają szybkie napisanie klienta HTTP do interakcji z interfejsem Places API.

Struktura projektu

Znajdziesz tu te pakiety i pliki:

  • **api** – ten pakiet zawiera klasy, które służą do interakcji z interfejsem Places API za pomocą Retrofit.
  • **ar—**ten pakiet zawiera wszystkie pliki związane z ARCore.
  • **model** – ten pakiet zawiera pojedynczą klasę danych Place, która służy do enkapsulacji pojedynczego miejsca zwracanego przez interfejs Places API.
  • MainActivity.kt – jest to pojedyncza Activity zawarta w aplikacji, która będzie wyświetlać mapę i widok z kamery.

5. Konfigurowanie sceny

Zacznij od podstawowych komponentów aplikacji, w tym elementów rzeczywistości rozszerzonej.

MainActivity zawiera SupportMapFragment, który odpowiada za wyświetlanie obiektu mapy, oraz podklasę ArFragmentPlacesArFragment—, która odpowiada za wyświetlanie sceny rzeczywistości rozszerzonej.

Konfiguracja rzeczywistości rozszerzonej

Oprócz wyświetlania sceny rzeczywistości rozszerzonej PlacesArFragment będzie też obsługiwać prośby o zezwolenie na dostęp do aparatu, jeśli użytkownik nie udzielił jeszcze takiego zezwolenia. O dodatkowe uprawnienia można też poprosić, zastępując metodę getAdditionalPermissions. Ponieważ musisz też przyznać uprawnienia do lokalizacji, określ to i zastąp metodę getAdditionalPermissions:

class PlacesArFragment : ArFragment() {

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

Uruchom

Otwórz kod szkieletowy w katalogu starter w Android Studio. Jeśli na pasku narzędzi klikniesz Uruchom > Uruchom „aplikację” i wdrożysz aplikację na urządzeniu lub emulatorze, najpierw pojawi się prośba o włączenie zezwoleń na dostęp do lokalizacji i aparatu. Kliknij Zezwól. Powinny się wtedy pojawić widok z kamery i widok mapy obok siebie, jak na tym obrazie:

e3e3073d5c86f427.png

Wykrywanie samolotów

Gdy rozejrzysz się po otoczeniu za pomocą kamery, możesz zauważyć kilka białych kropek nałożonych na poziome powierzchnie, podobnie jak na dywanie na tym obrazie.

2a9b6ea7dcb2e249.png

Te białe kropki to wskazówki dostarczane przez ARCore, które informują o wykryciu płaszczyzny poziomej. Wykryte płaszczyzny pozwalają utworzyć tzw. „punkt zakotwiczenia”, dzięki któremu możesz umieszczać wirtualne obiekty w rzeczywistej przestrzeni.

Więcej informacji o ARCore i sposobie, w jaki ta technologia rozpoznaje otoczenie, znajdziesz w artykule o jej podstawowych koncepcjach.

6. Pobieranie informacji o miejscach w pobliżu

Następnie musisz uzyskać dostęp do aktualnej lokalizacji urządzenia i ją wyświetlić, a potem pobrać pobliskie miejsca za pomocą interfejsu Places API.

Konfigurowanie 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 korzystanie z pakietu Maps SDK na Androida. Otwórz plik gradle.properties i zastąp ciąg znaków "YOUR API KEY HERE" utworzonym kluczem interfejsu API.

Wyświetlanie lokalizacji urządzenia na mapie

Po dodaniu klucza interfejsu API dodaj na mapie element pomocniczy, który pomoże użytkownikom zorientować się, gdzie znajdują się na mapie. Aby to zrobić, przejdź do metody setUpMaps i w wywołaniu mapFragment.getMapAsync ustaw wartość googleMap.isMyLocationEnabled na true.. Spowoduje to wyświetlenie niebieskiej kropki na mapie.

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

Pobieranie bieżącej lokalizacji

Aby uzyskać lokalizację urządzenia, musisz użyć klasy FusedLocationProviderClient. Wystąpienie tego obiektu zostało już uzyskane w metodzie onCreate klasy MainActivity. Aby użyć tego obiektu, wypełnij metodę getCurrentLocation, która akceptuje argument lambda, dzięki czemu lokalizacja może zostać przekazana do wywołującego tę metodę.

Aby zastosować tę metodę, możesz uzyskać dostęp do właściwości lastLocation obiektu FusedLocationProviderClient, a następnie dodać 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 poziomu funkcji lambda podanej w metodzie getMapAsync w metodzie setUpMaps, z której pobierane są miejsca w pobliżu.

Inicjowanie połączenia sieciowego dotyczącego miejsc

W wywołaniu metody getNearbyPlaces zwróć uwagę, że do metody placesServices.nearbyPlaces przekazywane są te parametry: klucz interfejsu API, lokalizacja urządzenia, promień w metrach (ustawiony na 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 dokończyć wywołanie sieciowe, przekaż klucz interfejsu API zdefiniowany w pliku gradle.properties. Ten fragment kodu jest zdefiniowany w pliku build.gradle w konfiguracji android > defaultConfig:

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

Spowoduje to udostępnienie wartości zasobu ciągu znaków google_maps_key w czasie kompilacji.

Aby zrealizować wywołanie sieciowe, możesz po prostu odczytać ten zasób tekstowy za pomocą getString w obiekcie Context.

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

7. Miejsca w AR

Do tej pory wykonano te czynności:

  1. Poproszenie użytkownika o przyznanie uprawnień do kamery i lokalizacji przy pierwszym uruchomieniu aplikacji
  2. Konfigurowanie ARCore w celu rozpoczęcia śledzenia płaszczyzn poziomych
  3. Konfigurowanie pakietu Maps SDK za pomocą klucza interfejsu API
  4. Określanie bieżącej lokalizacji urządzenia
  5. Pobieranie pobliskich miejsc (zwłaszcza parków) za pomocą interfejsu Places API

Ostatnim krokiem w tym ćwiczeniu jest umieszczenie pobranych miejsc w rzeczywistości rozszerzonej.

Rozumienie sceny

ARCore rozpoznaje scenę w rzeczywistym świecie za pomocą kamery urządzenia, wykrywając w każdej klatce obrazu ciekawe i wyróżniające się punkty, zwane punktami charakterystycznymi. Gdy te punkty charakterystyczne są zgrupowane i wydają się leżeć na wspólnej płaszczyźnie poziomej, np. na stołach i podłogach, ARCore może udostępnić tę funkcję aplikacji jako płaszczyznę poziomą.

Jak już wspomnieliśmy, ARCore pomaga użytkownikowi, gdy wykryje płaszczyznę, wyświetlając białe kropki.

2a9b6ea7dcb2e249.png

Dodawanie kotwic

Po wykryciu płaszczyzny możesz dołączyć do niej obiekt zwany punktem zaczepienia. Za pomocą kotwicy możesz umieszczać wirtualne obiekty i mieć pewność, że będą one zawsze w tym samym miejscu w przestrzeni. Zmodyfikuj kod, aby dołączyć go po wykryciu samolotu.

W modelu setUpAr element OnTapArPlaneListener jest przymocowany do elementu PlacesArFragment. Ten detektor jest wywoływany za każdym razem, gdy w scenie AR zostanie dotknięta płaszczyzna. W ramach tego wywołania możesz utworzyć AnchorAnchorNode z podanego HitResult w obiekcie nasłuchującym w ten sposób:

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

AnchorNode będziesz dołączać obiekty węzłów podrzędnych – instancje PlaceNode – w scenie, co jest obsługiwane w wywołaniu metody addPlaces.

Uruchom

Jeśli uruchomisz aplikację z powyższymi modyfikacjami, rozejrzyj się, aż zostanie wykryty samolot. Kliknij białe kropki wskazujące samolot. Na mapie powinny się teraz wyświetlić znaczniki wszystkich najbliższych parków. Jak jednak widać, obiekty wirtualne są przytwierdzone do utworzonego punktu zakotwiczenia, a nie umieszczone w miejscach, w których znajdują się te parki.

f93eb87c98a0098d.png

W ostatnim kroku skoryguj to, używając biblioteki narzędziowej pakietu Maps SDK na AndroidaSensorManager na urządzeniu.

8. Pozycjonowanie miejsc

Aby umieścić ikonę wirtualnego miejsca w rzeczywistości rozszerzonej w odpowiednim kierunku, potrzebujesz 2 informacji:

  • gdzie jest północ geograficzna,
  • kąt między północą a każdym miejscem,

Określanie kierunku północnego

Kierunek północny można określić za pomocą czujników położenia (geomagnetycznego i akcelerometru) dostępnych na urządzeniu. Za pomocą tych 2 czujników możesz zbierać informacje w czasie rzeczywistym o położeniu urządzenia w przestrzeni. 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ć na nich SensorEventListener. Te czynności są już wykonywane w metodach cyklu życia MainActivity:

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, który zawiera szczegóły dotyczące danych z danego czujnika, które zmieniają się z upływem czasu. Dodaj do tej metody ten kod:

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 niego aktualizuje odpowiedni odczyt (akcelerometru lub magnetometru). Na podstawie odczytów z tych czujników można określić, o ile stopni od północy jest odchylone urządzenie (czyli wartość orientationAngles[0]).

Nagłówek sferyczny

Po określeniu północy kolejnym krokiem jest wyznaczenie kąta między północą a każdym miejscem, a następnie wykorzystanie tych informacji do umieszczenia miejsc we właściwym kierunku w rzeczywistości rozszerzonej.

Aby obliczyć kierunek, użyjesz biblioteki narzędziowej pakietu Maps SDK na Androida, która zawiera kilka funkcji pomocniczych do obliczania odległości i kierunkó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 kierunek/kurs między 2 obiektami LatLng. Te informacje są potrzebne w metodzie getPositionVector zdefiniowanej w Place.kt. Ta metoda zwróci ostatecznie obiekt Vector3, który będzie następnie używany przez każdy obiekt PlaceNode jako jego lokalna pozycja w przestrzeni AR.

Zastąp definicję nagłówka w tej metodzie tym kodem:

val heading = latLng.sphericalHeading(placeLatLng)

Powinno to spowodować taką definicję 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, który pozwala prawidłowo zorientować miejsca w rzeczywistości rozszerzonej, jest użycie wyniku getPositionVector podczas dodawania obiektów PlaceNode do sceny. Przejdź do addPlacesMainActivity, tuż pod wierszem, w którym ustawiono element nadrzędny w każdym placeNode (tuż pod placeNode.setParent(anchorNode)). Ustaw localPosition elementu placeNode na wynik wywołania getPositionVector w ten sposób:

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

Domyślnie metoda getPositionVector ustawia odległość węzła w osi Y na 1 metr, zgodnie z wartością y w metodzie getPositionVector. Jeśli chcesz dostosować tę odległość, np. do 2 metrów, zmień odpowiednio tę wartość.

Po wprowadzeniu tej zmiany dodane obiekty PlaceNode powinny być teraz zorientowane we właściwym kierunku. Teraz uruchom aplikację, aby zobaczyć wynik.

9. Gratulacje

Gratulujemy zajścia tak daleko!

Więcej informacji