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

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 Maps SDK cho Android với ứng dụng của bạn và sử dụng các tính năng cốt lõi của SDK 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, California, 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 Maps SDK cho Android để thêm Google Maps vào một ứng dụng Android.
  • Thêm, tuỳ chỉnh và nhóm các điểm đánh dấu.
  • Vẽ đường nhiều đoạn và đa giác trên bản đồ.
  • Điều khiển điểm nhìn của camera theo phương thức 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 Maps SDK 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à dự án có 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à dự án.

  1. Trong Cloud Console, hãy nhấp vào trình đơn thả xuống dự án rồi 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 các API và SDK của Google Maps Platform cần thiết 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 Nền tảng Google Maps đều cần có khoá API.

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 làm theo tất cả các bước để tự xây dựng giải pháp, hãy tiếp tục đọc.

  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 đây để tải mã nguồn xuống.

  1. Sau khi nhận được mã, hãy 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 để ứng dụng tải khi bạn chạy.

d1d068b5d4ae38b9.png

Thêm khoá API

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

  1. Để cung cấp thông tin này, hãy mở tệp có tên là 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 một 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 khoá API của bạn được coi là thông tin nhạy cảm và không nên được kiểm tra trong quá trình 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 trong toàn bộ ứng dụng, hãy thêm trình bổ trợ Secrets Gradle Plugin for Android vào tệp build.gradle của ứng dụng nằm trong thư mục app/ rồi 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 cần sửa đổi tệp build.gradle ở cấp dự án để thêm classpath 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 khoá mà bạn đã xác định trong tệp local.properties dưới dạng các biến bản dựng trong tệp kê khai Android và dưới dạng các biến trong lớp BuildConfig do Gradle tạo tại thời điểm tạo bản dựng. Việc sử dụng trình bổ trợ này sẽ xoá mã khởi động mà bạn cần để đọc các thuộc tính từ local.properties để có thể truy cập vào mã đó trong toàn bộ ứng dụng.

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

  1. Giờ đây, khoá API của bạn có thể được truy cập trong ứng dụng. Bước tiếp theo là thêm phần phụ thuộc Maps SDK for Android vào tệp build.gradle của ứng dụng.

Trong dự án khởi đầ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 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 vào AndroidManifest.xml để truyền vào khoá API mà bạn đã tạo ở bước trước. Để làm như vậy, hãy mở tệp này trong Android Studio và thêm thẻ meta-data sau đây vào bên trong đối tượng application trong tệp AndroidManifest.xml, 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 một tệp bố cục mới có tên là activity_main.xml trong thư mục app/src/main/res/layout/ rồi 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 một 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.

  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 đoạn mã sau để ghi đè phương thức onCreate, nhờ đó bạn có thể đặt nội dung của phương thức này bằng bố cục mới mà bạn vừa tạo.

MainActivity

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

5. Định kiểu bản đồ dựa trên đám mây (Không bắt buộc)

Bạn có thể tuỳ chỉnh kiểu bản đồ bằng tính năng Định 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 đồ có kiểu bản đồ được liên kết, hãy xem hướng dẫn về 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 Mã bản đồ vào ứng dụng

Để sử dụng mã bản đồ mà bạn đã tạo, hãy sửa đổi tệp activity_main.xml và truyền mã bản đồ của bạn trong 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 hoàn tất, hãy chạy ứng dụng để xem bản đồ theo kiểu mà bạn đã chọn!

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

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

bc5576877369b554.png

Lấy một tham chiếu đến GoogleMap

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

MainActivity.onCreate()

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

Quá trình triển khai trước tiên sẽ tìm 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 một giá trị tham chiếu, lệnh gọi getMapAsync() sẽ được gọi, sau đó truyền vào một lambda. Đây là nơi đối tượng GoogleMap được truyền. Bên trong hàm lambda này, lệnh gọi phương thức addMarkers() sẽ được gọi (phương thức này sẽ được xác định trong thời gian ngắn).

Lớp được cung cấp: PlacesReader

Trong dự án khởi đầu, lớp PlacesReader đã được cung cấp cho bạn. Lớp này đọc danh sách 49 địa điểm được lưu trữ trong một 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 ở San Francisco, California, Hoa Kỳ.

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

