Hiển thị các địa điểm lân cận trong môi trường AR trên Android (Kotlin)

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ừ Google Maps Platform để hiển thị các địa điểm lân cận trong thực tế tăng cường (AR) trên Android.

2344909dd9a52c60.png

Điều kiện tiên quyết

  • Hiểu biết cơ bản về việc 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 máy ảnh và vị trí của thiết bị.
  • Tích hợp với API Địa điểm để 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 mặt phẳng ngang để các đối tượng ảo có thể được liên kết 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 Maps SDK cho Android Library Library để định vị các đối tượng ảo ở đúng tiêu đề.

Bạn cần có

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 phải 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 thành các bước sau:

  1. Truy cập Trình quản lý SDK mà bạn có thể truy cập bằng cách nhấp vào Công cụ > Trình quản lý SDK.

6c44a9cb9cf6c236.png

  1. Kiểm tra xem bạn đã cài đặt Android 10.0 hay chưa. Nếu không, hãy cài đặt ứng dụng 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, sau đó nhấp lại vào OK trong hộp thoại xuất hiện.

368f17a974c75c73.png

  1. Cuối cùng, hãy cài đặt Dịch vụ Google Play bằng cách chuyển đến thẻ 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 lần nữa trong hộp thoại xuất hiện**.**

497a954b82242f4b.png

API bắt buộc

Trong Bước 3 của phần sau, hãy bật SDK Maps dành cho AndroidAPI địa điểm 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 với Nền tảng Google Maps để hoàn thành các bước sau:

  1. Tạo một tài khoản thanh toán.
  2. Tạo một dự án.
  3. Bật API và SDK của nền tảng Google Maps (được liệt kê trong phần trước).
  4. Tạo khoá API.

Không bắt buộc: Trình mô phỏng Android

Nếu bạn 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 một cảnh AR cũng như giả mạo vị trí thiết bị của bạn. Vì bạn cũng sẽ sử dụng Sceneform trong bài tập này, bạn cũng cần đảm bảo làm theo các bước trong "Định cấu hình trình mô phỏng để hỗ trợ Cấu hình."

3. Bắt đầu nhanh

Để giúp bạn bắt đầu nhanh nhất có thể, hãy xem một số mã dành cho người mới bắt đầu để giúp bạn thực hiện trong lớp học lập trình này. Bạn có thể chuyển sang giải pháp này, nhưng nếu muốn xem tất cả các bước, hãy tiếp tục đọc phần tiếp theo.

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 tiếp tục và mở dự án mà bạn thấy 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 đó. Bên trong kho lưu trữ này, bạn sẽ tìm thấy một mô-đun có tên 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, trong đó chỉ định những quyền mà người dùng cần được cấp trước khi người dùng có thể sử dụng những quyền đó, hãy khai báo những điều sau:

  • android.permission.INTERNET—điều này nhằm giúp ứng dụng của bạn có thể thực hiện các hoạt động mạng và tìm nạp dữ liệu qua Internet, chẳng hạn như thông tin địa điểm qua API Địa điểm.
  • android.permission.CAMERA—bạn cần có quyền sử dụng máy ảnh để có thể sử dụng máy ảnh của thiết bị nhằm hiển thị các vật thể trong môi trường thực tế tăng cường.
  • android.permission.ACCESS_FINE_LOCATION—cần có quyền truy cập thông tin vị trí để có thể tìm nạp những địa điểm lân cận tương đối giữa vị trí của thiết bị.

Đối với uses-feature, ứng dụng này xác định những tính năng phần cứng cần thiết trong ứng dụng, sau đây là khai báo:

  • Bắt buộc phải có OpenGL ES phiên bản 3.0.
  • Cần có thiết bị hỗ trợ ARCore.

Ngoài ra, các thẻ siêu dữ liệu sau đây được thêm vào dưới đố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 nhập siêu dữ liệu đầu tiên là để cho biết ARCore là yêu cầu để ứng dụng này chạy và mục thứ hai là cách bạn cung cấp khóa API Nền tảng Google Maps cho SDK Maps dành cho Android.

build.gradle

Trong build.gradle, các phần phụ thuộc bổ sung sau đượ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"
}

Dưới đâ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-locationplay-services-maps, được dùng để truy cập vào thông tin vị trí của thiết bị và truy cập vào chức năng liên quan đến Google Maps.
  • com.google.maps.android:maps-utils-ktxthư viện Kotlin tiện ích (KTX) cho SDK Maps dành cho Thư viện tiện ích Android. Chức năng này sẽ được dùng trong thư viện này để đặt các đối tượng ảo trong không gian thực về sau.
  • com.google.ar.sceneform.ux:sceneform-ux là thư viện Sceneform cho phép bạn hiển thị các cảnh 3D thực tế mà không phải học OpenGL.
  • Phần phụ thuộc trong mã nhóm com.squareup.retrofit2 là các phần phụ thuộc Retrofit, cho phép bạn nhanh chóng viết một ứng dụng HTTP để tương tác với API Địa điểm.

