Thêm bản đồ vào ứng dụng Android (Kotlin) của bạn

1. Trước khi bắt đầu

Lớp học lập trình này hướng dẫn bạn cách tích hợp SDK Maps dành cho Android vào ứng dụng của bạn và sử dụng các tính năng cốt lõi của ứng dụng này bằng cách tạo một ứng dụng hiển thị bản đồ các cửa hàng xe đạp ở San Francisco, CA, Hoa Kỳ.

f05e1ca27ff42bf6.png

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

  • Có kiến thức cơ bản về Kotlin và phát triển Android

Bạn sẽ thực hiện

  • Bật và sử dụng SDK Maps dành cho Android để thêm Google Maps vào ứng dụng Android.
  • Thêm, tùy chỉnh và nhóm các điểm đánh dấu.
  • Vẽ nhiều đường kẻ và đa giác trên bản đồ.
  • Điều khiển góc nhìn của camera bằng cách lập trình.

Bạn cần có

2. Bắt đầu thiết lập

Đối với bước bật sau đây , bạn cần bật SDK Maps cho Android.

Thiết lập Nền tảng Google Maps

Nếu bạn chưa có tài khoản Google Cloud Platform và một dự án đã bật tính năng thanh toán, vui lòng xem hướng dẫn Bắt đầu sử dụng Google Maps Platform để tạo tài khoản thanh toán và một dự án.

  1. Trong Cloud Console, hãy nhấp vào trình đơn thả xuống dự án và chọn dự án mà bạn muốn sử dụng cho lớp học lập trình này.

  1. Bật API và SDK của Nền tảng Google Maps bắt buộc cho lớp học lập trình này trong Google Cloud Marketplace. Để làm như vậy, hãy làm theo các bước trong video này hoặc tài liệu này.
  2. Tạo khoá API trong trang Thông tin xác thực của Cloud Console. Bạn có thể làm theo các bước trong video này hoặc tài liệu này. Tất cả các yêu cầu gửi đến Google Maps Platform đều yêu cầu khóa API.

3. Bắt đầu nhanh

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

  1. Sao chép kho lưu trữ nếu bạn đã cài đặt git.
git clone https://github.com/googlecodelabs/maps-platform-101-android.git

Ngoài ra, bạn có thể nhấp vào nút sau để tải mã nguồn xuống.

  1. Sau khi nhận được mã, hãy tiếp tục và mở dự án trong thư mục starter trong Android Studio.

4. Thêm Google Maps

Trong phần này, bạn sẽ thêm Google Maps để nó tải khi bạn khởi chạy ứng dụng.

d1d068b5d4ae38b9.png

Thêm khóa API của bạn

Bạn cần cung cấp khóa API mà bạn đã tạo ở bước trước cho ứng dụng để SDK Maps dành cho Android có thể liên kết khóa của bạn với ứng dụng.

  1. Để cung cấp mục này, hãy mở tệp có tên local.properties trong thư mục gốc của dự án (cùng cấp với gradle.propertiessettings.gradle).
  2. Trong tệp đó, hãy xác định khoá mới GOOGLE_MAPS_API_KEY có giá trị là khoá API mà bạn đã tạo.

local.properties

GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE

Xin lưu ý rằng local.properties được liệt kê trong tệp .gitignore trong kho lưu trữ Git. Điều này là do khóa API của bạn được coi là thông tin nhạy cảm và không nên kiểm tra nguồn kiểm soát nguồn, nếu có thể.

  1. Tiếp theo, để hiển thị API của bạn để có thể sử dụng trên toàn ứng dụng, hãy thêm trình bổ trợ Bí mật Gradle cho Android vào tệp build.gradle của ứng dụng nằm trong thư mục app/ và thêm dòng sau vào khối plugins:

build.gradle cấp ứng dụng

plugins {
    // ...
    id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}

Bạn cũng sẽ cần sửa đổi tệp build.gradle cấp dự án để thêm đường dẫn lớp sau:

build.gradle cấp dự án

buildscript {
    dependencies {
        // ...
        classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:1.3.0"
    }
}

Trình bổ trợ này sẽ cung cấp các khóa bạn đã xác định trong tệp local.properties dưới dạng biến bản dựng trong tệp kê khai Android và dưới dạng biến trong lớp BuildConfig do Gradle tạo vào thời điểm tạo. Việc sử dụng trình bổ trợ này sẽ xóa mã nguyên mẫu mà sẽ cần thiết để đọc các thuộc tính khỏi local.properties để có thể truy cập mã trong suốt ứng dụng của bạn.

Thêm phần phụ thuộc vào Google Maps

  1. Bây giờ, bạn có thể truy cập vào khóa API bên trong ứng dụng, bước tiếp theo là thêm SDK Maps dành cho Android vào tệp build.gradle của ứng dụng.

Trong dự án dành cho người mới bắt đầu đi kèm với lớp học lập trình này, phần phụ thuộc này đã được thêm vào cho bạn.

build.gradle

dependencies {
   // Dependency to include Maps SDK for Android
   implementation 'com.google.android.gms:play-services-maps:17.0.0'
}
  1. Tiếp theo, hãy thêm thẻ meta-data mới trong AndroidManifest.xml để chuyển vào khóa API mà bạn đã tạo ở bước trước đó. Để thực hiện việc này, hãy tiếp tục và mở tệp này trong Android Studio rồi thêm thẻ meta-data sau vào trong đối tượng application trong tệp AndroidManifest.xml của bạn, nằm trong app/src/main.

AndroidManifest.xml

<meta-data
   android:name="com.google.android.geo.API_KEY"
   android:value="${GOOGLE_MAPS_API_KEY}" />
  1. Tiếp theo, hãy tạo tệp bố cục mới có tên là activity_main.xml trong thư mục app/src/main/res/layout/ và xác định tệp như sau:

activity_main.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <fragment
       class="com.google.android.gms.maps.SupportMapFragment"
       android:id="@+id/map_fragment"
       android:layout_width="match_parent"
       android:layout_height="match_parent" />

