1. Trước khi bắt đầu
Trừu tượng
Lớp học lập trình này hướng dẫn bạn cách sử dụng dữ liệu từ Nền tảng Google Maps để hiển thị các địa điểm lân cận ở chế độ thực tế tăng cường (AR) trên Android.
Điều kiện tiên quyết
- Có kiến thức cơ bản về hoạt động phát triển Android bằng Android Studio
- Quen thuộc với Kotlin
Kiến thức bạn sẽ học được
- Yêu cầu người dùng cấp quyền truy cập vào camera và thông tin vị trí của thiết bị.
- Tích hợp với Places API để tìm nạp các địa điểm lân cận xung quanh vị trí của thiết bị.
- Tích hợp với ARCore để tìm các bề mặt phẳng ngang, nhờ đó các đối tượng ảo có thể được cố định và đặt trong không gian 3D bằng Sceneform.
- Thu thập thông tin về vị trí của thiết bị trong không gian bằng SensorManager và sử dụng Thư viện tiện ích Maps SDK cho Android để đặt các đối tượng ảo ở tiêu đề chính xác.
Bạn cần có
- Android Studio 2020.3.1 trở lên
- Một máy phát triển hỗ trợ OpenGL ES 3.0 trở lên
- Một thiết bị hỗ trợ ARCore hoặc Trình mô phỏng Android có hỗ trợ ARCore (hướng dẫn được cung cấp trong bước tiếp theo)
2. Bắt đầu thiết lập
Android Studio
Lớp học lập trình này sử dụng Android 10.0 (API cấp 29) và yêu cầu bạn cài đặt Dịch vụ Google Play trong Android Studio. Để cài đặt cả hai phần phụ thuộc này, hãy hoàn tất các bước sau:
- Chuyển đến Trình quản lý SDK. Bạn có thể truy cập vào trình quản lý này bằng cách nhấp vào Tools (Công cụ) > SDK Manager (Trình quản lý SDK).
- Kiểm tra xem bạn đã cài đặt Android 10.0 hay chưa. Nếu chưa, hãy cài đặt bằng cách chọn hộp đánh dấu bên cạnh Android 10.0 (Q), sau đó nhấp vào OK rồi nhấp lại vào OK trong hộp thoại xuất hiện.
- Cuối cùng, hãy cài đặt Dịch vụ Google Play bằng cách chuyển đến thẻ SDK Tools (Bộ công cụ SDK), chọn hộp đánh dấu bên cạnh Dịch vụ Google Play, nhấp vào OK, sau đó chọn OK một lần nữa trong hộp thoại xuất hiện**.**
Các API bắt buộc
Trong Bước 3 của phần sau, hãy bật SDK Maps dành cho Android và Places API cho lớp học lập trình này.
Bắt đầu sử dụng Nền tảng Google Maps
Nếu bạn chưa từng sử dụng Nền tảng Google Maps, hãy làm theo hướng dẫn Bắt đầu sử dụng Nền tảng Google Maps hoặc xem danh sách phát Bắt đầu sử dụng Nền tảng Google Maps để hoàn tất các bước sau:
- Tạo tài khoản thanh toán.
- Tạo dự án.
- Bật các API và SDK của Nền tảng Google Maps (được liệt kê trong phần trước).
- Tạo khoá API.
Không bắt buộc: Trình mô phỏng Android
Nếu không có thiết bị hỗ trợ ARCore, bạn có thể sử dụng Trình mô phỏng Android để mô phỏng cảnh thực tế tăng cường cũng như giả mạo vị trí của thiết bị. Vì bạn cũng sẽ sử dụng Sceneform trong bài tập này, nên bạn cũng cần đảm bảo làm theo các bước trong phần "Định cấu hình trình mô phỏng để hỗ trợ Sceneform".
3. Bắt đầu nhanh
Để giúp bạn bắt đầu nhanh nhất có thể, sau đây là một số mã khởi đầu giúp bạn theo dõi lớp học lập trình này. Bạn có thể chuyển ngay đến giải pháp, nhưng nếu muốn xem tất cả các bước, hãy tiếp tục đọc.
Bạn có thể sao chép kho lưu trữ nếu đã cài đặt git
.
git clone https://github.com/googlecodelabs/display-nearby-places-ar-android.git
Ngoài ra, bạn có thể nhấp vào nút bên dưới để tải mã nguồn xuống.
Sau khi nhận được mã, hãy mở dự án trong thư mục starter
.
4. Tổng quan dự án
Khám phá mã bạn đã tải xuống ở bước trước. Trong kho lưu trữ này, bạn sẽ thấy một mô-đun duy nhất có tên là app
, chứa gói com.google.codelabs.findnearbyplacesar
.
AndroidManifest.xml
Các thuộc tính sau được khai báo trong tệp AndroidManifest.xml
để cho phép bạn sử dụng các tính năng bắt buộc trong lớp học lập trình này:
<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" />
Đối với uses-permission
, chỉ định những quyền mà người dùng cần cấp trước khi có thể sử dụng các chức năng đó, bạn cần khai báo những quyền sau:
android.permission.INTERNET
– để ứng dụng của bạn có thể thực hiện các thao tác mạng và tìm nạp dữ liệu qua Internet, chẳng hạn như thông tin về địa điểm thông qua Places API.android.permission.CAMERA
– bạn cần cấp quyền truy cập camera để có thể dùng camera của thiết bị hiển thị các đối tượng ở chế độ thực tế tăng cường.android.permission.ACCESS_FINE_LOCATION
– bạn cần có quyền truy cập vào vị trí để có thể tìm nạp những địa điểm ở gần so với vị trí của thiết bị.
Đối với uses-feature
, chỉ định những tính năng phần cứng mà ứng dụng này cần, bạn phải khai báo những nội dung sau:
- Bạn phải dùng OpenGL ES phiên bản 3.0.
- Bạn phải có thiết bị hỗ trợ ARCore.
Ngoài ra, các thẻ siêu dữ liệu sau đây sẽ được thêm vào đối tượng ứng dụng:
<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>
Mục siêu dữ liệu đầu tiên là để cho biết rằng ARCore là một yêu cầu để ứng dụng này chạy và mục thứ hai là cách bạn cung cấp khoá API Google Maps Platform cho Maps SDK cho Android.
build.gradle
Trong build.gradle
, các phần phụ thuộc bổ sung sau đây được chỉ định:
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"
}
Sau đây là nội dung mô tả ngắn gọn về từng phần phụ thuộc:
- Các thư viện có mã nhóm
com.google.android.gms
, cụ thể làplay-services-location
vàplay-services-maps
, được dùng để truy cập thông tin vị trí của thiết bị và truy cập chức năng liên quan đến Google Maps. com.google.maps.android:maps-utils-ktx
là thư viện tiện ích Kotlin (KTX) cho Thư viện tiện ích Maps SDK cho Android. Chức năng này sẽ được dùng trong thư viện này để sau này định vị các đối tượng ảo trong không gian thực.com.google.ar.sceneform.ux:sceneform-ux
là thư viện Sceneform. Thư viện này sẽ cho phép bạn kết xuất các cảnh 3D chân thực mà không cần phải tìm hiểu về OpenGL.- Các phần phụ thuộc trong mã nhận dạng nhóm
com.squareup.retrofit2
là các phần phụ thuộc Retrofit. Các phần phụ thuộc này cho phép bạn nhanh chóng viết một ứng dụng HTTP để tương tác với Places API.
Cấu trúc dự án
Tại đây, bạn sẽ thấy các gói và tệp sau:
- **api** – gói này chứa các lớp dùng để tương tác với Places API bằng Retrofit.
- **ar** – gói này chứa tất cả các tệp liên quan đến ARCore.
- **model** – gói này chứa một lớp dữ liệu duy nhất
Place
, dùng để đóng gói một địa điểm duy nhất do Places API trả về. - MainActivity.kt – Đây là
Activity
duy nhất có trong ứng dụng của bạn, sẽ hiển thị một bản đồ và một khung hiển thị camera.
5. Thiết lập cảnh
Khám phá các thành phần cốt lõi của ứng dụng, bắt đầu từ các thành phần thực tế tăng cường.
MainActivity
chứa SupportMapFragment
(xử lý việc hiển thị đối tượng bản đồ) và một lớp con của ArFragment
– PlacesArFragment
– (xử lý việc hiển thị cảnh thực tế tăng cường).
Thiết lập thực tế tăng cường
Ngoài việc hiển thị cảnh thực tế tăng cường, PlacesArFragment
cũng sẽ xử lý yêu cầu cấp quyền truy cập camera từ người dùng nếu chưa được cấp. Bạn cũng có thể yêu cầu thêm quyền bằng cách ghi đè phương thức getAdditionalPermissions
. Vì bạn cũng cần được cấp quyền truy cập vào vị trí, hãy chỉ định quyền đó và ghi đè phương thức getAdditionalPermissions
:
class PlacesArFragment : ArFragment() {
override fun getAdditionalPermissions(): Array<String> =
listOf(Manifest.permission.ACCESS_FINE_LOCATION)
.toTypedArray()
}
Chạy ứng dụng
Tiếp tục mở mã khung trong thư mục starter
trong Android Studio. Nếu nhấp vào Run (Chạy) > Run ‘app' (Chạy "ứng dụng") trên thanh công cụ và triển khai ứng dụng cho thiết bị hoặc trình mô phỏng, trước tiên, bạn sẽ được nhắc bật quyền truy cập vào vị trí và camera. Hãy nhấp vào Cho phép. Sau khi nhấp, bạn sẽ thấy chế độ xem camera và chế độ xem bản đồ xuất hiện song song như sau:
Phát hiện máy bay
Khi dùng camera để quan sát môi trường xung quanh, bạn có thể nhận thấy một vài chấm trắng xuất hiện trên các bề mặt ngang, giống như các chấm trắng trên thảm trong hình ảnh này.
Những chấm trắng này là các đường hướng dẫn do ARCore cung cấp để cho biết rằng một mặt phẳng ngang đã được phát hiện. Các mặt phẳng được phát hiện này cho phép bạn tạo một "điểm neo" để có thể đặt các đối tượng ảo vào không gian thực.
Để biết thêm thông tin về ARCore và cách ARCore nhận biết môi trường xung quanh bạn, hãy đọc về các khái niệm cơ bản của ARCore.
6. Tìm địa điểm lân cận
Tiếp theo, bạn cần truy cập và hiển thị vị trí hiện tại của thiết bị, sau đó tìm nạp các địa điểm lân cận bằng Places API.
Thiết lập Maps
Khoá API Nền tảng Google Maps
Trước đó, bạn đã tạo một khoá API Nền tảng Google Maps để cho phép truy vấn Places API và có thể sử dụng Maps SDK cho Android. Tiếp theo, hãy mở tệp gradle.properties
rồi thay thế chuỗi "YOUR API KEY HERE"
bằng khoá API mà bạn đã tạo.
Hiển thị vị trí của thiết bị trên bản đồ
Sau khi thêm khoá API, hãy thêm một đối tượng hỗ trợ vào bản đồ để giúp người dùng định hướng vị trí của họ so với bản đồ. Để làm như vậy, hãy chuyển đến phương thức setUpMaps
và bên trong lệnh gọi mapFragment.getMapAsync
, hãy đặt googleMap.isMyLocationEnabled
thành true.
. Thao tác này sẽ cho thấy dấu chấm màu xanh dương trên bản đồ.
private fun setUpMaps() {
mapFragment.getMapAsync { googleMap ->
googleMap.isMyLocationEnabled = true
// ...
}
}
Lấy vị trí hiện tại
Để lấy vị trí của thiết bị, bạn cần sử dụng lớp FusedLocationProviderClient
. Việc lấy một thực thể của đối tượng này đã được thực hiện trong phương thức onCreate
của MainActivity
. Để sử dụng đối tượng này, hãy điền vào phương thức getCurrentLocation
. Phương thức này chấp nhận một đối số lambda để có thể truyền vị trí cho phương thức gọi của phương thức này.
Để hoàn tất phương thức này, bạn có thể truy cập vào thuộc tính lastLocation
của đối tượng FusedLocationProviderClient
, sau đó thêm addOnSuccessListener
như sau:
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
currentLocation = location
onSuccess(location)
}.addOnFailureListener {
Log.e(TAG, "Could not get location")
}
Phương thức getCurrentLocation
được gọi từ trong hàm lambda được cung cấp trong getMapAsync
trong phương thức setUpMaps
mà từ đó các địa điểm lân cận được tìm nạp.
Bắt đầu cuộc gọi mạng lưới địa điểm
Trong lệnh gọi phương thức getNearbyPlaces
, hãy lưu ý rằng các tham số sau được truyền vào phương thức placesServices.nearbyPlaces
– khoá API, vị trí của thiết bị, bán kính tính bằng mét (được đặt thành 2 km) và loại địa điểm (hiện được đặt thành park
).
val apiKey = "YOUR API KEY"
placesService.nearbyPlaces(
apiKey = apiKey,
location = "${location.latitude},${location.longitude}",
radiusInMeters = 2000,
placeType = "park"
)
Để hoàn tất lệnh gọi mạng, hãy chuyển khoá API mà bạn đã xác định trong tệp gradle.properties
. Đoạn mã sau được xác định trong tệp build.gradle
trong cấu hình android > defaultConfig:
android {
defaultConfig {
resValue "string", "google_maps_key", (project.findProperty("GOOGLE_MAPS_API_KEY") ?: "")
}
}
Thao tác này sẽ giúp giá trị tài nguyên chuỗi google_maps_key
có sẵn tại thời điểm tạo.
Để hoàn tất lệnh gọi mạng, bạn chỉ cần đọc tài nguyên chuỗi này thông qua getString
trên đối tượng Context
.
val apiKey = this.getString(R.string.google_maps_key)
7. Địa điểm ở chế độ thực tế tăng cường
Cho đến nay, bạn đã thực hiện những việc sau:
- Yêu cầu người dùng cấp quyền truy cập camera và vị trí khi chạy ứng dụng lần đầu
- Thiết lập ARCore để bắt đầu theo dõi các mặt phẳng ngang
- Thiết lập Maps SDK bằng khoá API
- Đã nhận được vị trí hiện tại của thiết bị
- Tìm nạp các địa điểm lân cận (cụ thể là công viên) bằng Places API
Bước còn lại để hoàn tất bài tập này là định vị những địa điểm mà bạn đang tìm nạp trong thực tế tăng cường.
Tìm hiểu về cảnh
ARCore có thể hiểu cảnh trong thế giới thực thông qua camera của thiết bị bằng cách phát hiện các điểm thú vị và riêng biệt (gọi là điểm đặc trưng) trong mỗi khung hình. Khi các điểm đặc trưng này được nhóm lại và xuất hiện trên một mặt phẳng ngang chung (chẳng hạn như bàn và sàn nhà), ARCore có thể cung cấp tính năng này cho ứng dụng dưới dạng một mặt phẳng ngang.
Như bạn đã thấy trước đó, ARCore giúp hướng dẫn người dùng khi phát hiện thấy một mặt phẳng bằng cách hiển thị các dấu chấm màu trắng.
Thêm nút neo
Sau khi phát hiện thấy một mặt phẳng, bạn có thể đính kèm một đối tượng gọi là neo. Thông qua một điểm neo, bạn có thể đặt các đối tượng ảo và đảm bảo rằng các đối tượng đó sẽ xuất hiện ở cùng một vị trí trong không gian. Hãy sửa đổi mã để đính kèm một mã sau khi phát hiện thấy máy bay.
Trong setUpAr
, OnTapArPlaneListener
được đính kèm vào PlacesArFragment
. Trình nghe này được gọi mỗi khi một mặt phẳng được nhấn vào trong cảnh thực tế tăng cường. Trong lệnh gọi này, bạn có thể tạo một Anchor
và một AnchorNode
từ HitResult
được cung cấp trong trình nghe như sau:
arFragment.setOnTapArPlaneListener { hitResult, _, _ ->
val anchor = hitResult.createAnchor()
anchorNode = AnchorNode(anchor)
anchorNode?.setParent(arFragment.arSceneView.scene)
addPlaces(anchorNode!!)
}
AnchorNode
là nơi bạn sẽ đính kèm các đối tượng nút con (các thực thể PlaceNode
) trong cảnh được xử lý trong lệnh gọi phương thức addPlaces
.
Chạy ứng dụng
Nếu bạn chạy ứng dụng với các nội dung sửa đổi ở trên, hãy quan sát xung quanh cho đến khi phát hiện thấy một mặt phẳng. Hãy nhấn vào các chấm trắng cho biết một mặt phẳng. Sau khi thực hiện thao tác này, bạn sẽ thấy các điểm đánh dấu trên bản đồ cho tất cả các công viên gần bạn nhất. Tuy nhiên, nếu bạn nhận thấy, các đối tượng ảo bị mắc kẹt trên điểm neo đã được tạo và không được đặt tương ứng với vị trí của các công viên đó trong không gian.
Trong bước cuối cùng, bạn sẽ khắc phục vấn đề này bằng cách sử dụng Thư viện tiện ích Maps SDK cho Android và SensorManager trên thiết bị.
8. Định vị địa điểm
Để có thể đặt biểu tượng địa điểm ảo ở chế độ thực tế tăng cường theo hướng chính xác, bạn sẽ cần 2 thông tin sau:
- Hướng chính bắc
- Góc giữa hướng bắc và mỗi địa điểm
Xác định hướng bắc
Bạn có thể xác định hướng bắc bằng cách sử dụng các cảm biến vị trí (từ trường và gia tốc kế) có trên thiết bị. Khi dùng 2 cảm biến này, bạn có thể thu thập thông tin theo thời gian thực về vị trí của thiết bị trong không gian. Để biết thêm thông tin về cảm biến vị trí, hãy đọc bài viết Tính toán hướng của thiết bị.
Để truy cập vào các cảm biến này, bạn cần lấy SensorManager
rồi đăng ký SensorEventListener
trên các cảm biến đó. Các bước này đã được thực hiện cho bạn trong các phương thức vòng đời của 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)
}
Trong phương thức onSensorChanged
, một đối tượng SensorEvent
được cung cấp, trong đó có thông tin chi tiết về dữ liệu của một cảm biến nhất định khi dữ liệu đó thay đổi theo thời gian. Hãy thêm đoạn mã sau vào phương thức đó:
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)
}
Đoạn mã trên sẽ kiểm tra loại cảm biến và tuỳ thuộc vào loại cảm biến, đoạn mã sẽ cập nhật chỉ số cảm biến thích hợp (chỉ số gia tốc kế hoặc từ kế). Dựa vào các chỉ số của cảm biến này, giờ đây, bạn có thể xác định giá trị của số độ từ hướng bắc so với thiết bị (tức là giá trị của orientationAngles[0]
).
Tiêu đề hình cầu
Sau khi xác định hướng bắc, bước tiếp theo là xác định góc giữa hướng bắc và từng địa điểm, sau đó sử dụng thông tin đó để đặt các địa điểm ở tiêu đề chính xác trong thực tế tăng cường.
Để tính hướng đi, bạn sẽ sử dụng Thư viện tiện ích Maps SDK cho Android. Thư viện này chứa một số hàm trợ giúp để tính khoảng cách và hướng đi thông qua hình học cầu. Để biết thêm thông tin, hãy đọc bài tổng quan này về thư viện.
Tiếp theo, bạn sẽ sử dụng phương thức sphericalHeading
trong thư viện tiện ích. Phương thức này tính toán hướng/hướng đi giữa 2 đối tượng LatLng
. Bạn cần thông tin này trong phương thức getPositionVector
được xác định trong Place.kt
. Phương thức này cuối cùng sẽ trả về một đối tượng Vector3
, sau đó đối tượng này sẽ được mỗi PlaceNode
dùng làm vị trí cục bộ trong không gian thực tế tăng cường.
Tiếp tục thay thế định nghĩa tiêu đề trong phương thức đó bằng nội dung sau:
val heading = latLng.sphericalHeading(placeLatLng)
Khi đó, bạn sẽ có định nghĩa phương thức sau:
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)
}
Vị trí cục bộ
Bước cuối cùng để định hướng chính xác các địa điểm trong thực tế tăng cường là sử dụng kết quả của getPositionVector
khi các đối tượng PlaceNode
được thêm vào cảnh. Tiếp tục chuyển đến addPlaces
trong MainActivity
, ngay bên dưới dòng đặt phần tử mẹ trên mỗi placeNode
(ngay bên dưới placeNode.setParent(anchorNode)
). Đặt localPosition
của placeNode
thành kết quả của việc gọi getPositionVector
như sau:
val placeNode = PlaceNode(this, place)
placeNode.setParent(anchorNode)
placeNode.localPosition = place.getPositionVector(orientationAngles[0], currentLocation.latLng)
Theo mặc định, phương thức getPositionVector
đặt khoảng cách y của nút thành 1 mét theo quy định của giá trị y
trong phương thức getPositionVector
. Nếu bạn muốn điều chỉnh khoảng cách này (ví dụ: thành 2 mét), hãy sửa đổi giá trị đó nếu cần.
Với thay đổi này, các đối tượng PlaceNode
được thêm hiện sẽ được định hướng theo tiêu đề chính xác. Bây giờ, hãy chạy ứng dụng để xem kết quả!
9. Xin chúc mừng
Chúc mừng bạn đã đi được đến đây!