PlacesReader

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ác cửa hàng xe đạp, hãy thêm một thuộc tính trong MainActivity có tên là places và xác định thuộc tính đó 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 một PlacesReader, phương thức này sẽ trả về một List<Place>. Place có một thuộc tính gọi là name (tên của địa điểm) và latLng (toạ độ nơi địa điểm đó nằ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 địa điểm đã được tải vào bộ nhớ, bước tiếp theo là biểu thị những đị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 đó 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 danh sách places, sau đó gọi phương thức addMarker() trên đối tượng GoogleMap được cung cấp. Bạn có thể tuỳ chỉnh chính điểm đánh dấu bằng cách tạo điểm đánh dấu thông qua việc khởi tạo một đối tượng MarkerOptions. Trong trường hợp này, tiêu đề và vị trí của điểm đánh dấu được cung cấp, lần lượt đại diện cho tên cửa hàng xe đạp và toạ độ của cửa hàng đó.

  1. Hãy chạy ứng dụng và chuyển đến San Francisco để xem các điểm đánh dấu mà bạn vừa thêm!

7. Tuỳ chỉnh điểm đánh dấu

Có một số lựa chọn tuỳ chỉnh cho các điểm đánh dấu mà bạn vừa thêm để giúp các điểm này 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ố tính năng đó bằng cách tuỳ chỉnh hình ảnh của từng điểm đánh dấu cũng như cửa sổ thông tin xuất hiện khi một điểm đánh dấu được nhấn.

a26f82802fe838e9.png

Thêm một 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 mã (nếu được đặt). Bạn có thể tuỳ chỉnh để thông tin này hiển thị thêm thông tin, chẳng hạn như địa chỉ và điểm xếp hạng của địa điểm.

Tạo marker_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. Để làm như vậ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 New > Layout Resource File (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 vào 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 sẽ được mở rộng sau để 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 đây (đoạn mã này sẽ thêm 3 TextViews trong một nhóm khung hiển thị LinearLayout dọc) rồi ghi đè mã mặc định trong tệp.

marker_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 một phương thức triển khai InfoWindowAdapter

Sau khi tạo tệp bố cục cho cửa sổ thông tin tuỳ chỉnh, bước tiếp theo là triển khai giao diện GoogleMap.InfoWindowAdapter. Giao diện này chứa 2 phương thức là 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 đầu tiên dùng để tuỳ chỉnh chính cửa sổ, còn phương thức thứ hai dùng để tuỳ 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à tuỳ chỉnh giá trị trả về của getInfoContents() trong khi trả về giá trị rỗng trong getInfoWindow(), cho biết rằng bạn nên sử dụng cửa sổ mặc định.

  1. Tạo một tệp Kotlin mới tên là MarkerInfoWindowAdapter trong cùng 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 khung hiển thị dự án trong Android Studio, sau đó chọn New (Mới) > Kotlin File/Class (Tệp/Lớp Kotlin).

3975ba36eba9f8e1.png

  1. Trong hộp thoại, hãy nhập MarkerInfoWindowAdapter và giữ cho File (Tệp) được đánh dấu.

992235af53d3897f.png

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

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(), Marker được cung cấp trong phương thức sẽ được truyền đến một loại Place và nếu không thể truyền, phương thức sẽ trả về giá trị rỗng (bạn chưa đặt thuộc tính thẻ trên Marker, nhưng bạn sẽ làm việc đó ở bước tiếp theo).

Tiếp theo, bố cục marker_info_contents.xml sẽ được mở rộng, sau đó đặt văn bản trên TextViews chứa thẻ Place.

Cập nhật MainActivity

Để kết hợp tất cả các thành phần mà bạn đã tạo cho đến nay, bạn cần thêm 2 dòng vào lớp MainActivity.

Trước tiên, để truyền InfoWindowAdapter, MarkerInfoWindowAdapter tuỳ chỉnh, 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 phiên bản mới của MarkerInfoWindowAdapter.

  1. Hãy thực hiện việc này bằng cách thêm mã sau sau lệnh gọi phương thức addMarkers() bên trong biểu thức lambda getMapAsync().

MainActivity.onCreate()

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

Cuối cùng, bạn cần đặt từng Đị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. Để làm việc đó, hãy sửa đổi lệnh gọi places.forEach{} trong hàm addMarkers() bằng đoạn mã 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 hình ảnh điểm đánh dấu tuỳ chỉnh

Tuỳ chỉnh hình ảnh điểm đánh dấu là một trong những cách thú vị để cho biế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 sẽ hiển thị xe đạp thay vì các điểm đánh dấu màu đỏ mặc định để biểu thị từng cửa hàng trên bản đồ. Dự án khởi đầu có 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 tuỳ chỉnh trên điểm đánh dấu

Khi có biểu tượng xe đạp có thể vẽ bằng vectơ, bước tiếp theo là đặt biểu tượng đó làm biểu tượng của từng điểm đánh dấu trên bản đồ. MarkerOptions có một phương thức icon, nhận một 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ẽ được mà bạn vừa thêm thành một BitmapDescriptor. Một tệp có tên là BitMapHelper có trong dự án khởi đầu chứa một hàm trợ giúp có tên là vectorToBitmap(). Hàm này sẽ thực hiện đúng như vậy.

BitmapHelper

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 một Context, một giá trị nhận dạng tài nguyên có thể vẽ, cũng như một số nguyên màu và tạo ra một biểu thị BitmapDescriptor của giá trị nhận dạng đó.

Bằng cách sử dụng phương thức trợ giúp, hãy khai báo một thuộc tính mới có tên là bicycleIcon và đưa ra định nghĩa sau đây: MainActivity.bicycleIcon

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 colorPrimary được xác định trước trong ứng dụng của bạn, sau đó dùng màu đó để tạo sắc thái cho biểu tượng xe đạp và trả về biểu tượng đó dưới dạng BitmapDescriptor.

  1. Bằng cách sử dụng thuộc tính này, hãy gọi phương thức icon của MarkerOptions trong phương thức addMarkers() để hoàn tất việc tuỳ chỉnh biểu tượng. Khi đó, thuộc tính điểm đánh dấu sẽ có dạ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ập nhật!

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

Tuỳ thuộc vào mức độ phóng to bản đồ, có thể bạn nhận thấy các điểm đánh dấu mà bạn đã thêm bị chồng lên nhau. Các điểm đánh dấu chồng lên nhau rất khó tương tác và tạo ra nhiều nhiễu, ảnh hưởng đến khả năng sử dụng ứng dụng của bạn.

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 nhóm lại gần nhau, thì bạn nên triển khai tính năng nhóm điểm đánh dấu. Với tính năng phân cụm, khi bạn phóng to và thu nhỏ bản đồ, các điểm đánh dấu ở gần nhau sẽ được phân cụm lại với nhau như thế này:

f05e1ca27ff42bf6.png

Để triển khai tính năng này, bạn cần có sự trợ giúp của Thư viện tiện ích Maps SDK cho Android.

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

Thư viện tiện ích Maps SDK dành cho Android được tạo ra để mở rộng chức năng của Maps SDK dành cho Android. Thư viện 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 cụm điểm đánh dấu, bản đồ nhiệt, hỗ trợ KML và GeoJson, mã hoá và giải mã đường nhiều đoạn, cũng như một số hàm trợ giúp liên quan đến hình học cầu.

Cập nhật build.gradle

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

  1. Hãy 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. Sau khi thêm dòng này, bạn phải thực hiện đồng bộ hoá 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 cụm

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

  1. Triển khai giao diện ClusterItem.
  2. Tạo lớp con cho lớp DefaultClusterRenderer.
  3. Tạo một 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 một điểm đánh dấu có thể phân cụm trên bản đồ đều cần 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. Tiếp theo, hãy mở tệp Place.kt và sửa đổi tệp đó như sau:

Đị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 LatLng của địa điểm.
  • getTitle(), đại diện cho tên của địa điểm
  • getSnippet(), đại diện cho địa chỉ của địa điểm.

Tạo lớp con cho lớp DefaultClusterRenderer

Lớp chịu trách nhiệm triển khai tính năng phân cụ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 di chuyển và thu phóng trên bản đồ. Theo mặc định, nó đi kèm với trình kết xuất mặc định, DefaultClusterRenderer, triển khai ClusterRenderer. Đối với các trường hợp đơn giản, điều này là đủ. Tuy nhiên, trong trường hợp của bạn, vì cần tuỳ chỉnh các điểm đánh dấu, nên bạn cần mở rộng lớp này và thêm các chế độ tuỳ chỉnh vào đó.

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

PlaceRenderer

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 kết xuất trên bản đồ. Tại đây, bạn có thể cung cấp các chế độ tuỳ chỉnh thông qua MarkerOptions – trong trường hợp này, chế độ này sẽ đặ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 được kết xuất 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 các mục

Cuối cùng, để tính năng phân cụm hoạt động, bạn cần sửa đổi MainActivity để tạo một phiên bản ClusterManager và cung cấp các phần phụ thuộc cần thiết cho phiên bản đó. ClusterManager xử lý việc thêm các điểm đánh dấu (các đối tượng ClusterItem) nội bộ, vì vậy, 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 được uỷ quyền cho ClusterManager. Ngoài ra, ClusterManager cũng gọi setInfoWindowAdapter() nội bộ, vì vậy, bạn sẽ phải đặt một cửa sổ thông tin tuỳ chỉnh trên đối tượng MarkerManager.Collection của ClusterManger.

  1. Để bắt đầu, hãy sửa đổi nội dung của lambda trong lệnh gọi getMapAsync() trong MainActivity.onCreate(). Tiếp tục và nhận xét lệnh gọi đến addMarkers()setInfoWindowAdapter(), thay vào đó, hãy gọi một phương thức có tên là addClusteredMarkers() mà bạn sẽ 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 sẽ tạo một ClusterManager, truyền trình kết xuất tuỳ chỉnh PlacesRenderer vào đó, 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 bạn sẽ phải đặt cửa sổ thông tin tuỳ chỉnh trên đối tượng ClusterManager.markerCollection. Cuối cùng, vì bạn muốn việc phân cụm thay đổi khi người dùng di chuyển và thu phóng trên bản đồ, nên OnCameraIdleListener được cung cấp cho googleMap, sao cho khi camera ở trạng thái rảnh, clusterManager.onCameraIdle() sẽ được gọi.

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

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

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

Ví dụ: nếu muốn biểu thị các tuyến đường và khu vực trên bản đồ, bạn có thể dùng đường nhiều đoạn và đa giác để hiển thị các tuyến đường và khu vực này trên bản đồ. Hoặc nếu muốn cố định hình ảnh vào bề 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ẽ tìm hiểu cách vẽ các hình dạng, cụ thể là một hình tròn, xung quanh một điểm đánh dấu bất cứ khi nào người dùng nhấn vào điểm đánh dấu đó.

f98ce13055430352.png

Thêm trình nghe lượt nhấp

Thông thường, cách bạn thêm trình nghe lượt nhấp vào một điểm đánh dấu là truyề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 cụm, nên bạn cần cung cấp trình nghe lượt nhấp cho ClusterManager.

  1. Trong phương thức addClusteredMarkers() trong MainActivity, hãy 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 sẽ thêm một trình nghe và gọi phương thức addCircle() mà bạn sẽ 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 chưa 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))
   )
}