</FrameLayout>

Bố cục này có một FrameLayout duy nhất chứa SupportMapFragment. Mảnh này chứa đối tượng GoogleMaps cơ bản mà bạn sử dụng trong các bước sau này.

  1. Cuối cùng, hãy cập nhật lớp MainActivity nằm trong app/src/main/java/com/google/codelabs/buildyourfirstmap bằng cách thêm mã sau để ghi đè phương thức onCreate, nhờ đó, bạn có thể đặt nội dung của phương thức bằng bố cục mới mà bạn vừa tạo.

Hoạt động chính

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
}
  1. Bây giờ, hãy tiếp tục và chạy ứng dụng. Bây giờ, bạn sẽ thấy tải bản đồ trên màn hình của thiết bị.

5. Tạo bản đồ dựa trên đám mây (Không bắt buộc)

Bạn có thể tùy chỉnh kiểu của bản đồ bằng cách tạo kiểu bản đồ dựa trên đám mây.

Tạo mã bản đồ

Nếu bạn chưa tạo mã bản đồ với kiểu bản đồ được liên kết, hãy xem hướng dẫn Mã bản đồ để hoàn tất các bước sau:

  1. Tạo mã bản đồ.
  2. Liên kết mã bản đồ với kiểu bản đồ.

Thêm ID bản đồ vào ứng dụng của bạn

Để sử dụng mã bản đồ mà bạn đã tạo, hãy sửa đổi tệp activity_main.xml và chuyển mã bản đồ vào thuộc tính map:mapId của SupportMapFragment.

activity_main.xml

<fragment xmlns:map="http://schemas.android.com/apk/res-auto"
    class="com.google.android.gms.maps.SupportMapFragment"
    <!-- ... -->
    map:mapId="YOUR_MAP_ID" />

Sau khi bạn hoàn tất bước này, hãy tiếp tục và chạy ứng dụng để xem bản đồ của bạn theo phong cách mà bạn đã chọn!

6. Thêm điểm đánh dấu

Trong nhiệm vụ này, bạn thêm điểm đánh dấu vào bản đồ thể hiện những địa điểm yêu thích mà bạn muốn đánh dấu trên bản đồ. Trước tiên, bạn truy xuất danh sách các địa điểm đã được cung cấp trong dự án dành cho người mới bắt đầu, sau đó thêm những địa điểm đó vào bản đồ. Trong ví dụ này, đây là những cửa hàng xe đạp.

bc5576877369b554.png

Tham khảo Google Maps

Trước tiên, bạn cần lấy thông tin tham chiếu đến đối tượng GoogleMap để có thể sử dụng các phương thức của đối tượng đó. Để làm việc đó, hãy thêm mã sau vào phương thức MainActivity.onCreate() của bạn ngay sau lệnh gọi tới setContentView():

MainActivity.onCreate()

val mapFragment = supportFragmentManager.findFragmentById(   
    R.id.map_fragment
) as? SupportMapFragment
mapFragment?.getMapAsync { googleMap ->
    addMarkers(googleMap)
}

Trước tiên, cách triển khai sẽ tìm thấy SupportMapFragment mà bạn đã thêm ở bước trước bằng cách sử dụng phương thức findFragmentById() trên đối tượng SupportFragmentManager. Sau khi nhận được tệp đối chiếu, lệnh gọi getMapAsync() sẽ được gọi, sau đó được chuyển vào hàm lambda. Hàm lambda này là nơi chuyển đối tượng GoogleMap. Bên trong hàm lambda này, lệnh gọi phương thức addMarkers() sẽ được gọi. Lệnh này sẽ sớm được xác định.

Lớp được cung cấp: Places Reader

Trong dự án dành cho người mới bắt đầu, bạn đã cung cấp lớp PlacesReader. Lớp này đọc danh sách 49 địa điểm được lưu trữ trong tệp JSON có tên là places.json và trả về các địa điểm này dưới dạng List<Place>. Các địa điểm này đại diện cho danh sách các cửa hàng xe đạp quanh San Francisco, CA, Hoa Kỳ.

Nếu muốn biết cách triển khai lớp học này, bạn có thể truy cập vào lớp học trên GitHub hoặc mở lớp PlacesReader trong Android Studio.

Places Reader

package com.google.codelabs.buildyourfirstmap.place

import android.content.Context
import com.google.codelabs.buildyourfirstmap.R
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.io.InputStream
import java.io.InputStreamReader

/**
* Reads a list of place JSON objects from the file places.json
*/
class PlacesReader(private val context: Context) {

   // GSON object responsible for converting from JSON to a Place object
   private val gson = Gson()

   // InputStream representing places.json
   private val inputStream: InputStream
       get() = context.resources.openRawResource(R.raw.places)

   /**
    * Reads the list of place JSON objects in the file places.json
    * and returns a list of Place objects
    */
   fun read(): List<Place> {
       val itemType = object : TypeToken<List<PlaceResponse>>() {}.type
       val reader = InputStreamReader(inputStream)
       return gson.fromJson<List<PlaceResponse>>(reader, itemType).map {
           it.toPlace()
       }
   }

Tải địa điểm

Để tải danh sách cửa hàng xe đạp, hãy thêm một tài sản trong MainActivity có tên là places và xác định tài sản như sau:

MainActivity.Places

private val places: List<Place> by lazy {
   PlacesReader(this).read()
}

Mã này gọi phương thức read() trên PlacesReader, trả về List<Place>. Place có một thuộc tính có tên là name, tên của địa điểm và latLng – tọa độ của địa điểm.

Địa điểm

data class Place(
   val name: String,
   val latLng: LatLng,
   val address: LatLng,
   val rating: Float
)

Thêm điểm đánh dấu vào bản đồ

Bây giờ, danh sách các địa điểm đã được tải vào bộ nhớ, bước tiếp theo là trình bày các địa điểm này trên bản đồ.

