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.
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
- Android Studio 2020.3.1 lub nowszy
- komputer deweloperski obsługujący OpenGL ES 3.0 lub nowszy;
- urządzenie obsługujące ARCore lub emulator Androida z włączoną obsługą ARCore (instrukcje znajdziesz w następnym kroku);
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:
- Otwórz Menedżera SDK, klikając Narzędzia > Menedżer SDK.
- 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.
- 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**.**
Wymagane interfejsy API
W kroku 3 w sekcji poniżej włącz Pakiet SDK Map Google na Androida i interfejs 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:
- Utwórz konto rozliczeniowe.
- Utwórz projekt.
- Włącz interfejsy API i pakiety SDK Google Maps Platform (wymienione w poprzedniej sekcji).
- 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
, czyliplay-services-location
iplay-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ę ArFragment
—PlacesArFragment
—, 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:
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.
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:
- Poproszenie użytkownika o przyznanie uprawnień do kamery i lokalizacji przy pierwszym uruchomieniu aplikacji
- Konfigurowanie ARCore w celu rozpoczęcia śledzenia płaszczyzn poziomych
- Konfigurowanie pakietu Maps SDK za pomocą klucza interfejsu API
- Określanie bieżącej lokalizacji urządzenia
- 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.
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ć Anchor
i AnchorNode
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!!)
}
W 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.
W ostatnim kroku skoryguj to, używając biblioteki narzędziowej pakietu Maps SDK na Androida i SensorManager 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 addPlaces
w MainActivity
, 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!