Thuộc tính circle được đặt để bất cứ khi nào một điểm đánh dấu mới được nhấn, vòng tròn trước đó sẽ bị xoá và một vòng tròn mới sẽ được thêm vào. Lưu ý rằng API để thêm một hình tròn khá giống với API để thêm một điểm đánh dấu.

  1. Bây giờ, hãy chạy ứng dụng để xem các thay đổi.

10. Điều khiển camera

Trong nhiệm vụ cuối cùng, bạn xem xét một số chế độ điều khiển camera để có thể lấy nét khung hình xung quanh một khu vực nhất định.

Camera và chế độ xem

Nếu bạn nhận thấy khi chạy ứng dụng, camera sẽ hiển thị lục địa Châu Phi và bạn phải xoay và thu phóng một cách tỉ mỉ đến 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 cách này không hữu ích nếu bạn muốn hiển thị ngay các điểm đánh dấu.

Để hỗ trợ việc này, bạn có thể đặt vị trí của camera 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 theo, hãy thêm đoạn mã sau vào lệnh gọi getMapAsync() để điều chỉnh khung hiển thị camera sao cho khung hiển thị này được khởi động ở San Francisco khi ứng dụng 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 camera chỉ diễn ra sau khi bản đồ được tải. Bước này là cần thiết vì các thuộc tính bản đồ, chẳng hạn như kích thước, cần được tính toán trước khi thực hiện lệnh gọi cập nhật camera.

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

  1. Chạy ứng dụng và lưu ý rằng camera hiện đã được khởi chạy ở San Francisco.