  1. Tạo một phương thức trong MainActivity có tên là addMarkers() và xác định phương thức này như sau:

MainActivity.addMarkers()

/**
* Adds marker representations of the places list on the provided GoogleMap object
*/
private fun addMarkers(googleMap: GoogleMap) {
   places.forEach { place ->
       val marker = googleMap.addMarker(
           MarkerOptions()
               .title(place.name)
               .position(place.latLng)
       )
   }
}

Phương thức này lặp lại trong danh sách places, sau đó gọi phương thức addMarker() trên đối tượng GoogleMap đã cung cấp. Điểm đánh dấu này được tạo bằng cách tạo đối tượng MarkerOptions để bạn có thể tùy chỉnh chính điểm đánh dấu. Trong trường hợp này, tiêu đề và vị trí của điểm đánh dấu được cung cấp, đại diện cho tên cửa hàng xe đạp và tọa độ tương ứng.

  1. Hãy tiếp tục và chạy ứng dụng, cũng như sẽ đến San Francisco để xem những điểm đánh dấu bạn vừa thêm!

7. Tùy chỉnh điểm đánh dấu

Có một số tùy chọn tùy chỉnh cho các điểm đánh dấu bạn vừa thêm để giúp chúng nổi bật và truyền tải thông tin hữu ích cho người dùng. Trong nhiệm vụ này, bạn sẽ khám phá một số điểm bằng cách tùy chỉnh hình ảnh của mỗi điểm đánh dấu cũng như cửa sổ thông tin hiển thị khi điểm đánh dấu được nhấn vào.

a26f82802fe838e9.png

Thêm cửa sổ thông tin

Theo mặc định, cửa sổ thông tin khi bạn nhấn vào một điểm đánh dấu sẽ hiển thị tiêu đề và đoạn trích của điểm đánh dấu (nếu được đặt). Bạn tùy chỉnh cài đặt này để có thể hiển thị thêm thông tin như địa chỉ và điểm xếp hạng của địa điểm.

Tạo timestamp_info_contents.xml

Trước tiên, hãy tạo một tệp bố cục mới có tên là marker_info_contents.xml.

  1. Để thực hiện việc này, hãy nhấp chuột phải vào thư mục app/src/main/res/layout trong chế độ xem dự án trong Android Studio rồi chọn Mới > Tệp tài nguyên bố cục.

8cac51fcbef9171b.png

  1. Trong hộp thoại, hãy nhập marker_info_contents trong trường Tên tệpLinearLayout vào trường Root element, sau đó nhấp vào OK.

8783af12baf07a80.png

Tệp bố cục này sau đó được tăng cường để thể hiện nội dung trong cửa sổ thông tin.

  1. Sao chép nội dung trong đoạn mã sau, thao tác này thêm ba TextViews trong một nhóm chế độ xem LinearLayout dọc và ghi đè mã mặc định trong tệp.

mark_info_contents.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:orientation="vertical"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:gravity="center_horizontal"
   android:padding="8dp">

   <TextView
       android:id="@+id/text_view_title"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textColor="@android:color/black"
       android:textSize="18sp"
       android:textStyle="bold"
       tools:text="Title"/>

   <TextView
       android:id="@+id/text_view_address"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textColor="@android:color/black"
       android:textSize="16sp"
       tools:text="123 Main Street"/>

   <TextView
       android:id="@+id/text_view_rating"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textColor="@android:color/black"
       android:textSize="16sp"
       tools:text="Rating: 3"/>

</LinearLayout>

Tạo phương thức triển khai InfowindowAdapter

Sau khi tạo tệp bố cục cho cửa sổ thông tin tùy chỉnh, bước tiếp theo là triển khai giao diện GoogleMap.InfoInfoAdapter. Giao diện này chứa hai phương thức, getInfoWindow()getInfoContents(). Cả hai phương thức đều trả về một đối tượng View không bắt buộc, trong đó phương thức cũ được dùng để tùy chỉnh cửa sổ, trong khi phương thức thứ hai là tùy chỉnh nội dung của cửa sổ. Trong trường hợp của bạn, bạn triển khai cả hai và tùy chỉnh trả về getInfoContents() trong khi trả về null trong getInfoWindow(). Điều này cho biết nên sử dụng cửa sổ mặc định.

  1. Tạo một tệp mới trong Kotlin có tên là MarkerInfoWindowAdapter trong cùng một gói với MainActivity bằng cách nhấp chuột phải vào thư mục app/src/main/java/com/google/codelabs/buildyourfirstmap trong chế độ xem dự án trong Android Studio, sau đó chọn Mới > Tệp/Lớp Kotlin.

3975ba36eba9f8e1.png

  1. Trong hộp thoại, hãy nhập MarkerInfoWindowAdapter và giữ cho Tệp được làm nổi bật.

992235af53d3897f.png

  1. Sau khi bạn tạo tệp, hãy sao chép nội dung trong đoạn mã sau vào tệp mới của bạn.

MarkerInfowindowAdapter

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.Marker
import com.google.codelabs.buildyourfirstmap.place.Place

class MarkerInfoWindowAdapter(
    private val context: Context
) : GoogleMap.InfoWindowAdapter {
   override fun getInfoContents(marker: Marker?): View? {
       // 1. Get tag
       val place = marker?.tag as? Place ?: return null

       // 2. Inflate view and set title, address, and rating
       val view = LayoutInflater.from(context).inflate(
           R.layout.marker_info_contents, null
       )
       view.findViewById<TextView>(
           R.id.text_view_title
       ).text = place.name
       view.findViewById<TextView>(
           R.id.text_view_address
       ).text = place.address
       view.findViewById<TextView>(
           R.id.text_view_rating
       ).text = "Rating: %.2f".format(place.rating)

       return view
   }

   override fun getInfoWindow(marker: Marker?): View? {
       // Return null to indicate that the 
       // default window (white bubble) should be used
       return null
   }
}

Trong nội dung của phương thức getInfoContents(), Điểm đánh dấu đã cung cấp trong phương thức sẽ được truyền tới loại Place và nếu không thể truyền, phương thức này sẽ trả về null (bạn chưa đặt thuộc tính thẻ trên Marker, nhưng bạn sẽ làm điều đó trong bước tiếp theo).

Tiếp theo, bố cục marker_info_contents.xml được tăng cường, theo sau là đặt văn bản chứa TextViews thành thẻ Place.

Cập nhật MainActivity

Để dán tất cả thành phần bạn đã tạo từ trước đến nay, bạn cần thêm hai dòng trong lớp MainActivity.

Trước tiên, để chuyển InfoWindowAdapter tùy chỉnh, MarkerInfoWindowAdapter, bên trong lệnh gọi phương thức getMapAsync, hãy gọi phương thức setInfoWindowAdapter() trên đối tượng GoogleMap và tạo một thực thể mới của MarkerInfoWindowAdapter.