Cấu trúc dự án

Tại đây, bạn sẽ tìm thấy các gói và tệp sau:

  • **api – **gói này chứa các lớp được dùng để tương tác với API địa điểm 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 Place. Lớp này dùng để đóng gói một vị trí duy nhất do API Địa điểm trả về.
  • MainActivity.kt – Đây là Activity duy nhất có trong ứng dụng của bạn, chế độ này sẽ hiển thị bản đồ và một chế độ xem máy ảnh.

5. Thiết lập cảnh

Tìm hiểu các thành phần cốt lõi của ứng dụng, bắt đầu từ những thành phần thực tế tăng cường.

MainActivity chứa SupportMapFragment sẽ xử lý việc hiển thị đối tượng bản đồ và một lớp con của ArFragmentPlacesArFragment—xử lý hiển thị cảnh thực tế tăng cường.

Thiết lập tính năng 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ý việc yêu cầu người dùng cấp quyền máy ảnh nếu chưa được cấp quyền. Bạn cũng có thể yêu cầu các quyền bổ sung 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 thông tin vị trí, hãy chỉ định điều đó và ghi đè phương thức getAdditionalPermissions:

class PlacesArFragment : ArFragment() {

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

Chạy chiến dịch

Hãy tiếp tục và mở mã bộ xương trong thư mục starter trong Android Studio. Nếu bạn nhấp vào Chạy > Chạy "ứng dụng\39; trên thanh công cụ và triển khai ứng dụng cho thiết bị hoặc trình mô phỏng của bạn, trước tiên bạn sẽ được nhắc bật quyền truy cập vị trí và máy ảnh. Hãy tiếp tục và nhấp vào Cho phép. Khi làm như vậy, bạn sẽ thấy chế độ xem máy ảnh và chế độ xem bản đồ cạnh nhau như sau:

e3e3073d5c86f427.png

Phát hiện máy bay

Khi quan sát môi trường mà bạn đang nhìn thấy, bạn có thể thấy một vài chấm trắng phủ trên các bề mặt ngang, giống như những chấm màu trắng trên thảm trong hình ảnh này.

2a9b6ea7dcb2e249.png

Những chấm màu trắng này là nguyên tắc do ARCore cung cấp để cho biết rằng đã phát hiện một máy bay ngang. Các máy bay đã phát hiện này cho phép bạn tạo những gì được gọi là "anchor" để bạn có thể xác định vị trí của các vật thể ảo trong không gian thực.

Để biết thêm thông tin về ARCore và cách thuộc tính này hiểu 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. Xem các đị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 API Địa điểm.

Thiết lập Maps

Khóa API API Google Maps

Trước đó, bạn đã tạo khóa API Google Maps Platform để có thể truy vấn API địa điểm và có thể sử dụng SDK Maps dành cho Android. Hãy tiếp tục và mở tệp gradle.properties rồi thay thế chuỗi "YOUR API KEY HERE" bằng khóa API mà bạn đã tạo.

Hiển thị vị trí của thiết bị trên bản đồ

Khi bạn đã thêm khóa API của mình, hãy thêm một trình trợ giúp trên bản đồ để hỗ trợ định hướng cho người dùng ở vị trí tương đối của họ trên 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ẽ hiển thị 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

Để biết vị trí của thiết bị, bạn cần khai thác lớp FusedLocationProviderClient. Việc lấy một bản sao của điều 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 phương thức getCurrentLocation. Phương thức này chấp nhận đối số hàm lambda để có thể chuyển vị trí đến 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 theo sau bằng cách thêm một 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 đã 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 tại địa điểm

Trong lệnh gọi phương thức getNearbyPlaces, hãy lưu ý rằng các thông số sau được chuyển vào phương thức placesServices.nearbyPlaces – khóa 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 tiếp tục và chuyển khóa 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 của bạn 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ẽ cung cấp giá trị tài nguyên chuỗi google_maps_key vào thời gian 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 qua getString trên đối tượng Context.

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

7. Địa điểm trong môi trường AR

Cho đến thời điểm này, bạn đã thực hiện những việc sau:

  1. Máy ảnh đã yêu cầu quyền truy cập vị trí và máy ảnh khi người dùng chạy ứng dụng lần đầu tiên
  2. Thiết lập ARCore để bắt đầu theo dõi máy bay ngang
  3. Thiết lập SDK Maps bằng khoá API
  4. Đã xác định được vị trí hiện tại của thiết bị
  5. Tìm nạp các địa điểm lân cận (cụ thể là công viên) bằng API địa điểm

Bước còn lại để hoàn thành bài tập này là xác định vị trí mà bạn đang tìm nạp trong thực tế tăng cường.

Hiểu rõ

ARCore có thể hiểu được thế giới thực thông qua máy ảnh của thiết bị bằng cách phát hiện các điểm thú vị và riêng biệt được gọi là các điểm nổi bật trong mỗi khung hình ảnh. Khi các điểm nổi bật này được nhóm lại và có vẻ nằm trên một mặt phẳng ngang chung, như bảng và sàn, 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, ARCore giúp hướng dẫn người dùng khi một máy bay được phát hiện bằng cách hiển thị các chấm màu trắng.

2a9b6ea7dcb2e249.png

Thêm quảng cáo cố định

Sau khi phát hiện một máy bay, bạn có thể đính kèm một vật thể gọi là đường liên kết. Thông qua quảng cáo cố định, 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 tiếp tục và sửa đổi mã để đính kèm một mã sau khi phát hiện thấy một máy bay.

Trong setUpAr, một OnTapArPlaneListener được đính kèm vào PlacesArFragment. Trình nghe này được gọi bất cứ khi nào có một máy bay được nhấn trong cảnh AR. Trong cuộc gọi này, bạn có thể tạo AnchorAnchorNode 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 của nút con – các thực thể của PlaceNode – trong cảnh được xử lý trong lệnh gọi phương thức addPlaces.

Chạy

Nếu bạn chạy ứng dụng có các sửa đổi ở trên, hãy xem xung quanh bạn cho đến khi phát hiện thấy một chiếc máy bay. Hãy tiếp tục và nhấn vào các chấm màu trắng biểu thị máy bay. Khi làm như vậy, giờ đâ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ị kẹt trên neo liên kết đã được tạo và không được đặt so với vị trí của các công viên đó trong không gian.

f93eb87c98a0098d.png

Đối với bước cuối cùng, bạn sẽ khắc phục bằng cách sử dụng SDK Maps cho Thư viện tiện ích AndroidSensorManager trên thiết bị.

8. Địa điểm định vị

Để có thể định vị biểu tượng địa điểm ảo trong thực tế tăng cường cho một tiêu đề chính xác, bạn cần hai thông tin:

  • Trong khi thực sự đang ở phía bắc
  • Góc giữa địa điểm này 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ừ địa lý và gia tốc kế) có trên thiết bị. Bằng cách sử dụng hai 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 phần Hướng điện của thiết bị.

Để sử dụng 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\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)
}

Trong phương thức onSensorChanged, đối tượng SensorEvent được cung cấp. Thông tin này chứa thông tin chi tiết về dữ liệu của cảm biến cụ thể khi đối tượng này thay đổi theo thời gian. Hãy tiếp tục và thêm 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à tùy thuộc vào loại cảm biến, mã này sẽ cập nhật chỉ số cảm biến thích hợp (có thể đọc gia tốc kế hoặc từ kế). Bằng cách sử dụng các chỉ số cảm biến này, bạn có thể xác định giá trị của số độ ở 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

Bây giờ, vùng phía bắc đã được xác định, bước tiếp theo là xác định góc giữa hai vị trí phía bắc và mỗi địa điểm, sau đó dùng thông tin đó để xác định vị trí của các địa điểm trong tiêu đề chính xác trong thực tế tăng cường.

Để tính toán tiêu đề, bạn sẽ sử dụng SDK Maps dành cho Thư viện tiện ích Android, trong đó có một số chức năng trợ giúp để tính khoảng cách và tiêu đề thông qua hình học hình cầu. Để biết thêm thông tin, hãy đọc tổng quan về thư viện này.

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 sẽ tính toán tiêu đề/hình ảnh giữa hai đối tượng LatLng. Bạn cần cung cấp thông tin này trong phương thức getPositionVector đã xác định trong Place.kt. Cuối cùng, phương thức này sẽ trả về một đối tượng Vector3. Sau đó, mỗi PlaceNode sẽ được sử dụng làm vị trí cục bộ cho không gian thực tế tăng cường.

Hãy tiếp tục và thay thế định nghĩa tiêu đề bằng phương thức đó bằng cách sau:

val heading = latLng.sphericalHeading(placeLatLng)

Việc này sẽ dẫn đến đị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ộ trên thiết 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 PlaceNode đối tượng được thêm vào cảnh. Hãy tiếp tục và chuyển đến addPlaces trong MainActivity, ngay bên dưới dòng mà cha mẹ đặt trên mỗi placeNode (ngay bên dưới placeNode.setParent(anchorNode)). Hãy đặt localPosition của placeNode thành kết quả 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 sẽ đặt khoảng cách y của nút thành 1 mét như được chỉ định bởi 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, hãy nói đến 2 mét, hãy tiếp tục và sửa đổi giá trị đó nếu cần.

Với thay đổi này, đối tượng PlaceNode đã được thêm sẽ được định hướng trong tiêu đề chính xác. Bây giờ, hãy tiếp tục và chạy ứng dụng để xem kết quả!

9. Xin chúc mừng

Chúc mừng bạn đã đạt được rất nhiều bước!

Tìm hiểu thêm