Nghe các thay đổi về camera

Ngoài việc sửa đổi vị trí camera, bạn cũng có thể theo dõi các bản cập nhật camera khi người dùng di chuyển trên 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 camera di chuyển.

Để cho vui, bạn có thể sửa đổi mã để các điểm đánh dấu trở nên trong mờ bất cứ khi nào camera di chuyển.

  1. Trong phương thức addClusteredMarkers(), hãy 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 một OnCameraMoveStartedListener để bất cứ khi nào máy ảnh bắt đầu di chuyển, tất cả các giá trị alpha của điểm đánh dấu (cả cụm và điểm đánh dấu) đều được sửa đổi thành 0.3f để các điểm đánh dấu xuất hiện mờ.

  1. Cuối cùng, để sửa đổi các điểm đánh dấu mờ thành không mờ khi camera dừng, hãy sửa đổi nội dung của setOnCameraIdleListener trong phương thức addClusteredMarkers() thành nội dung 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. Maps KTX

Đố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, bạn có thể sử dụng các thư viện KTX hoặc tiện ích Kotlin để tận dụng các tính năng của ngôn ngữ Kotlin như coroutine, hàm/thuộc tính mở rộng, v.v. Mỗi Google Maps SDK đều có một thư viện KTX tương ứng như minh hoạ bên dưới:

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