  1. Hãy làm việc này bằng cách thêm mã sau đây vào lệnh gọi phương thức addMarkers() bên trong hàm lambda getMapAsync().

MainActivity.onCreate()

// Set custom info window adapter
googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))

Cuối cùng, bạn sẽ cần đặt mỗi Địa điểm làm thuộc tính thẻ trên mọi Điểm đánh dấu được thêm vào bản đồ.

  1. Để thực hiện việc đó, hãy sửa đổi lệnh gọi places.forEach{} trong hàm addMarkers() bằng cách sau:

MainActivity.addMarkers()

places.forEach { place ->
   val marker = googleMap.addMarker(
       MarkerOptions()
           .title(place.name)
           .position(place.latLng)
           .icon(bicycleIcon)
   )

   // Set place as the tag on the marker object so it can be referenced within
   // MarkerInfoWindowAdapter
   marker.tag = place
}

Thêm một hình ảnh điểm đánh dấu tùy chỉnh

Tùy chỉnh hình ảnh điểm đánh dấu là một trong những cách thú vị để truyền đạt loại địa điểm mà điểm đánh dấu đại diện trên bản đồ của bạn. Đối với bước này, bạn hiển thị xe đạp thay vì điểm đánh dấu màu đỏ mặc định để đại diện cho mỗi cửa hàng trên bản đồ. Dự án dành cho người mới bắt đầu bao gồm biểu tượng xe đạp ic_directions_bike_black_24dp.xml trong app/src/res/drawable mà bạn sử dụng.

6eb7358bb61b0a88.png

Đặt bitmap tùy chỉnh trên điểm đánh dấu

Với biểu tượng xe đạp có thể vẽ vectơ theo ý bạn, bước tiếp theo là đặt biểu tượng có thể vẽ đó làm từng điểm đánh dấu#39; biểu tượng trên bản đồ. MarkerOptions có một phương thức icon. Phương thức này sử dụng BitmapDescriptor mà bạn dùng để thực hiện việc này.

Trước tiên, bạn cần chuyển đổi vectơ vẽ mà bạn vừa thêm vào BitmapDescriptor. Tệp có tên BitMapHelper có trong dự án dành cho người mới bắt đầu chứa một hàm trợ giúp có tên là vectorToBitmap(). Hàm này sẽ thực hiện việc này.

Trình trợ giúpBitmapHelper

package com.google.codelabs.buildyourfirstmap

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.util.Log
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.DrawableCompat
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.BitmapDescriptorFactory

object BitmapHelper {
   /**
    * Demonstrates converting a [Drawable] to a [BitmapDescriptor], 
    * for use as a marker icon. Taken from ApiDemos on GitHub:
    * https://github.com/googlemaps/android-samples/blob/main/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/MarkerDemoActivity.kt
    */
   fun vectorToBitmap(
      context: Context,
      @DrawableRes id: Int, 
      @ColorInt color: Int
   ): BitmapDescriptor {
       val vectorDrawable = ResourcesCompat.getDrawable(context.resources, id, null)
       if (vectorDrawable == null) {
           Log.e("BitmapHelper", "Resource not found")
           return BitmapDescriptorFactory.defaultMarker()
       }
       val bitmap = Bitmap.createBitmap(
           vectorDrawable.intrinsicWidth,
           vectorDrawable.intrinsicHeight,
           Bitmap.Config.ARGB_8888
       )
       val canvas = Canvas(bitmap)
       vectorDrawable.setBounds(0, 0, canvas.width, canvas.height)
       DrawableCompat.setTint(vectorDrawable, color)
       vectorDrawable.draw(canvas)
       return BitmapDescriptorFactory.fromBitmap(bitmap)
   }
}

Phương thức này nhận Context, một mã tài nguyên có thể vẽ cũng như một số nguyên màu và tạo một biểu thị BitmapDescriptor về số đó.

Sử dụng phương thức trợ giúp, khai báo một thuộc tính mới có tên là bicycleIcon và cung cấp định nghĩa sau cho thuộc tính: MainActivity.cyclingIcon

private val bicycleIcon: BitmapDescriptor by lazy {
   val color = ContextCompat.getColor(this, R.color.colorPrimary)
   BitmapHelper.vectorToBitmap(this, R.drawable.ic_directions_bike_black_24dp, color)
}

Thuộc tính này sử dụng màu xác định trước colorPrimary trong ứng dụng của bạn và dùng màu đó để phủ màu biểu tượng xe đạp và trả về dưới dạng BitmapDescriptor.

  1. Bằng cách sử dụng thuộc tính này, hãy tiếp tục và gọi phương thức icon của MarkerOptions trong phương thức addMarkers() để hoàn tất quá trình tùy chỉnh biểu tượng. Để làm điều này, thuộc tính điểm đánh dấu sẽ trông giống như sau:

MainActivity.addMarkers()

val marker = googleMap.addMarker(
    MarkerOptions()
        .title(place.name)
        .position(place.latLng)
        .icon(bicycleIcon)
)
  1. Chạy ứng dụng để xem các điểm đánh dấu được cập nhật!

8. Điểm đánh dấu cụm

Tùy thuộc vào khoảng cách mà bạn phóng to bản đồ, bạn có thể nhận thấy rằng các điểm đánh dấu mà bạn đã thêm trùng lặp. Các điểm đánh dấu trùng lặp rất khó tương tác và tạo ra nhiều tiếng ồn. Điều này ảnh hưởng đến khả năng hữu dụng của ứng dụng.

68591edc86d73724.png

Để cải thiện trải nghiệm người dùng cho việc này, bất cứ khi nào bạn có một tập dữ liệu lớn được phân nhóm chặt chẽ, phương pháp hay nhất là triển khai việc phân nhóm điểm đánh dấu. Với tính năng nhóm, khi bạn phóng to và thu nhỏ bản đồ, các điểm đánh dấu ở gần sẽ được nhóm lại với nhau như sau:

f05e1ca27ff42bf6.png

Để triển khai chế độ này, bạn cần trợ giúp về SDK Maps dành cho Thư viện tiện ích Android.

SDK Maps dành cho Thư viện tiện ích Android

SDK Maps dành cho Thư viện tiện ích Android đã được tạo ra như một cách mở rộng chức năng của SDK Maps dành cho Android. Nền tảng này cung cấp các tính năng nâng cao, chẳng hạn như tính năng phân nhóm điểm đánh dấu, bản đồ nhiệt, hỗ trợ KML và GeoJson, mã hóa và giải mã nhiều đường cùng một số chức năng trợ giúp liên quan đến hình học hình cầu.

Cập nhật tệp build.gradle của bạn

Vì thư viện tiện ích được đóng gói riêng biệt với SDK Maps dành cho Android, nên bạn cần thêm một phần phụ thuộc bổ sung vào tệp build.gradle.

  1. Hãy tiếp tục và cập nhật phần dependencies trong tệp app/build.gradle của bạn.

build.gradle

implementation 'com.google.maps.android:android-maps-utils:1.1.0'
  1. Khi thêm dòng này, bạn phải thực hiện đồng bộ hóa dự án để tìm nạp các phần phụ thuộc mới.

b7b030ec82c007fd.png

Triển khai tính năng phân nhóm

Để triển khai tính năng nhóm trên ứng dụng, hãy làm theo ba bước sau:

  1. Triển khai giao diện ClusterItem.
  2. Lớp phụ lớp DefaultClusterRenderer.
  3. Tạo ClusterManager và thêm các mục.

Triển khai giao diện ClusterItem

Tất cả các đối tượng đại diện cho điểm đánh dấu có thể phân nhóm trên bản đồ cần phải triển khai giao diện ClusterItem. Trong trường hợp của bạn, điều đó có nghĩa là mô hình Place cần tuân thủ ClusterItem. Hãy tiếp tục và mở tệp Place.kt và thực hiện các tùy chọn sửa đổi sau đối với tệp:

Địa điểm

data class Place(
   val name: String,
   val latLng: LatLng,
   val address: String,
   val rating: Float
) : ClusterItem {
   override fun getPosition(): LatLng =
       latLng

   override fun getTitle(): String =
       name

   override fun getSnippet(): String =
       address
}

ClusterItem xác định 3 phương thức sau:

  • getPosition(), đại diện cho địa điểm của LatLng.
  • getTitle(), đại diện cho tên địa điểm
  • getSnippet(), đại diện cho địa chỉ của địa điểm.

Lớp con của lớp DefaultClusterRenderinger

Lớp học triển khai việc phân nhóm, ClusterManager, sử dụng nội bộ một lớp ClusterRenderer để xử lý việc tạo các cụm khi bạn kéo và thu phóng xung quanh bản đồ. Theo mặc định, cấu hình này đi kèm với trình kết xuất mặc định DefaultClusterRenderer sẽ triển khai ClusterRenderer. Bạn chỉ cần cung cấp trường hợp này là đủ. Tuy nhiên, trong trường hợp của bạn, vì các điểm đánh dấu cần được tùy chỉnh, bạn cần phải mở rộng lớp này và thêm các tùy chỉnh trong đó.

Hãy tiếp tục và tạo tệp Kotlin PlaceRenderer.kt trong gói com.google.codelabs.buildyourfirstmap.place và xác định tệp như sau:

Trình kết xuất địa điểm

package com.google.codelabs.buildyourfirstmap.place

import android.content.Context
import androidx.core.content.ContextCompat
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.Marker
import com.google.android.gms.maps.model.MarkerOptions
import com.google.codelabs.buildyourfirstmap.BitmapHelper
import com.google.codelabs.buildyourfirstmap.R
import com.google.maps.android.clustering.ClusterManager
import com.google.maps.android.clustering.view.DefaultClusterRenderer

/**
* A custom cluster renderer for Place objects.
*/
class PlaceRenderer(
   private val context: Context,
   map: GoogleMap,
   clusterManager: ClusterManager<Place>
) : DefaultClusterRenderer<Place>(context, map, clusterManager) {

   /**
    * The icon to use for each cluster item
    */
   private val bicycleIcon: BitmapDescriptor by lazy {
       val color = ContextCompat.getColor(context,
           R.color.colorPrimary
       )
       BitmapHelper.vectorToBitmap(
           context,
           R.drawable.ic_directions_bike_black_24dp,
           color
       )
   }

   /**
    * Method called before the cluster item (the marker) is rendered.
    * This is where marker options should be set.
    */
   override fun onBeforeClusterItemRendered(
      item: Place,
      markerOptions: MarkerOptions
   ) {
       markerOptions.title(item.name)
           .position(item.latLng)
           .icon(bicycleIcon)
   }

   /**
    * Method called right after the cluster item (the marker) is rendered.
    * This is where properties for the Marker object should be set.
    */
   override fun onClusterItemRendered(clusterItem: Place, marker: Marker) {
       marker.tag = clusterItem
   }
}

Lớp này ghi đè 2 hàm sau:

  • onBeforeClusterItemRendered(), được gọi trước khi cụm được hiển thị trên bản đồ. Tại đây, bạn có thể cung cấp các tùy chỉnh thông qua MarkerOptions. Trong trường hợp này, cột này đặt tiêu đề, vị trí và biểu tượng của điểm đánh dấu.
  • onClusterItemRenderer(), được gọi ngay sau khi điểm đánh dấu hiển thị trên bản đồ. Đây là nơi bạn có thể truy cập vào đối tượng Marker đã tạo. Trong trường hợp này, đối tượng sẽ đặt thuộc tính thẻ của điểm đánh dấu\.