Trong nhiệm vụ này, bạn sẽ sử dụng các thư viện Maps KTX và Maps Utils KTX cho ứng dụng của mình, đồng thời tái cấu trúc các cách triển khai của các nhiệm vụ trước để 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. Đưa các phần phụ thuộc KTX vào tệp build.gradle cấp ứng dụng

Vì ứng dụng sử dụng cả Maps SDK cho Android và Maps SDK cho Thư viện tiện ích Android, nên bạn sẽ cần phải thêm các thư viện KTX tương ứng cho những thư viện này. Bạn cũng sẽ sử dụng một tính năng có trong thư viện AndroidX Lifecycle KTX trong tác vụ này, vì vậy, hãy thêm 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. Sử dụng các hàm tiện ích GoogleMap.addMarker() và GoogleMap.addCircle()

Thư viện Maps KTX cung cấp một API thay thế theo kiểu DSL cho GoogleMap.addMarker(MarkerOptions)GoogleMap.addCircle(CircleOptions) được dùng trong các bước trước. Để sử dụng các API nêu trên, bạn cần tạo một lớp chứa các lựa chọn cho một điểm đánh dấu hoặc hình tròn, trong khi với các lựa chọn thay thế KTX, bạn có thể đặt các lựa chọn cho điểm đánh dấu hoặc hình tròn trong 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 này sẽ giúp bạn đọc dễ hiểu hơn nhiều nhờ giá trị cố định của hàm có trình nhận trong Kotlin.

  1. Sử 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 các tiện ích hàm tạm ngưng để sử dụng trong coroutine. Cụ thể, có các lựa chọn thay thế hàm tạm ngưng cho SupportMapFragment.getMapAsync(OnMapReadyCallback)GoogleMap.setOnMapLoadedCallback(OnMapLoadedCallback). Việc sử dụng các API thay thế này giúp bạn không cần truyền lệnh gọi lại mà vẫn có thể nhận được phản hồi của các phương thức này theo cách tuần tự và đồng bộ.

Vì đây là các hàm tạm ngưng, nên bạn cần sử dụng chúng trong một coroutine. Thư viện Lifecycle Runtime KTX cung cấp một tiện ích để cung cấp các phạm vi coroutine nhận biết vòng đời, nhờ đó, các coroutine được chạy và dừng tại sự kiện vòng đời thích hợp.

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(Bundle)

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

  1. Hãy tiếp tục và tạo lại ứng dụng bằng các thay đổi đã tái cấu trúc!

12. Xin chúc mừng

Xin chúc mừng! Bạn đã tìm hiểu 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ó trong Maps SDK dành cho Android.

Tìm hiểu thêm

  • Places SDK for Android – khám phá bộ dữ liệu phong phú về địa điểm để tìm thấy 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 Maps SDK cho Android và Maps SDK cho Thư viện tiện ích Android theo cách thân thiện với Kotlin.
  • android-place-ktx – một thư viện nguồn mở cho phép bạn tích hợp với Places SDK for Android theo cách thân thiện với Kotlin.
  • android-samples – mã mẫu trên GitHub minh hoạ tất cả các tính năng được đề cập 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 để tạo ứng dụng Android bằng Google Maps Platform