Tạo ClusterManager và thêm mục

Cuối cùng, để hoạt động phân nhóm hoạt động, bạn cần sửa đổi MainActivity để tạo ClusterManager và cung cấp các phần phụ thuộc cần thiết cho hoạt động này. ClusterManager xử lý nội bộ việc thêm các điểm đánh dấu (các đối tượng ClusterItem) nên thay vì thêm trực tiếp các điểm đánh dấu trên bản đồ, trách nhiệm này sẽ được ủy quyền cho ClusterManager. Ngoài ra, ClusterManager cũng gọi setInfoWindowAdapter() trong nội bộ, do đó, việc đặt cửa sổ thông tin tùy chỉnh sẽ phải được thực hiện trên đối tượng MarkerManager.Collection của ClusterManger.

  1. Để bắt đầu, hãy sửa đổi nội dung của hàm lambda trong lệnh gọi getMapAsync() trong MainActivity.onCreate(). Hãy tiếp tục và nhận xét cuộc gọi đến addMarkers()setInfoWindowAdapter(), sau đó gọi một phương thức có tên là addClusteredMarkers() mà bạn xác định tiếp theo.

MainActivity.onCreate()

mapFragment?.getMapAsync { googleMap ->
    //addMarkers(googleMap)
    addClusteredMarkers(googleMap)

    // Set custom info window adapter.
    // googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
  1. Tiếp theo, trong MainActivity, hãy xác định addClusteredMarkers().

MainActivity.addClusteredMarkers()

/**
* Adds markers to the map with clustering support.
*/
private fun addClusteredMarkers(googleMap: GoogleMap) {
   // Create the ClusterManager class and set the custom renderer.
   val clusterManager = ClusterManager<Place>(this, googleMap)
   clusterManager.renderer =
       PlaceRenderer(
           this,
           googleMap,
           clusterManager
       )

   // Set custom info window adapter
   clusterManager.markerCollection.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))

   // Add the places to the ClusterManager.
   clusterManager.addItems(places)
   clusterManager.cluster()

   // Set ClusterManager as the OnCameraIdleListener so that it
   // can re-cluster when zooming in and out.
   googleMap.setOnCameraIdleListener {
       clusterManager.onCameraIdle()
   }
}

Phương thức này tạo thực thể cho ClusterManager, chuyển trình kết xuất tuỳ chỉnh PlacesRenderer vào phương thức đó, thêm tất cả địa điểm và gọi phương thức cluster(). Ngoài ra, vì ClusterManager sử dụng phương thức setInfoWindowAdapter() trên đối tượng bản đồ, nên việc đặt cửa sổ thông tin tùy chỉnh sẽ phải được thực hiện trên đối tượng ClusterManager.markerCollection. Cuối cùng, vì bạn muốn nhóm lại để thay đổi khi người dùng kéo và thu phóng xung quanh bản đồ, nên OnCameraIdleListener được cung cấp cho googleMap, để khi máy ảnh chuyển sang chế độ không hoạt động, clusterManager.onCameraIdle() sẽ được gọi.

  1. Hãy tiếp tục và chạy ứng dụng này để xem các cửa hàng theo cụm mới!

9. Vẽ trên bản đồ

Trong khi bạn đã khám phá một cách vẽ trên bản đồ (bằng cách thêm các điểm đánh dấu), SDK Maps dành cho Android sẽ hỗ trợ nhiều cách khác mà bạn có thể vẽ để hiển thị thông tin hữu ích trên bản đồ.

Ví dụ: nếu bạn muốn biểu thị các tuyến đường và khu vực trên bản đồ, bạn có thể sử dụng đa đường và đa giác để hiển thị chúng trên bản đồ. Nếu muốn sửa hình ảnh khỏi bề mặt của mặt đất, bạn có thể sử dụng lớp phủ mặt đất.

Trong nhiệm vụ này, bạn sẽ học cách vẽ hình dạng, cụ thể là một vòng tròn, xung quanh một điểm đánh dấu mỗi khi được nhấn.

f98ce13055430352.png

Thêm trình xử lý lượt nhấp

Thông thường, cách thêm trình nghe lượt nhấp vào một điểm đánh dấu là chuyển trực tiếp trình nghe lượt nhấp vào đối tượng GoogleMap thông qua setOnMarkerClickListener(). Tuy nhiên, vì bạn đang sử dụng tính năng phân nhóm nên trình nghe lượt nhấp cần được cung cấp cho ClusterManager.

  1. Trong phương thức addClusteredMarkers() trong MainActivity, hãy tiếp tục và thêm dòng sau ngay sau lệnh gọi đến cluster().

MainActivity.addClusteredMarkers()

// Show polygon
clusterManager.setOnClusterItemClickListener { item ->
   addCircle(googleMap, item)
   return@setOnClusterItemClickListener false
}

Phương thức này thêm một trình nghe và gọi phương thức addCircle() mà bạn xác định tiếp theo. Cuối cùng, false được trả về từ phương thức này để cho biết rằng phương thức này không sử dụng sự kiện này.

  1. Tiếp theo, bạn cần xác định thuộc tính circle và phương thức addCircle() trong MainActivity.

MainActivity.addCircle()

private var circle: Circle? = null

/**
* Adds a [Circle] around the provided [item]
*/
private fun addCircle(googleMap: GoogleMap, item: Place) {
   circle?.remove()
   circle = googleMap.addCircle(
       CircleOptions()
           .center(item.latLng)
           .radius(1000.0)
           .fillColor(ContextCompat.getColor(this, R.color.colorPrimaryTranslucent))
           .strokeColor(ContextCompat.getColor(this, R.color.colorPrimary))
   )
}

Bạn đã đặt thuộc tính circle sao cho bất cứ khi nào nhấn vào điểm đánh dấu mới, thì vòng tròn trước đó sẽ bị xóa và một điểm mới sẽ được thêm vào. Lưu ý rằng API để thêm vòng tròn tương tự như việc thêm điểm đánh dấu.

  1. Hãy tiếp tục và chạy ứng dụng để xem các thay đổi này.

10. Điều khiển máy ảnh

Nhiệm vụ cuối cùng là xem một số bộ điều khiển máy ảnh để có thể tập trung vào chế độ xem xung quanh một khu vực nhất định.

Máy ảnh và chế độ xem

Nếu bạn nhận thấy khi chạy ứng dụng, máy ảnh sẽ hiển thị lục địa Châu Phi và bạn phải chăm chỉ và phóng to San Francisco để tìm các điểm đánh dấu mà bạn đã thêm. Mặc dù đây có thể là một cách thú vị để khám phá thế giới, nhưng sẽ không hữu ích nếu bạn muốn hiển thị các điểm đánh dấu ngay lập tức.

Để giúp bạn làm việc đó, bạn có thể đặt vị trí của máy ảnh theo phương thức lập trình để chế độ xem được căn giữa vị trí bạn muốn.

  1. Tiếp tục và thêm mã sau vào lệnh gọi getMapAsync() để điều chỉnh chế độ xem máy ảnh để được khởi chạy đến San Francisco khi ứng dụng được chạy.

MainActivity.onCreate()

mapFragment?.getMapAsync { googleMap ->
   // Ensure all places are visible in the map.
   googleMap.setOnMapLoadedCallback {
       val bounds = LatLngBounds.builder()
       places.forEach { bounds.include(it.latLng) }
       googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds.build(), 20))
   }
}

Trước tiên, setOnMapLoadedCallback() sẽ được gọi để quá trình cập nhật máy ảnh chỉ được thực hiện sau khi tải bản đồ. Bước này là cần thiết vì các thuộc tính của bản đồ, chẳng hạn như thứ nguyên, cần phải được tính toán trước khi thực hiện lệnh gọi cập nhật máy ảnh.

Trong hàm lambda, một đối tượng LatLngBounds mới được xây dựng để xác định khu vực hình chữ nhật trên bản đồ. Sản phẩm này được xây dựng dần dần bằng cách bao gồm tất cả giá trị của địa điểm LatLng trong đó để đảm bảo tất cả các địa điểm nằm trong ranh giới. Sau khi đối tượng này được tạo, phương thức moveCamera() trên GoogleMap sẽ được gọi và CameraUpdate sẽ được cung cấp cho đối tượng này thông qua CameraUpdateFactory.newLatLngBounds(bounds.build(), 20).

  1. Chạy ứng dụng và nhận thấy rằng máy ảnh hiện được khởi tạo ở San Francisco.

Nghe các thay đổi đối với máy ảnh

Ngoài việc sửa đổi vị trí của máy ảnh, bạn cũng có thể nghe thông tin cập nhật bằng máy ảnh khi người dùng di chuyển xung quanh bản đồ. Điều này có thể hữu ích nếu bạn muốn sửa đổi giao diện người dùng khi máy ảnh di chuyển xung quanh.

Để giải trí, bạn sửa đổi mã để làm cho các điểm đánh dấu bị mờ bất cứ khi nào máy ảnh được di chuyển.

  1. Trong phương thức addClusteredMarkers(), hãy tiếp tục và thêm các dòng sau vào cuối phương thức:

MainActivity.addClusteredMarkers()

// When the camera starts moving, change the alpha value of the marker to translucent.
googleMap.setOnCameraMoveStartedListener {
   clusterManager.markerCollection.markers.forEach { it.alpha = 0.3f }
   clusterManager.clusterMarkerCollection.markers.forEach { it.alpha = 0.3f }
}

Thao tác này sẽ thêm OnCameraMoveStartedListener để bất cứ khi nào máy ảnh bắt đầu di chuyển, tất cả các giá trị đánh dấu alpha

  1. Cuối cùng, để sửa đổi các điểm đánh dấu mờ trở về độ mờ khi máy ảnh dừng, hãy sửa đổi nội dung của setOnCameraIdleListener trong phương thức addClusteredMarkers() thành phương thức sau:

MainActivity.addClusteredMarkers()

googleMap.setOnCameraIdleListener {
   // When the camera stops moving, change the alpha value back to opaque.
   clusterManager.markerCollection.markers.forEach { it.alpha = 1.0f }
   clusterManager.clusterMarkerCollection.markers.forEach { it.alpha = 1.0f }

   // Call clusterManager.onCameraIdle() when the camera stops moving so that reclustering
   // can be performed when the camera stops moving.
   clusterManager.onCameraIdle()
}
  1. Hãy tiếp tục và chạy ứng dụng để xem kết quả!

11. KTX trên Maps

Đối với các ứng dụng Kotlin sử dụng một hoặc nhiều SDK Android của Nền tảng Google Maps, tiện ích Kotlin hoặc thư viện KTX có sẵn để giúp bạn tận dụng các tính năng về ngôn ngữ của Kotlin, chẳng hạn như coroutine, thuộc tính/hàm mở rộng và nhiều tính năng khác. Mỗi SDK của Google Maps có một thư viện KTX tương ứng như được hiển thị dưới đây:

Sơ đồ KTX của nền tảng Google Maps

Trong nhiệm vụ này, bạn sẽ sử dụng thư viện KTX và Maps utils cho KTX trong ứng dụng của mình, đồng thời tái cấu trúc các thao tác trước đó\39; triển khai để bạn có thể sử dụng các tính năng ngôn ngữ dành riêng cho Kotlin trong ứng dụng.

  1. Bao gồm các phần phụ thuộc KTX trong tệp build.gradle cấp ứng dụng

Vì ứng dụng sử dụng cả SDK Maps cho Android và SDK Maps cho Thư viện tiện ích Android, nên bạn sẽ cần đưa các thư viện KTX tương ứng vào các thư viện này. Bạn cũng sẽ dùng một tính năng có trong thư viện KX Lifecycle KTX trong nhiệm vụ này, vì vậy, hãy đưa cả phần phụ thuộc đó vào tệp build.gradle ở cấp ứng dụng.

build.gradle

dependencies {
    // ...

    // Maps SDK for Android KTX Library
    implementation 'com.google.maps.android:maps-ktx:3.0.0'

    // Maps SDK for Android Utility Library KTX Library
    implementation 'com.google.maps.android:maps-utils-ktx:3.0.0'

    // Lifecycle Runtime KTX Library
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
}
  1. Dùng các hàm mở rộng GoogleMap.addMarker() và GoogleMap.addCircle()

Thư viện KTX cung cấp một API thay thế kiểu DSL cho GoogleMap.addMarker(MarkerOptions)GoogleMap.addCircle(CircleOptions) được sử dụng trong các bước trước. Để sử dụng các API nói trên, bạn cần xây dựng một lớp chứa các tùy chọn cho điểm đánh dấu hoặc vòng tròn, trong khi với các tùy chọn thay thế KTX, bạn có thể đặt các tùy chọn cho điểm đánh dấu hoặc vòng tròn trong hàm lambda mà bạn cung cấp.

Để sử dụng các API này, hãy cập nhật phương thức MainActivity.addMarkers(GoogleMap)MainActivity.addCircle(GoogleMap):

MainActivity.addMarkers(GoogleMap)

/**
 * Adds markers to the map. These markers won't be clustered.
 */
private fun addMarkers(googleMap: GoogleMap) {
    places.forEach { place ->
        val marker = googleMap.addMarker {
            title(place.name)
            position(place.latLng)
            icon(bicycleIcon)
        }
        // Set place as the tag on the marker object so it can be referenced within
        // MarkerInfoWindowAdapter
        marker.tag = place
    }
}

MainActivity.addCircle(GoogleMap)

/**
 * Adds a [Circle] around the provided [item]
 */
private fun addCircle(googleMap: GoogleMap, item: Place) {
    circle?.remove()
    circle = googleMap.addCircle {
        center(item.latLng)
        radius(1000.0)
        fillColor(ContextCompat.getColor(this@MainActivity, R.color.colorPrimaryTranslucent))
        strokeColor(ContextCompat.getColor(this@MainActivity, R.color.colorPrimary))
    }
}

Việc viết lại các phương thức trên theo cách ngắn gọn hơn nhiều nhờ cách đọc bằng hàm hàm với trình nhận Kotlin.

  1. Dùng các hàm tạm ngưng tiện ích SupportMapFragment.aWaitMap() và GoogleMap.aWaitMapLoad()

Thư viện Maps KTX cũng cung cấp việc tạm ngưng phần mở rộng về hàm để dùng trong coroutine. Cụ thể, có các thay thế hàm tạm ngưng cho SupportMapFragment.getMapAsync(OnMapReadyCallback)GoogleMap.setOnMapLoadedCallback(OnMapLoadedCallback). Khi sử dụng các API thay thế này, bạn không cần chuyển các lệnh gọi lại và thay vào đó cho phép bạn nhận phản hồi của các phương thức này theo cách nối tiếp và đồng bộ.

Vì các phương thức này đang tạm ngưng các hàm, nên việc sử dụng các phương thức này sẽ phải xảy ra trong một coroutine. Thư viện Lifecycle Runtime KTX cung cấp một phần mở rộng để cung cấp phạm vi coroutine nhận biết vòng đời để coroutine được chạy và dừng tại sự kiện trong vòng đời thích hợp.

Khi kết hợp các khái niệm này, hãy cập nhật phương thức MainActivity.onCreate(Bundle):

MainActivity.onCreate(Gói)

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val mapFragment =
        supportFragmentManager.findFragmentById(R.id.map_fragment) as SupportMapFragment
    lifecycleScope.launchWhenCreated {
        // Get map
        val googleMap = mapFragment.awaitMap()

        // Wait for map to finish loading
        googleMap.awaitMapLoad()

        // Ensure all places are visible in the map
        val bounds = LatLngBounds.builder()
        places.forEach { bounds.include(it.latLng) }
        googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds.build(), 20))

        addClusteredMarkers(googleMap)
    }
}

Phạm vi coroutine của lifecycleScope.launchWhenCreated sẽ thực thi khối khi ít nhất hoạt động ở trạng thái được tạo. Ngoài ra, hãy lưu ý rằng các lệnh gọi để truy xuất đối tượng GoogleMap và để đợi bản đồ hoàn tất việc tải, đã được thay thế tương ứng bằng SupportMapFragment.awaitMap()GoogleMap.awaitMapLoad(). Việc tái cấu trúc mã sử dụng các hàm tạm ngưng này cho phép bạn viết mã dựa trên lệnh gọi lại tương đương theo cách tuần tự.

  1. Hãy tiếp tục và xây dựng lại ứng dụng bằng các thay đổi đã được tái cấu trúc của bạn!

12. Xin chúc mừng

Xin chúc mừng! Bạn đã tìm hiểu rất nhiều nội dung và hy vọng rằng bạn đã hiểu rõ hơn về các tính năng cốt lõi được cung cấp trong SDK Maps dành cho Android.

Tìm hiểu thêm

  • SDK địa điểm cho Android – khám phá tập hợp dữ liệu địa điểm phong phú để khám phá các doanh nghiệp xung quanh bạn.
  • android-maps-ktx – một thư viện nguồn mở cho phép bạn tích hợp với SDK Maps dành cho Android và SDK Maps dành cho Thư viện tiện ích Android theo cách phù hợp với Kotlin.
  • android-place-ktx – thư viện nguồn mở cho phép bạn tích hợp với SDK địa điểm dành cho Android theo cách thân thiện với Kotlin.
  • android-samples – Mã mẫu trên GitHub minh họa tất cả các tính năng có trong lớp học lập trình này và nhiều tính năng khác.
  • Các lớp học lập trình khác về Kotlin để xây dựng các ứng dụng Android thông qua Nền tảng Google Maps